CoolFace
Apppublic

Rhinox13/chatapi

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
app.py153 linesDownload Raw Back to backend
1from __future__ import annotations
2
3from pathlib import Path
4
5from flask import Flask, abort, send_from_directory
6from flask_cors import CORS
7
8from .core import AppDependencies, AuthContext, settings
9from .repositories import ConversationStore, SystemConfigStore, UserStore, utc_now_iso
10from .services import ImageAssetStore, MessageRateLimiter, PendingTurnRegistry
11from .services.csrf import register_csrf_protection
12from .services.realtime import RealtimeBroker
13from .routes import (
14    register_admin_routes,
15    register_auth_routes,
16    register_conversation_routes,
17    register_realtime_routes,
18    register_response_routes,
19    register_statistics_routes,
20    register_upload_routes,
21    register_user_api_key_routes,
22    register_user_config_routes,
23)
24
25
26def create_app() -> Flask:
27    store = ConversationStore(settings.db_path)
28    system_config_store = SystemConfigStore(settings.db_path)
29    user_store = UserStore(settings.db_path)
30
31    # Ensure admin user exists
32    admin = user_store.get_user_by_username(settings.admin_username)
33    if admin is None:
34        user_store.create_user(settings.admin_username, settings.admin_password, role="admin")
35    elif admin.role != "admin":
36        # Promote to admin if somehow not admin
37        with user_store._connection() as conn:
38            conn.execute(
39                "UPDATE users SET role = 'admin', updated_at = ? WHERE id = ?",
40                (utc_now_iso(), admin.id),
41            )
42
43    session_secret = system_config_store.get_or_create_session_secret(settings.session_secret)
44
45    app = Flask(__name__)
46    app.config.update(SECRET_KEY=session_secret)
47    app.config.update(
48        SESSION_COOKIE_HTTPONLY=True,
49        SESSION_COOKIE_SAMESITE="Lax",
50    )
51    CORS(app, supports_credentials=True, origins=settings.cors_origins)
52    register_csrf_protection(app, cors_origins=settings.cors_origins)
53
54    auth = AuthContext(store, user_store)
55    pending_turns = PendingTurnRegistry()
56    message_rate_limiter = MessageRateLimiter()
57    image_store = ImageAssetStore(
58        settings.uploads_img_dir,
59        system_config_store=system_config_store,
60        user_store=user_store,
61    )
62    realtime = RealtimeBroker(store, user_store)
63    deps = AppDependencies(
64        settings=settings,
65        auth=auth,
66        store=store,
67        system_config_store=system_config_store,
68        user_store=user_store,
69        pending_turns=pending_turns,
70        message_rate_limiter=message_rate_limiter,
71        image_store=image_store,
72    )
73    app.extensions["chat_store"] = store
74    app.extensions["chat_system_config_store"] = system_config_store
75    app.extensions["chat_user_store"] = user_store
76    app.extensions["chat_realtime"] = realtime
77    app.extensions["chat_image_store"] = image_store
78
79    messages = store.iter_messages()
80    owner_lookup = {conversation.id: conversation.owner_id for conversation in store.list_conversations_all()}
81    image_store.backfill_owners_from_messages(messages, owner_lookup)
82    image_store.cleanup_orphans(messages)
83
84    @app.after_request
85    def apply_security_headers(response):
86        response.headers.setdefault("X-Frame-Options", "DENY")
87        response.headers.setdefault("X-Content-Type-Options", "nosniff")
88        response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
89        response.headers.setdefault("Cross-Origin-Opener-Policy", "same-origin")
90        return response
91
92    @app.get("/api/health")
93    def health():
94        return {"ok": True, "title": system_config_store.get_effective_title("ChatAPI")}
95
96    register_auth_routes(
97        app,
98        auth=auth,
99        settings=settings,
100        system_config_store=system_config_store,
101        user_store=user_store,
102    )
103    register_admin_routes(app, auth=auth, store=store, user_store=user_store)
104    register_user_config_routes(
105        app,
106        auth=auth,
107        user_store=user_store,
108        system_config_store=system_config_store,
109    )
110    register_user_api_key_routes(app, auth=auth, user_store=user_store)
111    register_conversation_routes(app, deps=deps)
112    register_realtime_routes(app, deps=deps)
113    register_response_routes(app, deps=deps)
114    register_statistics_routes(app, deps=deps)
115    register_upload_routes(app, deps=deps)
116
117    if settings.web_dist_dir:
118        web_dist_dir = settings.web_dist_dir
119        index_file = web_dist_dir / "index.html"
120        if not web_dist_dir.exists():
121            raise FileNotFoundError(f"WEB_DIST_DIR not found: {web_dist_dir}")
122        if not web_dist_dir.is_dir():
123            raise NotADirectoryError(f"WEB_DIST_DIR is not a directory: {web_dist_dir}")
124
125        def _send_dist_file(request_path: str):
126            candidate = (web_dist_dir / request_path).resolve()
127            try:
128                candidate.relative_to(web_dist_dir.resolve())
129            except ValueError as exc:
130                raise FileNotFoundError(request_path) from exc
131            if candidate.is_file():
132                relative_path = candidate.relative_to(web_dist_dir).as_posix()
133                return send_from_directory(web_dist_dir, relative_path)
134            raise FileNotFoundError(request_path)
135
136        @app.get("/", defaults={"request_path": ""})
137        @app.get("/<path:request_path>")
138        def serve_web_dist(request_path: str):
139            if request_path.startswith("api/") or request_path.startswith("v1/"):
140                abort(404)
141            if not request_path:
142                if not index_file.exists():
143                    abort(404)
144                return send_from_directory(web_dist_dir, "index.html")
145            try:
146                return _send_dist_file(request_path)
147            except FileNotFoundError:
148                if index_file.exists():
149                    return send_from_directory(web_dist_dir, "index.html")
150                abort(404)
151
152    return app
153