232 lines
7.8 KiB
Python
232 lines
7.8 KiB
Python
"""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.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,
|
|
webhooks,
|
|
workspace,
|
|
)
|
|
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.notifications import router as notifications_router
|
|
from app.routers.realtime import router as realtime_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()
|
|
from app.db import get_conn
|
|
from app.password_utils import hash_password
|
|
admin_hash = hash_password("FlowDeck2026!")
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"INSERT OR IGNORE INTO users (login, full_name, email, password_hash, is_admin) VALUES ('admin', 'Admin', '', ?, 1)",
|
|
(admin_hash,)
|
|
)
|
|
conn.commit()
|
|
|
|
# ── 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())
|
|
|
|
logger.info("FlowDeck v5.11.2 started on port %d", settings.app_port)
|
|
try:
|
|
yield
|
|
finally:
|
|
for task in (scheduler_task, automation_task, backup_task, projects_task, trash_task):
|
|
task.cancel()
|
|
for task in (scheduler_task, automation_task, backup_task, projects_task, trash_task):
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
|
|
app = FastAPI(
|
|
title="FlowDeck",
|
|
version="5.11.2",
|
|
docs_url="/docs" if settings.log_level == "DEBUG" else None,
|
|
redoc_url=None,
|
|
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(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.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
|
|
@app.get("/manifest.json")
|
|
async def pwa_manifest():
|
|
return {
|
|
"name": "FlowDeck",
|
|
"short_name": "FlowDeck",
|
|
"start_url": "/",
|
|
"display": "standalone",
|
|
"background_color": "#191919",
|
|
"theme_color": "#191919",
|
|
"icons": [{"src": "/static/icon-192.png", "sizes": "192x192", "type": "image/png"}],
|
|
}
|
|
|
|
|
|
# ═══════════ 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 = """<!DOCTYPE html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>404 — FlowDeck</title>
|
|
<style>
|
|
:root{--bg:#191919;--text:#e0e0e0;--text-dim:#999;--accent:#2383E2}
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);display:flex;align-items:center;justify-content:center;min-height:100vh;text-align:center}
|
|
.box h1{font-size:6rem;font-weight:800;opacity:.08;line-height:1}
|
|
.box p{font-size:18px;color:var(--text-dim);margin:8px 0 24px}
|
|
.box a{color:var(--accent);text-decoration:none;font-size:14px}
|
|
.box a:hover{text-decoration:underline}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="box">
|
|
<h1>404</h1>
|
|
<p>This page doesn't exist or has been moved.</p>
|
|
<a href="/">← Back to FlowDeck</a>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
|
|
@app.exception_handler(404)
|
|
async def not_found_handler(request: Request, exc):
|
|
"""Redirect 404 HTML pages to /workspaces. API routes still get JSON."""
|
|
# Preserve JSON 404 for all API-like paths (including /db/xxx/api)
|
|
if "/api" in request.url.path:
|
|
from fastapi.responses import JSONResponse
|
|
return JSONResponse({"detail": "Not found"}, status_code=404)
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse("/workspaces", status_code=302)
|