"""Flask application factory.""" from __future__ import annotations from pathlib import Path from flask import Flask from flask_cors import CORS from .auth.users import AuthError, UserStore from .config import Config def bootstrap_admin(users: UserStore) -> None: """Ensure a default org + super-admin exists on first run (no self-registration). Default admin logs in with username `admin` / `1234`, then MUST set an email and change the password on first login (`must_setup=True`). """ org = users.orgs.get_or_none("org-default") if org is None: org = users.create_org("Default Organization", org_id="org-default") if users.get_user_or_none("admin") is None: users.create_user( org_id=org["id"], username="admin", password="1234", name="Super Admin", role="super_admin", must_setup=True, ) print("[bootstrap] created default super-admin: admin / 1234 (must set email + password)") def create_app() -> Flask: Config.ensure_dirs() app = Flask(__name__) app.config["SECRET_KEY"] = Config.SECRET_KEY CORS(app, resources={r"/api/*": {"origins": "*"}}) from .api.auth_routes import auth_bp from .api.admin_routes import admin_bp from .api.group_routes import groups_bp from .api.chat_routes import chat_bp from .api.me_routes import me_bp from .api.analytics_routes import analytics_bp from .api.helpers import register_error_handlers app.register_blueprint(auth_bp, url_prefix="/api/auth") app.register_blueprint(admin_bp, url_prefix="/api/admin") app.register_blueprint(groups_bp, url_prefix="/api/groups") app.register_blueprint(chat_bp, url_prefix="/api/chat") app.register_blueprint(me_bp, url_prefix="/api/me") app.register_blueprint(analytics_bp, url_prefix="/api/analytics") register_error_handlers(app) from .auth.users import UserStore from .llm import LLMClient from .llm import LLMError from .services.groups import GroupStore from .services.sessions import SessionStore from .services.trainee import MyPersonaStore app.extensions["user_store"] = UserStore(Config.DATA_DIR) app.extensions["group_store"] = GroupStore(Config.DATA_DIR) app.extensions["session_store"] = SessionStore(Config.DATA_DIR) app.extensions["my_persona_store"] = MyPersonaStore(Config.DATA_DIR) try: app.extensions["llm"] = LLMClient() except LLMError as exc: print(f"[warn] LLM not configured yet: {exc}") app.extensions["llm"] = None bootstrap_admin(app.extensions["user_store"]) @app.get("/health") def health(): return {"status": "ok", "service": Config.APP_NAME} # Serve built Vue frontend if present (production single-app mode). _register_frontend(app) return app def _register_frontend(app: Flask) -> None: from flask import send_from_directory # repo-root frontend/dist (factory.py -> app/ -> backend/ -> repo root) dist = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist" if not (dist / "index.html").exists(): print(f"[info] frontend build not found at {dist}; API-only mode") return @app.route("/") def index(): return send_from_directory(dist, "index.html") @app.route("/", methods=["GET", "HEAD", "OPTIONS", "POST", "PUT", "DELETE", "PATCH"]) def assets(path: str): # Never let the SPA fallback shadow API/auth routes: return 404 for them. if path.startswith("api/") or path.startswith("health"): return ("not found", 404) candidate = dist / path if candidate.is_file(): return send_from_directory(dist, path) # SPA fallback for client-side routes return send_from_directory(dist, "index.html")