Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions api/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions api/goals.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down
19 changes: 18 additions & 1 deletion api/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
98 changes: 62 additions & 36 deletions app.py
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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)))



Empty file added middlewares/__init__.py
Empty file.
18 changes: 18 additions & 0 deletions middlewares/auth.py
Original file line number Diff line number Diff line change
@@ -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

Binary file modified requirements.txt
Binary file not shown.
Empty file added services/__init__.py
Empty file.
13 changes: 13 additions & 0 deletions services/firebase_admin.py
Original file line number Diff line number Diff line change
@@ -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)
23 changes: 23 additions & 0 deletions services/user_link.py
Original file line number Diff line number Diff line change
@@ -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