diff --git a/api/dashboard.py b/api/dashboard.py index e6eda35..d387597 100644 --- a/api/dashboard.py +++ b/api/dashboard.py @@ -8,10 +8,29 @@ from models.user import db, UserProfile from flask_cors import CORS from datetime import datetime, timezone +from flask import request, jsonify, g +from firebase_admin import auth as admin_auth # Create the Blueprint for dashboard dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/api/dashboard') +# protect dashboard route with Firebase ID token +@dashboard_bp.before_request +def dashboard_auth_gate(): + if request.method == "OPTIONS": + return None + header = request.headers.get("Authorization", "") + if not header.startswith("Bearer "): + return jsonify({"error": "Unauthorized"}), 401 + token = header.split(" ", 1)[1].strip() + try: + g.firebase_user = admin_auth.verify_id_token(token) + except Exception: + return jsonify({"error": "Unauthorized"}), 401 + + + + # Dashboard Endpoints # # Endpoint to get the user's dashboard data, including profile info and metrics like VO2 Max diff --git a/api/goals.py b/api/goals.py index 0a85b04..b001067 100644 --- a/api/goals.py +++ b/api/goals.py @@ -1,10 +1,31 @@ from flask import Blueprint, request, jsonify from models.goal import db, Goal # Correctly import db and Goal from datetime import datetime +from flask import request, jsonify, g +from firebase_admin import auth as admin_auth + # Create the Blueprint for goals goals_bp = Blueprint('goals', __name__, url_prefix='/api/goals') +# protect goals route with Firebase ID token +@goals_bp.before_request +def goals_auth_gate(): + + if request.method == "OPTIONS": + return None + header = request.headers.get("Authorization", "") + if not header.startswith("Bearer "): + return jsonify({"error": "Unauthorized"}), 401 + token = header.split(" ", 1)[1].strip() + try: + g.firebase_user = admin_auth.verify_id_token(token) + except Exception: + return jsonify({"error": "Unauthorized"}), 401 + + + + # Create Goal @goals_bp.route('/', methods=['POST']) def create_goal(): diff --git a/api/profile.py b/api/profile.py index 34398de..23a1ed3 100644 --- a/api/profile.py +++ b/api/profile.py @@ -2,10 +2,27 @@ from flask import Blueprint, jsonify, request from models.user import db, UserProfile from flask_cors import CORS +from flask import request, jsonify, g +from firebase_admin import auth as admin_auth api = Blueprint('profile_api', __name__) +@api.before_request +def profile_auth_gate(): + # Allow CORS preflight through if you use it + if request.method == "OPTIONS": + return None + header = request.headers.get("Authorization", "") + if not header.startswith("Bearer "): + return jsonify({"error": "Unauthorized"}), 401 + + token = header.split(" ", 1)[1].strip() + try: + g.firebase_user = admin_auth.verify_id_token(token) + except Exception: + return jsonify({"error": "Unauthorized"}), 401 + # Profile Endpoints # # These routes are used by the frontend to fetch/update the user profile. @@ -48,4 +65,4 @@ def update_profile(): db.session.commit() # Save changes to the database - return jsonify({'message': 'Profile updated successfully'}), 200 + return jsonify({'message': 'Profile updated successfully'}), 200 \ No newline at end of file diff --git a/app.py b/app.py index e62cf7e..fa5415b 100644 --- a/app.py +++ b/app.py @@ -1,46 +1,33 @@ -from flask import Flask, jsonify, session, render_template, request, redirect -from flask_migrate import Migrate +from flask import Flask, jsonify, session, render_template, request, redirect, g from flask_cors import CORS from api.routes import api from api.goals import goals_bp from api.profile import api as profile_api from api.dashboard import dashboard_bp -from api.body_insight import body_insight_bp -from api.activity import activity_bp -from api.sessions import sessions_bp from models import db from dotenv import load_dotenv -from api.sync import sync_bp -from logging.handlers import RotatingFileHandler -import logging +from services.firebase_admin import init_firebase_admin +from middlewares.auth import require_auth +from sqlalchemy import inspect, text +from models.user import UserProfile +from services.user_link import upsert_user_from_claims import os import pyrebase + # Import scripts here from scripts.add_default_user import add_default_user -# ensure a folder for logs exists -os.makedirs('logs', exist_ok=True) - # Load environment variables from .env file load_dotenv() app = Flask(__name__) -CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}}) - +CORS(app, resources={r"/api/*": { + "origins": ["http://localhost:5173", "https://app.redbackfit.com"], + "allow_headers": ["Content-Type", "Authorization"], + "methods": ["GET","POST","PUT","DELETE","OPTIONS"] +}}) -# set up a rotating file handler -file_handler = RotatingFileHandler( - 'logs/app.log', maxBytes=10*1024, backupCount=5 -) -file_handler.setFormatter(logging.Formatter( - '%(asctime)s %(levelname)s in %(module)s: %(message)s' -)) -file_handler.setLevel(logging.INFO) - -app.logger.addHandler(file_handler) -app.logger.setLevel(logging.INFO) -app.logger.info("🟢 App startup complete") # Firebase configuration config = { @@ -59,28 +46,34 @@ # Flask config app.secret_key = os.getenv("SECRET_KEY", "default_secret_key") -app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL", "sqlite:///reflexionpro_backend.db") +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL", "sqlite:///goals.db") app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -# Initialize database +# DB init and create user db.init_app(app) -migrate = Migrate(app, db) - with app.app_context(): db.create_all() - - # Call function to create the default user add_default_user() + + insp = inspect(db.engine) + existing_cols = {c['name'] for c in insp.get_columns(UserProfile.__tablename__)} + if 'firebase_uid' not in existing_cols: + db.session.execute(text('ALTER TABLE user_profile ADD COLUMN firebase_uid VARCHAR(128)')) + db.session.commit() + + + db.session.execute(text('CREATE INDEX IF NOT EXISTS ix_user_profile_firebase_uid ON user_profile(firebase_uid)')) + db.session.commit() + + + # Register Blueprints app.register_blueprint(api, url_prefix='/api') app.register_blueprint(goals_bp, url_prefix='/api/goals') app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard') app.register_blueprint(profile_api, url_prefix='/api/profile') -app.register_blueprint(sync_bp, url_prefix='/api/synced') -app.register_blueprint(body_insight_bp, url_prefix='/api/body_insight') -app.register_blueprint(activity_bp, url_prefix='/api/activity') -app.register_blueprint(sessions_bp, url_prefix='/api/sessions') + # Main index route (login + welcome) @app.route('/', methods=['GET', 'POST']) def index(): @@ -110,7 +103,6 @@ def signup(): return redirect('/home') except Exception as e: error = "Signup failed. " + str(e).split("]")[-1].strip().strip('"') - return render_template('signup.html', error=error) # Logout route @@ -130,8 +122,42 @@ def home(): def hello(): return jsonify({'message': 'Hello from Flask!'}), 200 +@app.get("/api/me") +@require_auth +def me(): + claims = getattr(g, "firebase_user", {}) + user = upsert_user_from_claims(claims) + return { + "uid": claims.get("uid"), + "email": claims.get("email"), + "db_user_id": getattr(user, "id", None), + "email_verified": claims.get("email_verified", False), + "provider": (claims.get("firebase") or {}).get("sign_in_provider") + }, 200 + + +@app.errorhandler(401) +def _401(e): + return jsonify(error="Unauthorized"), 401 + +@app.errorhandler(403) +def _403(e): + return jsonify(error="Forbidden"), 403 + +@app.errorhandler(404) +def _404(e): + return jsonify(error="Not found"), 404 + +@app.errorhandler(500) +def _500(e): + return jsonify(error="Server error"), 500 + + + +#Entry point if __name__ == '__main__': debug_mode = os.getenv("FLASK_DEBUG", "False").lower() == "true" app.run(debug=debug_mode, port=int(os.getenv("PORT", 5000))) + diff --git a/middlewares/__init__.py b/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/middlewares/auth.py b/middlewares/auth.py new file mode 100644 index 0000000..e7e2b31 --- /dev/null +++ b/middlewares/auth.py @@ -0,0 +1,18 @@ +from functools import wraps +from flask import request, jsonify, g +from firebase_admin import auth as admin_auth + +def require_auth(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + auth_header = request.headers.get("Authorization", "") + if not auth_header.startswith("Bearer "): + return jsonify({"error": "Unauthorized"}), 401 + token = auth_header.split(" ", 1)[1].strip() + try: + g.firebase_user = admin_auth.verify_id_token(token) + except Exception: + return jsonify({"error": "Unauthorized"}), 401 + return fn(*args, **kwargs) + return wrapper + diff --git a/requirements.txt b/requirements.txt index 60e1b68..4127ecb 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/firebase_admin.py b/services/firebase_admin.py new file mode 100644 index 0000000..5d54c52 --- /dev/null +++ b/services/firebase_admin.py @@ -0,0 +1,13 @@ +import os +import firebase_admin +from firebase_admin import credentials +from dotenv import load_dotenv + +load_dotenv() + +def init_firebase_admin(): + """Initialize Firebase Admin once for the process.""" + if not firebase_admin._apps: + cred_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", "instance/firebase-admin.json") + cred = credentials.Certificate(cred_path) + firebase_admin.initialize_app(cred) diff --git a/services/user_link.py b/services/user_link.py new file mode 100644 index 0000000..26580de --- /dev/null +++ b/services/user_link.py @@ -0,0 +1,23 @@ +from models import db +from models.user import UserProfile + +def upsert_user_from_claims(claims): + uid = claims["uid"] + email = claims.get("email") + + u = UserProfile.query.filter_by(firebase_uid=uid).one_or_none() + if not u and email: + u = UserProfile.query.filter_by(account=email).one_or_none() + if u: + u.firebase_uid = uid + + if not u: + u = UserProfile(firebase_uid=uid, account=email or "", name="New User", + birthDate="N/A", gender="N/A", avatar="") + db.session.add(u) + + if email and u.account != email: + u.account = email + + db.session.commit() + return u