"""FlowDeck — Kanban léger intégré à Gitea.""" from __future__ import annotations import asyncio import logging from contextlib import asynccontextmanager from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from starlette.exceptions import HTTPException as _StarHTTPException from starlette.middleware.sessions import SessionMiddleware from app.config import settings from app.db import init_db from app.middleware.csrf import CSRFMiddleware from app.middleware.security import ContentSecurityPolicyMiddleware, RateLimitMiddleware from app.routers import ( admin, agent, api, auth, board, collections, dashboard, export, library, my_tasks, notes, onboarding, projects, public_api, search, security, sharing, sidebar_config, sync, webhooks, workspace, ) from app.routers.api_v2 import router as api_v2_router from app.routers.api_v2_agent import router as api_v2_agent_router from app.routers.audit import router as audit_router from app.routers.automations import router as automations_router from app.routers.collaboration import router as collaboration_router from app.routers.emoji import router as emoji_router from app.routers.gitea import router as gitea_router from app.routers.github_routes import router as github_router from app.routers.governance import router as governance_router from app.routers.imports import page_router as import_page_router from app.routers.imports import router as imports_router from app.routers.meetings import router as meetings_router from app.routers.notifications import router as notifications_router from app.routers.permissions import router as permissions_router from app.routers.realtime import router as realtime_router from app.routers.scim import router as scim_router from app.routers.search_ai import router as search_ai_router from app.routers.sites import router as sites_router from app.routers.sso import router as sso_router from app.routers.web_clipper import api_router as web_clipper_api_router from app.routers.web_clipper import router as web_clipper_router from app.routers.webauthn import router as webauthn_router from app.routers.wiki import router as wiki_router from app.routers.workers import router as workers_router from app.services.webhook_outbound import init_webhook_tables logging.basicConfig( level=getattr(logging, settings.log_level.upper(), logging.INFO), format="%(asctime)s [%(levelname)s] %(message)s", ) logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(_app: FastAPI): init_db() init_webhook_tables() import os import secrets from app.db import get_conn from app.password_utils import hash_password # A8 : plus de mot de passe admin codé en dur — mot de passe aléatoire au # premier boot (affiché une fois) ou FLOWDECK_ADMIN_PASSWORD ; re-seed si absent. with get_conn() as conn: if not conn.execute("SELECT 1 FROM users WHERE login='admin'").fetchone(): admin_pw = os.environ.get("FLOWDECK_ADMIN_PASSWORD") or secrets.token_urlsafe(12) conn.execute( "INSERT INTO users (login, full_name, email, password_hash, is_admin) VALUES ('admin', 'Admin', '', ?, 1)", (hash_password(admin_pw),), ) conn.commit() logger.warning( "Premier démarrage : compte admin créé, mot de passe = %s " "(définir FLOWDECK_ADMIN_PASSWORD pour le fixer)", admin_pw, ) # ── FlowDeck Agent (v4.10.0): scheduled custom-agent triggers ── from app.routers.agent import agent_scheduler scheduler_task = asyncio.create_task(agent_scheduler()) # ── Automations (v5.1.0): cron trigger scheduler ── from app.services.automations import automation_scheduler automation_task = asyncio.create_task(automation_scheduler()) # ── Backups (v5.2.0): automatic daily SQLite snapshot ── from app.services.backup import backup_scheduler backup_task = asyncio.create_task(backup_scheduler()) # ── Forge projects sync (v5.2.0): hourly refresh of `projects` ── from app.services.projects import project_sync_scheduler projects_task = asyncio.create_task(project_sync_scheduler()) # ── Global trash purge (v5.4.0): daily cleanup of 30-day-old pages ── from app.services.trash import trash_purge_scheduler trash_task = asyncio.create_task(trash_purge_scheduler()) # ── Reminders (v5.8.0): due-reminder scan for database rows ── from app.services.reminders import reminder_scheduler reminder_task = asyncio.create_task(reminder_scheduler()) # ── Semantic search (v6.9.0): incremental vector indexing ── from app.services.semantic_search import semantic_index_scheduler semantic_task = asyncio.create_task(semantic_index_scheduler()) # ── Calendar sync (v7.1.0): external calendars every 15 min ── from app.services.calendar_sync import calendar_sync_scheduler calendar_task = asyncio.create_task(calendar_sync_scheduler()) # ── Webhooks outbound (v6.4.0): retry failed deliveries ── from app.services.webhook_outbound import webhook_retry_scheduler webhook_task = None if settings.webhook_retry_enabled: webhook_task = asyncio.create_task(webhook_retry_scheduler()) logger.info("FlowDeck v%s started on port %d", dashboard._get_app_version(), settings.app_port) try: yield finally: _tasks = (scheduler_task, automation_task, backup_task, projects_task, trash_task, reminder_task, semantic_task, calendar_task) if webhook_task is not None: _tasks = _tasks + (webhook_task,) for task in _tasks: task.cancel() for task in _tasks: try: await task except asyncio.CancelledError: pass app = FastAPI( title="FlowDeck", version="7.3.1", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, ) app.add_middleware(SessionMiddleware, secret_key=settings.app_secret_key, max_age=3600) app.add_middleware(CSRFMiddleware) app.add_middleware(ContentSecurityPolicyMiddleware) app.add_middleware(RateLimitMiddleware) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) app.include_router(auth.router) app.include_router(sso_router) app.include_router(dashboard.router) app.include_router(board.router) app.include_router(notes.router) app.include_router(projects.router) app.include_router(projects.backups_router) app.include_router(api.router) app.include_router(webhooks.router) app.include_router(collections.router) app.include_router(my_tasks.router) app.include_router(workspace.router) app.include_router(library.router) app.include_router(admin.router) app.include_router(gitea_router) app.include_router(github_router) app.include_router(public_api.router) app.include_router(sharing.router) app.include_router(sidebar_config.router) app.include_router(export.router) app.include_router(notifications_router) app.include_router(automations_router) app.include_router(collaboration_router) app.include_router(emoji_router) app.include_router(realtime_router) app.include_router(agent.router) app.include_router(search.router) app.include_router(security.router) app.include_router(onboarding.router) app.include_router(sync.router) app.include_router(imports_router) app.include_router(import_page_router) app.include_router(permissions_router) app.include_router(web_clipper_api_router) app.include_router(web_clipper_router) app.include_router(api_v2_router) app.include_router(api_v2_agent_router) app.include_router(sites_router) app.include_router(search_ai_router) app.include_router(workers_router) app.include_router(meetings_router) # v7.2.0 — enterprise admin app.include_router(scim_router) app.include_router(webauthn_router) app.include_router(audit_router) app.include_router(governance_router) # v7.3.0 — teamspaces + verified wiki app.include_router(wiki_router) app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/manifest.json") async def pwa_manifest(): """Serve the static web manifest from disk (same URL as before v6.0.0).""" from fastapi.responses import FileResponse return FileResponse("static/manifest.json", media_type="application/manifest+json") @app.get("/sw.js") async def service_worker(): """Serve the PWA service worker at top-level scope (/).""" from fastapi.responses import FileResponse return FileResponse("static/sw.js", media_type="application/javascript") # ═══════════ API aliases (v4.0.1) ═══════════ @app.get("/api/csrf-token") async def csrf_token_endpoint(request: Request): """Return a fresh CSRF token. Used by the frontend to auto-recover from 403.""" import secrets from fastapi.responses import JSONResponse token = secrets.token_hex(32) response = JSONResponse({"csrf_token": token}) response.set_cookie( "csrf_token", token, httponly=False, samesite="lax", max_age=86400, path="/", ) return response @app.get("/api/pages") async def api_pages_alias(request: Request): """Alias /api/pages → /board/api/pages for API path consistency.""" from fastapi.responses import RedirectResponse qs = str(request.url.query) target = f"/board/api/pages{'?' + qs if qs else ''}" return RedirectResponse(url=target, status_code=307) @app.post("/api/pages") async def api_pages_post_alias(request: Request): """Alias POST /api/pages → /board/api/pages for API path consistency.""" from fastapi.responses import RedirectResponse return RedirectResponse(url="/board/api/pages", status_code=307) # ═══════════ Styled 404 handler ═══════════ NOT_FOUND_HTML = """