""" main.py - Flask API with auth routes and module registry Domain routes are registered via the routes registry. """ import os import flask from flask_cors import CORS import core.auth as auth import core.users as users import core.postgres as postgres import api.routes.routines as routines_routes import api.routes.medications as medications_routes import api.routes.routine_steps_extended as routine_steps_extended_routes import api.routes.routine_sessions_extended as routine_sessions_extended_routes import api.routes.routine_templates as routine_templates_routes import api.routes.routine_stats as routine_stats_routes import api.routes.routine_tags as routine_tags_routes import api.routes.notifications as notifications_routes import api.routes.preferences as preferences_routes import api.routes.rewards as rewards_routes import api.routes.victories as victories_routes import api.routes.adaptive_meds as adaptive_meds_routes import api.routes.snitch as snitch_routes app = flask.Flask(__name__) CORS(app) ROUTE_MODULES = [ routines_routes, medications_routes, routine_steps_extended_routes, routine_sessions_extended_routes, routine_templates_routes, routine_stats_routes, routine_tags_routes, notifications_routes, preferences_routes, rewards_routes, victories_routes, adaptive_meds_routes, snitch_routes, ] def register_routes(module): """Register a routes module. Module should have a register(app) function.""" ROUTE_MODULES.append(module) # ── Auth Routes ──────────────────────────────────────────────────── @app.route("/api/register", methods=["POST"]) def api_register(): data = flask.request.get_json() username = data.get("username") password = data.get("password") if not username or not password: return flask.jsonify({"error": "username and password required"}), 400 result = users.registerUser(username, password, data) if result: return flask.jsonify({"success": True}), 201 else: return flask.jsonify({"error": "username taken"}), 409 @app.route("/api/login", methods=["POST"]) def api_login(): data = flask.request.get_json() username = data.get("username") password = data.get("password") if not username or not password: return flask.jsonify({"error": "username and password required"}), 400 token = auth.getLoginToken(username, password) if token: response = {"token": token} # Issue refresh token when trusted device is requested if data.get("trust_device"): import jwt as pyjwt payload = pyjwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"]) user_uuid = payload.get("sub") if user_uuid: response["refresh_token"] = auth.createRefreshToken(user_uuid) return flask.jsonify(response), 200 else: return flask.jsonify({"error": "invalid credentials"}), 401 @app.route("/api/refresh", methods=["POST"]) def api_refresh(): """Exchange a refresh token for a new access token.""" data = flask.request.get_json() refresh_token = data.get("refresh_token") if data else None if not refresh_token: return flask.jsonify({"error": "refresh_token required"}), 400 access_token, user_uuid = auth.refreshAccessToken(refresh_token) if access_token: return flask.jsonify({"token": access_token}), 200 else: return flask.jsonify({"error": "invalid or expired refresh token"}), 401 # ── User Routes ──────────────────────────────────────────────────── @app.route("/api/getUserUUID/", methods=["GET"]) def api_getUserUUID(username): header = flask.request.headers.get("Authorization", "") if not header.startswith("Bearer "): return flask.jsonify({"error": "missing token"}), 401 token = header[7:] if auth.verifyLoginToken(token, username): return flask.jsonify(users.getUserUUID(username)), 200 else: return flask.jsonify({"error": "unauthorized"}), 401 @app.route("/api/user/", methods=["GET"]) def api_getUser(userUUID): header = flask.request.headers.get("Authorization", "") if not header.startswith("Bearer "): return flask.jsonify({"error": "missing token"}), 401 token = header[7:] if auth.verifyLoginToken(token, userUUID=userUUID): user = postgres.select_one("users", {"id": userUUID}) if user: user.pop("password_hashed", None) return flask.jsonify(user), 200 else: return flask.jsonify({"error": "user not found"}), 404 else: return flask.jsonify({"error": "unauthorized"}), 401 @app.route("/api/user/", methods=["PUT"]) def api_updateUser(userUUID): header = flask.request.headers.get("Authorization", "") if not header.startswith("Bearer "): return flask.jsonify({"error": "missing token"}), 401 token = header[7:] if auth.verifyLoginToken(token, userUUID=userUUID): data = flask.request.get_json() result = users.updateUser(userUUID, data) if result: return flask.jsonify({"success": True}), 200 else: return flask.jsonify({"error": "no valid fields to update"}), 400 else: return flask.jsonify({"error": "unauthorized"}), 401 @app.route("/api/user/", methods=["DELETE"]) def api_deleteUser(userUUID): header = flask.request.headers.get("Authorization", "") if not header.startswith("Bearer "): return flask.jsonify({"error": "missing token"}), 401 token = header[7:] if auth.verifyLoginToken(token, userUUID=userUUID): data = flask.request.get_json() password = data.get("password") if not password: return flask.jsonify( {"error": "password required for account deletion"} ), 400 result = auth.unregisterUser(userUUID, password) if result: return flask.jsonify({"success": True}), 200 else: return flask.jsonify({"error": "invalid password"}), 401 else: return flask.jsonify({"error": "unauthorized"}), 401 # ── Health Check ─────────────────────────────────────────────────── @app.route("/health", methods=["GET"]) def health_check(): return flask.jsonify({"status": "ok"}), 200 def _seed_templates_if_empty(): """Auto-seed routine templates if the table is empty.""" try: count = postgres.count("routine_templates") if count == 0: import logging logging.getLogger(__name__).info( "No templates found, seeding from seed_templates.sql..." ) seed_path = os.path.join( os.path.dirname(__file__), "..", "config", "seed_templates.sql" ) if os.path.exists(seed_path): with open(seed_path, "r") as f: sql = f.read() with postgres.get_cursor() as cur: cur.execute(sql) logging.getLogger(__name__).info("Templates seeded successfully.") except Exception as e: import logging logging.getLogger(__name__).warning(f"Failed to seed templates: {e}") def _seed_rewards_if_empty(): """Auto-seed reward pool if the table is empty.""" try: count = postgres.count("reward_pool") if count == 0: import logging logging.getLogger(__name__).info( "No rewards found, seeding from seed_rewards.sql..." ) seed_path = os.path.join( os.path.dirname(__file__), "..", "config", "seed_rewards.sql" ) if os.path.exists(seed_path): with open(seed_path, "r") as f: sql = f.read() with postgres.get_cursor() as cur: cur.execute(sql) logging.getLogger(__name__).info("Rewards seeded successfully.") except Exception as e: import logging logging.getLogger(__name__).warning(f"Failed to seed rewards: {e}") if __name__ == "__main__": for module in ROUTE_MODULES: if hasattr(module, "register"): module.register(app) _seed_templates_if_empty() _seed_rewards_if_empty() app.run(host="0.0.0.0", port=5000)