import logging import sys import os from flask import Flask, request, jsonify from api.admin_api import admin_api from api.auth_api import auth_api from api.child_api import child_api from api.child_override_api import child_override_api from api.chore_api import chore_api from api.chore_schedule_api import chore_schedule_api from api.image_api import image_api from api.kindness_api import kindness_api from api.penalty_api import penalty_api from api.reward_api import reward_api from api.task_api import task_api from api.tracking_api import tracking_api from api.user_api import user_api from api.push_subscription_api import push_subscription_api from api.digest_action_api import digest_action_api from config.version import get_full_version from db.default import initializeImages, createDefaultTasks, createDefaultRewards from events.broadcaster import Broadcaster from events.sse import sse_response_for_user, send_to_user from api.utils import get_current_user_id from utils.account_deletion_scheduler import start_deletion_scheduler from utils.digest_scheduler import start_digest_scheduler # Configure logging once at application startup logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S', stream=sys.stdout, force=True # Override any existing config ) logger = logging.getLogger(__name__) logging.getLogger("werkzeug").setLevel(logging.WARNING) logging.getLogger("events.sse").setLevel(logging.WARNING) app = Flask(__name__) #CORS(app, resources={r"/api/*": {"origins": ["http://localhost:3000", "http://localhost:5173"]}}) #Todo - add prefix to all these routes instead of in each blueprint app.register_blueprint(admin_api) app.register_blueprint(child_api) app.register_blueprint(child_override_api) app.register_blueprint(chore_api) app.register_blueprint(chore_schedule_api) app.register_blueprint(kindness_api) app.register_blueprint(penalty_api) app.register_blueprint(reward_api) app.register_blueprint(task_api) app.register_blueprint(image_api) app.register_blueprint(auth_api, url_prefix='/auth') app.register_blueprint(user_api) app.register_blueprint(tracking_api) app.register_blueprint(push_subscription_api) app.register_blueprint(digest_action_api) app.config.update( MAIL_SERVER='smtp.gmail.com', MAIL_PORT=587, MAIL_USE_TLS=True, MAIL_USERNAME='ryan.kegel@gmail.com', MAIL_PASSWORD='ruyj hxjf nmrz buar', MAIL_DEFAULT_SENDER='ryan.kegel@gmail.com', FRONTEND_URL=os.environ.get('FRONTEND_URL', 'https://localhost:5173'), # Dynamic via env var, defaults to localhost VAPID_CLAIMS_EMAIL=os.environ.get('VAPID_CLAIMS_EMAIL', 'admin@reward-app.local'), ) # Security: require SECRET_KEY and REFRESH_TOKEN_EXPIRY_DAYS from environment _secret_key = os.environ.get('SECRET_KEY') if not _secret_key: raise RuntimeError( 'SECRET_KEY environment variable is required. ' 'Set it to a random string (e.g. python -c "import secrets; print(secrets.token_urlsafe(64))")') app.config['SECRET_KEY'] = _secret_key _refresh_expiry = os.environ.get('REFRESH_TOKEN_EXPIRY_DAYS') if not _refresh_expiry: raise RuntimeError('REFRESH_TOKEN_EXPIRY_DAYS environment variable is required (e.g. 90).') try: app.config['REFRESH_TOKEN_EXPIRY_DAYS'] = int(_refresh_expiry) except ValueError: raise RuntimeError('REFRESH_TOKEN_EXPIRY_DAYS must be an integer.') _digest_token_secret = os.environ.get('DIGEST_TOKEN_SECRET') if not _digest_token_secret: raise RuntimeError( 'DIGEST_TOKEN_SECRET environment variable is required. ' 'Set it to a random string (e.g. python -c "import secrets; print(secrets.token_urlsafe(64))")') app.config['DIGEST_TOKEN_SECRET'] = _digest_token_secret _vapid_public_key = os.environ.get('VAPID_PUBLIC_KEY') _vapid_private_key = os.environ.get('VAPID_PRIVATE_KEY') if not _vapid_public_key or not _vapid_private_key: raise RuntimeError( 'VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY environment variables are required. ' 'Generate a key pair with: python -c "from py_vapid import Vapid; v=Vapid(); ' 'v.generate_keys(); print(v.public_key.public_bytes_raw().hex(), v.private_key.private_bytes_raw().hex())"' ) app.config['VAPID_PUBLIC_KEY'] = _vapid_public_key app.config['VAPID_PRIVATE_KEY'] = _vapid_private_key @app.route("/version") def api_version(): return jsonify({"version": get_full_version()}) @app.route("/events") def events(): user_id = get_current_user_id() if not user_id: return {"error": "Authentication required"}, 401 return sse_response_for_user(user_id) @app.route("/notify/") def notify_user(user_id): # Example trigger send_to_user(user_id, { "type": "notification", "message": f"Hello {user_id}, this is a private message!" }) return {"status": "sent"} def start_background_threads(): broadcaster = Broadcaster() broadcaster.daemon = True broadcaster.start() # TODO: implement users initializeImages() createDefaultTasks() createDefaultRewards() start_background_threads() start_deletion_scheduler() start_digest_scheduler(app) if __name__ == '__main__': app.run(debug=False, host='0.0.0.0', port=5000, threaded=True)