feat: administration — users, roles, stats, audit
FlowDeck CI / test (push) Failing after 4s
FlowDeck CI / docker (push) Has been skipped

DB:
- login_history table (user_id, ip, user_agent, timestamp)
- Migration v2.5: alter users + create login_history

Auth:
- Premier utilisateur = admin (is_admin=1)
- _log_login() après chaque connexion (register, local-login, OAuth)

Backend (app/routers/admin.py):
- admin_required middleware (vérifie is_admin)
- GET  /api/admin/users     → liste users + stats par user
- POST /api/admin/users     → créer user
- PUT  /api/admin/users/:id → modifier (name, email, password, admin, active)
- DELETE /api/admin/users/:id → supprimer + cascade
- GET  /api/admin/stats     → totaux globaux
- GET  /api/admin/audit     → historique login

Frontend (settings.html):
- Nav Admin visible seulement si userIsAdmin
- Users & Roles: stats cards, create/edit/delete users, table complète
- Audit Log: historique login avec date, IP, user-agent
- CSS professionnel: table-wrap, admin-table, stat-card, role-badge, status-dot
This commit is contained in:
2026-07-13 12:59:36 -04:00
parent ea591bd577
commit 97d6ad7a91
5 changed files with 455 additions and 23 deletions
+26 -2
View File
@@ -99,6 +99,7 @@ async def login(request: Request, provider: str = Query("gitea")):
user = conn.execute("SELECT * FROM users WHERE login='admin'").fetchone()
user_data = dict(user)
session = SessionManager.create_session(user_data)
_log_login(user_data["id"], request)
response = RedirectResponse(url="/workspaces", status_code=302)
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
@@ -136,13 +137,18 @@ async def register(request: Request):
if existing:
from fastapi.responses import JSONResponse
return JSONResponse({"error": "Account already exists"}, status_code=409)
# First user is admin
user_count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
is_admin = 1 if user_count == 0 else 0
conn.execute(
"INSERT INTO users (login, full_name, email, password_hash) VALUES (?, ?, ?, ?)",
(email, name, email, hash_password(password)),
"INSERT INTO users (login, full_name, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)",
(email, name, email, hash_password(password), is_admin),
)
conn.commit()
user = conn.execute("SELECT * FROM users WHERE login=?", (email,)).fetchone()
user_data = dict(user)
# Log login
_log_login(user_data["id"], request)
session = SessionManager.create_session(user_data)
from fastapi.responses import JSONResponse
response = JSONResponse({"status": "ok", "user": {"login": email, "name": name}})
@@ -199,6 +205,7 @@ async def local_login(request: Request):
)
conn.commit()
session = SessionManager.create_session(ud)
_log_login(ud["id"], request)
response = JSONResponse({"status": "ok", "user": {"login": ud["login"], "name": ud["full_name"]}})
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
@@ -265,6 +272,7 @@ async def callback(
# Create session
session = SessionManager.create_session(user_data)
_log_login(user_data["id"], request)
response = RedirectResponse(url="/workspaces", status_code=302)
response.set_cookie("flowdeck_session", session, httponly=True, max_age=86400 * 7, samesite="lax", path="/")
return response
@@ -286,3 +294,19 @@ async def current_user(request: Request):
if not user:
return {"authenticated": False}
return {"authenticated": True, "user": user}
# ── Helpers ──
def _log_login(user_id: int, request: Request):
"""Record login in history."""
try:
from app.db import get_conn
ip = request.client.host if request.client else ''
ua = request.headers.get('user-agent', '')[:500]
with get_conn() as conn:
conn.execute(
"INSERT INTO login_history (user_id, ip_address, user_agent) VALUES (?, ?, ?)",
(user_id, ip, ua),
)
conn.commit()
except Exception:
pass