SysOps: approved_backup — 2026-08-16 11:01 UTC
This commit is contained in:
@@ -3,6 +3,8 @@ import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import Response
|
||||
@@ -38,6 +40,9 @@ from app.routes import (
|
||||
from app.routes.revenue_cockpit import api as revenue_cockpit_api
|
||||
from app.routes.admin_api import admin_router, ai_router, herman_api, voice_api
|
||||
from app.routes.settings_api import settings_router
|
||||
from app.routes import auth_routes
|
||||
from app import auth_users
|
||||
from app.config import settings as app_settings
|
||||
from app.routes.agents_api import router as agents_api_router
|
||||
from app.routes.marketing_api import router as marketing_api_router
|
||||
from app.routes.projects_api import router as projects_api_router
|
||||
@@ -59,6 +64,131 @@ class NoCacheStaticFiles(StaticFiles):
|
||||
|
||||
|
||||
app = FastAPI(title="Foodlinkk Command Center", version="2.5.0")
|
||||
|
||||
PUBLIC_PREFIXES = (
|
||||
"/login",
|
||||
"/logout",
|
||||
"/static/",
|
||||
"/sw.js",
|
||||
"/manifest.webmanifest",
|
||||
"/static/manifest.json",
|
||||
"/api/auth/login",
|
||||
)
|
||||
PUBLIC_EXACT = {"/favicon.ico", "/robots.txt"}
|
||||
|
||||
|
||||
def _is_public(path: str) -> bool:
|
||||
if path in PUBLIC_EXACT:
|
||||
return True
|
||||
for pref in PUBLIC_PREFIXES:
|
||||
if path == pref or path.startswith(pref):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AuthGateMiddleware:
|
||||
"""Require login; must run *inside* SessionMiddleware."""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope, receive, send):
|
||||
if scope["type"] != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
from starlette.requests import Request
|
||||
|
||||
request = Request(scope, receive=receive)
|
||||
path = request.url.path
|
||||
user = None
|
||||
try:
|
||||
uid = request.session.get("user_id")
|
||||
except Exception:
|
||||
uid = None
|
||||
if uid:
|
||||
try:
|
||||
user = auth_users.get_user_by_id(int(uid))
|
||||
except Exception:
|
||||
user = None
|
||||
if not user or not user.get("is_active"):
|
||||
try:
|
||||
request.session.clear()
|
||||
except Exception:
|
||||
pass
|
||||
user = None
|
||||
request.state.user = user
|
||||
|
||||
async def call_next():
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
if _is_public(path):
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
if not user:
|
||||
if path.startswith("/api/") or path.startswith("/ws"):
|
||||
response = JSONResponse({"detail": "Niet ingelogd"}, status_code=401)
|
||||
else:
|
||||
nxt = path
|
||||
if request.url.query:
|
||||
nxt = f"{path}?{request.url.query}"
|
||||
from urllib.parse import quote
|
||||
response = RedirectResponse(f"/login?next={quote(nxt)}", status_code=303)
|
||||
await response(scope, receive, send)
|
||||
return
|
||||
|
||||
if (
|
||||
not path.startswith("/api/")
|
||||
and not path.startswith("/ws")
|
||||
and not path.startswith("/static")
|
||||
and not auth_users.path_allowed(user, path)
|
||||
):
|
||||
response = RedirectResponse("/", status_code=303)
|
||||
await response(scope, receive, send)
|
||||
return
|
||||
|
||||
await self.app(scope, receive, send)
|
||||
|
||||
|
||||
# Order: first added = innermost. Session must be outermost so request.session works.
|
||||
app.add_middleware(AuthGateMiddleware)
|
||||
app.add_middleware(
|
||||
SessionMiddleware,
|
||||
secret_key=app_settings.SESSION_SECRET,
|
||||
session_cookie="flk_session",
|
||||
same_site="lax",
|
||||
https_only=False,
|
||||
max_age=app_settings.SESSION_MAX_AGE,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@app.api_route("/sw.js", methods=["GET", "HEAD"], include_in_schema=False)
|
||||
async def service_worker():
|
||||
"""Root-scoped service worker for installable PWA (Android + iOS Safari)."""
|
||||
sw_path = BASE_DIR / "static" / "sw.js"
|
||||
return FileResponse(
|
||||
sw_path,
|
||||
media_type="application/javascript; charset=utf-8",
|
||||
headers={
|
||||
"Service-Worker-Allowed": "/",
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.api_route("/manifest.webmanifest", methods=["GET", "HEAD"], include_in_schema=False)
|
||||
async def web_manifest():
|
||||
man_path = BASE_DIR / "static" / "manifest.json"
|
||||
return FileResponse(
|
||||
man_path,
|
||||
media_type="application/manifest+json",
|
||||
headers={"Cache-Control": "no-cache"},
|
||||
)
|
||||
|
||||
|
||||
app.mount("/static", NoCacheStaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
||||
|
||||
for r in (
|
||||
@@ -77,6 +207,7 @@ for r in (
|
||||
reports.router,
|
||||
voice.router,
|
||||
settings.router,
|
||||
auth_routes.router,
|
||||
browser.router,
|
||||
herman.router,
|
||||
studio.router,
|
||||
@@ -96,6 +227,8 @@ for r in (
|
||||
agents_api_router,
|
||||
marketing_api_router,
|
||||
projects_api_router,
|
||||
auth_routes.api_router,
|
||||
auth_routes.users_api,
|
||||
):
|
||||
app.include_router(r)
|
||||
|
||||
@@ -103,6 +236,10 @@ for r in (
|
||||
@app.on_event("startup")
|
||||
def on_startup() -> None:
|
||||
init_pool()
|
||||
try:
|
||||
auth_users.ensure_admin_seed()
|
||||
except Exception as e:
|
||||
print(f"auth seed warning: {e}")
|
||||
|
||||
|
||||
@app.on_event("shutdown")
|
||||
|
||||
Reference in New Issue
Block a user