Files
sales-trainer/backend/app/factory.py
Macky e8a95bf561 Add username-based login + mandatory first-time admin setup; push-ready
- User id/login = username (was email). Email is a separate settable field.
- Default admin: username admin / password 1234, must_setup=True.
- Login forces /setup on first login: set email + change password, then clears must_setup.
- New /api/auth/setup endpoint; JWT sub = username; admin routes use username.
- Frontend: Login uses username, router guard forces /setup, new Setup.vue (email + new
  password + confirm), i18n EN/TH.
- Tests: test_setup.py added; all suites adapted (m0/m1/routes/security/setup/e2e) PASS.
2026-08-07 17:56:59 +07:00

108 lines
3.8 KiB
Python

"""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("/<path:path>", 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")