Découpe par concern de l'ancien app/routers/dashboard.py (2 735 lignes, 63 routes) en package `app/routers/dashboard/` : - 8 modules de routes : local_workspace 559 L (15 r.), pages_html 485 (6), account_settings 439 (16), workspace 321 (9), pages_api 240 (6), workspaces 131 (6), public 78 (1), account_api 77 (4) - _common.py (774 L) : les 15 helpers top-level INTERCALÉS dans l'ancien fichier + état (logger, _VERSION, WORKSPACE_COOKIE) - __init__.py : ordre d'enregistrement identique à l'origine, re-export complet (7 importateurs inchangés : main, board ×3, my_tasks, web_clipper, wiki, sites `_dash._render_blocks_public`, tests) + __all__ Preuve contractuelle : docs/openapi-v2.json régénéré = IDENTIQUE byte-à-byte (509 chemins, ordre préservé). Pièges rattrapés : - segment décorateur sans sa fonction → assert `def in seg` + récupération git (corps perdus en silence à la 1ʳᵉ exécution) - collision `settings` (section vs from app.config import settings → hasattr du fromlist) → renommée account_settings - WORKSPACE_COOKIE utilisé sans import dans workspaces.py (F821) - script __all__ mangeant la fin du fichier → __init__ réécrit Reste A28 : collections.py 2 622 L, board.py 2 101 L (lot 3). suite **1091/1091** · ruff OK · OpenAPI 509 identique · docs à jour
78 lines
2.4 KiB
Python
78 lines
2.4 KiB
Python
"""FlowDeck — Dashboard : account_api.
|
|
|
|
Découpe A28 de l'ancien app/routers/dashboard.py (2 735 lignes, 63 routes) — un module par concern, contrat inchangé.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Body, HTTPException, Request
|
|
|
|
from app.db import get_conn
|
|
|
|
from ._common import _require_user_id
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["dashboard"])
|
|
|
|
|
|
|
|
|
|
@router.put("/api/user/profile")
|
|
def update_profile(request: Request, body: dict = Body(default={})):
|
|
full_name = body.get("full_name", "").strip()
|
|
uid = _require_user_id(request)
|
|
with get_conn() as conn:
|
|
conn.execute("UPDATE users SET full_name=? WHERE id=?", (full_name, uid))
|
|
conn.commit()
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
@router.put("/api/user/password")
|
|
def update_password(request: Request, body: dict = Body(default={})):
|
|
from app.password_utils import hash_password, verify_password
|
|
password = body.get("password", "").strip()
|
|
if len(password) < 6:
|
|
return {"error": "Password must be at least 6 characters"}
|
|
uid = _require_user_id(request)
|
|
# A3 : le mot de passe actuel est exigé (session volée ≠ droit de changer le mdp).
|
|
current = body.get("current_password", "")
|
|
with get_conn() as conn:
|
|
row = conn.execute("SELECT password_hash FROM users WHERE id=?", (uid,)).fetchone()
|
|
if not row or not verify_password(current, row["password_hash"]):
|
|
raise HTTPException(403, "Current password is incorrect")
|
|
conn.execute("UPDATE users SET password_hash=? WHERE id=?", (hash_password(password), uid))
|
|
conn.commit()
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
@router.post("/api/user/token")
|
|
def generate_token(request: Request):
|
|
import secrets
|
|
uid = _require_user_id(request)
|
|
token = secrets.token_hex(32)
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO user_tokens (gitea_user_id, gitea_token, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)",
|
|
(uid, token),
|
|
)
|
|
conn.commit()
|
|
return {"token": f"fd_{token}"}
|
|
|
|
|
|
|
|
|
|
@router.delete("/api/user/forge/{provider}")
|
|
def disconnect_forge(request: Request, provider: str):
|
|
uid = _require_user_id(request)
|
|
with get_conn() as conn:
|
|
conn.execute(
|
|
"DELETE FROM user_oauth_tokens WHERE user_id=? AND provider=?", (uid, provider)
|
|
)
|
|
conn.commit()
|
|
return {"status": "ok"}
|