Flowdeck 7.85.6 — porte centrale unique (phase 2) : écran réduit à l'entrée ObsiGate SSO + porte de secours administrateur par variables d'environnement, désactivée par défaut
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped

This commit is contained in:
Spark
2026-11-07 04:52:51 +00:00
parent 31fe0c181a
commit cf2e39e15f
7 changed files with 472 additions and 25 deletions
+14 -6
View File
@@ -158,15 +158,23 @@ def get_reglages_acces(_admin=Depends(admin_required)):
def put_reglages_acces(_admin=Depends(admin_required), body: dict = Body(default={})):
"""Modifie les réglages d'accès — réversible à tout moment.
Corps accepté : ``{"inscription_locale_ouverte": true|false}``.
Fermée (défaut) : aucun compte local ne se crée sur l'instance,
les comptes naissent dans le Hub Dracodev. La connexion des
comptes existants n'est jamais affectée.
Corps accepté : ``{"inscription_locale_ouverte": true|false,
"porte_centrale_active": true|false}`` (chaque clé optionnelle).
Inscription fermée (défaut) : aucun compte local ne se crée sur
l'instance, les comptes naissent dans le Hub Dracodev.
Porte centrale (défaut : inactive) : activée, seuls la voie
ObsiGate (SSO) et la porte de secours administrateur ouvrent
une session.
"""
from app.services import reglages_acces
kwargs = {}
if "inscription_locale_ouverte" in body:
return reglages_acces.set_reglages_acces(
inscription_locale_ouverte=bool(body["inscription_locale_ouverte"]))
kwargs["inscription_locale_ouverte"] = bool(
body["inscription_locale_ouverte"])
if "porte_centrale_active" in body:
kwargs["porte_centrale_active"] = bool(body["porte_centrale_active"])
if kwargs:
return reglages_acces.set_reglages_acces(**kwargs)
return reglages_acces.get_reglages_acces()
+99 -6
View File
@@ -94,15 +94,64 @@ body{{background:#191919;color:#fff;font-family:-apple-system,BlinkMacSystemFont
</html>"""
def _rendre_page_connexion(html: str) -> str:
"""Rend la page de connexion en tenant compte du réglage #146.
def _page_porte_centrale() -> str:
"""Écran de connexion en mode porte centrale (phase 2).
Inscription fermée : l'onglet Register disparaît et un avis nomme
le Hub Dracodev (lien dérivé de l'hôte, comme les autres liens de
la suite). Inscription ouverte : page intégralement inchangée.
Ne présente **que** l'entrée centrale : le bouton ObsiGate (SSO),
bien visible. Aucun formulaire local, aucun onglet Register —
la porte de secours administrateur passe par le chemin discret
``?secours=1``, jamais proposé ici.
"""
from app.services import draco_urls
hub = draco_urls.hub_url()
return """<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlowDeck — Connexion</title>
<style>
body{background:#191919;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;}
.box{background:#222;border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:40px;width:100%;max-width:400px;}
.box h1{font-size:24px;margin:0 0 8px;}
.box p{color:rgba(255,255,255,.55);font-size:14px;line-height:1.6;margin:0 0 20px;}
.box a{color:#2383E2;}
.sso{display:flex;align-items:center;justify-content:center;gap:8px;width:100%;padding:14px;border-radius:8px;font-size:15px;font-weight:500;cursor:pointer;border:1px solid rgba(35,131,226,.6);background:#2383E2;color:#fff;text-decoration:none;box-sizing:border-box;}
.sso:hover{background:#2C8CEB;}
</style>
</head>
<body>
<div class="box">
<h1>FlowDeck</h1>
<p>Cette instance n'accepte que la connexion centrale de la suite
Dracodev. Les comptes se créent dans le
<a href="__HUB_URL__">Hub Dracodev</a>.</p>
<a class="sso" href="__SSO_LOGIN_URL__">🧭 Login with ObsiGate (SSO)</a>
</div>
</body>
</html>""".replace("__HUB_URL__", hub)
def _rendre_page_connexion(html: str, secours: bool = False) -> str:
"""Rend la page de connexion en tenant compte des réglages d'accès.
Porte centrale active (phase 2) : seul l'écran central est rendu
— sauf par le chemin discret de secours (``secours=True``), qui
rend le formulaire local en marquant ses envois ``secours``.
Inscription fermée (#146) : l'onglet Register disparaît et un
avis nomme le Hub Dracodev (lien dérivé de l'hôte, comme les
autres liens de la suite). Sinon : page intégralement inchangée.
"""
from app.services import draco_urls, reglages_acces
if reglages_acces.porte_centrale_active():
if not secours:
return _with_nonce(_page_porte_centrale())
html = html.replace(
"const body={email,password};",
"const body={email,password,secours:true};")
return _with_nonce(html)
if reglages_acces.inscription_locale_ouverte():
return _with_nonce(html)
html = html.replace(
@@ -257,6 +306,9 @@ def register_page(request: Request):
"""
from app.services import reglages_acces
if reglages_acces.porte_centrale_active():
return HTMLResponse(_with_nonce(_page_porte_centrale()),
status_code=403)
if not reglages_acces.inscription_locale_ouverte():
return HTMLResponse(_page_inscription_fermee(), status_code=403)
return HTMLResponse(_with_nonce(LOCAL_LOGIN_HTML.replace(
@@ -283,7 +335,13 @@ def login(request: Request, provider: str = Query("gitea")):
# Local login page (POST handled by /auth/local-login)
from fastapi.responses import HTMLResponse
if provider == "local":
return HTMLResponse(_rendre_page_connexion(LOCAL_LOGIN_HTML), status_code=200)
# Chemin discret de la porte de secours (phase 2) :
# ?secours=1 rend le formulaire local même en mode porte
# centrale — jamais lié depuis l'écran principal.
secours = request.query_params.get("secours") == "1"
return HTMLResponse(
_rendre_page_connexion(LOCAL_LOGIN_HTML, secours=secours),
status_code=200)
# OAuth flow — check if provider is configured
from app.auth.providers import get_provider
@@ -341,6 +399,13 @@ def register(request: Request, body: dict = Body(default={})):
# naissent dans le Hub Dracodev — aucun compte local ne se crée
# ici. La connexion des comptes existants n'est pas touchée.
from app.services import draco_urls, reglages_acces
if reglages_acces.porte_centrale_active():
from fastapi.responses import JSONResponse
return JSONResponse(
{"error": reglages_acces.message_porte_centrale(),
"hub_url": draco_urls.hub_url()},
status_code=403,
)
if not reglages_acces.inscription_locale_ouverte():
from fastapi.responses import JSONResponse
return JSONResponse(
@@ -400,6 +465,34 @@ def local_login(request: Request, body: dict = Body(default={})):
if not email or not password:
return JSONResponse({"error": "Email and password required"}, status_code=400)
# Porte centrale unique (phase 2) : les identifiants locaux
# ordinaires ne sont plus acceptés — comptes historiques inclus.
# Seule exception : la porte de secours administrateur, par le
# drapeau explicite `secours` (posé par le chemin discret) et les
# identifiants des variables d'environnement. Les sessions déjà
# établies ne passent pas par ici et ne sont pas touchées.
from app.services import reglages_acces as _reglages_acces
if _reglages_acces.porte_centrale_active():
if not body.get("secours"):
return JSONResponse(
{"error": _reglages_acces.message_porte_centrale()},
status_code=403,
)
compte_secours = _reglages_acces.authentifier_secours(email, password)
if compte_secours is None:
return JSONResponse(
{"error": "Porte de secours : identifiants invalides."},
status_code=401,
)
session = SessionManager.create_session(compte_secours, request)
_log_login(compte_secours["id"], request)
response = JSONResponse({"status": "ok", "user": {
"login": compte_secours["login"],
"name": compte_secours["full_name"]}})
response.set_cookie("flowdeck_session", session, httponly=True,
max_age=86400 * 7, samesite="lax", path="/")
return response
with get_conn() as conn:
user = conn.execute("SELECT * FROM users WHERE login=?", (email,)).fetchone()
if not user: