Mode Demo/Reel au login + base patchcenter_demo

- Choix Production/Demo sur la page de login
- Base patchcenter_demo avec schema identique + 10 serveurs mpcz
- Le mode est stocke dans le JWT token
- La session DB bascule automatiquement selon le mode
This commit is contained in:
Pierre & Lumière 2026-04-10 19:39:35 +02:00
parent e96d79aae3
commit c2f3d669eb
5 changed files with 92 additions and 46 deletions

View File

@ -1,6 +1,7 @@
import os
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_db")
DATABASE_URL_DEMO = os.getenv("DATABASE_URL_DEMO", "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_demo")
SECRET_KEY = os.getenv("SECRET_KEY", "slpm-patchcenter-secret-key-2026-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 # 8 heures

View File

@ -1,7 +1,12 @@
"""SQLAlchemy engine et session"""
"""SQLAlchemy engine et session — supporte mode reel et demo"""
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from .config import DATABASE_URL
from .config import DATABASE_URL, DATABASE_URL_DEMO
# Production engine
engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_size=10)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Demo engine
engine_demo = create_engine(DATABASE_URL_DEMO, pool_pre_ping=True, pool_size=5)
SessionLocalDemo = sessionmaker(autocommit=False, autoflush=False, bind=engine_demo)

View File

@ -2,11 +2,28 @@
from fastapi import Request
from sqlalchemy import text
from .auth import decode_token
from .database import SessionLocal
from .database import SessionLocal, SessionLocalDemo
def get_db():
db = SessionLocal()
def get_db(request: Request = None):
"""Retourne la session DB selon le mode (demo/reel) stocke dans le cookie JWT."""
demo = False
if request:
user = get_current_user(request)
if user and user.get("mode") == "demo":
demo = True
factory = SessionLocalDemo if demo else SessionLocal
db = factory()
try:
yield db
finally:
db.close()
def get_db_for_login(demo: bool = False):
"""Session DB pour le login (avant que le cookie existe)."""
factory = SessionLocalDemo if demo else SessionLocal
db = factory()
try:
yield db
finally:

View File

@ -2,7 +2,8 @@ from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import text
from ..dependencies import get_db, get_current_user
from ..dependencies import get_current_user
from ..database import SessionLocal, SessionLocalDemo
from ..auth import verify_password, create_access_token, hash_password
from ..services.audit_service import log_login, log_logout, log_login_failed
from ..config import APP_NAME, APP_VERSION
@ -17,7 +18,12 @@ async def login_page(request: Request):
})
@router.post("/login")
async def login(request: Request, username: str = Form(...), password: str = Form(...), db=Depends(get_db)):
async def login(request: Request, username: str = Form(...), password: str = Form(...),
mode: str = Form("reel")):
# Select DB based on mode
factory = SessionLocalDemo if mode == "demo" else SessionLocal
db = factory()
try:
row = db.execute(text("SELECT id, username, password_hash, role, is_active FROM users WHERE LOWER(username) = LOWER(:u)"),
{"u": username}).fetchone()
if not row:
@ -42,27 +48,33 @@ async def login(request: Request, username: str = Form(...), password: str = For
return templates.TemplateResponse("login.html", {
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Mot de passe incorrect"
})
token = create_access_token({"sub": row.username, "role": row.role, "uid": row.id})
user = {"sub": row.username, "role": row.role, "uid": row.id}
# Include mode in JWT token
token = create_access_token({"sub": row.username, "role": row.role, "uid": row.id, "mode": mode})
user = {"sub": row.username, "role": row.role, "uid": row.id, "mode": mode}
log_login(db, request, user)
db.commit()
# Redirect qw_only users to quickwin
# Redirect
perms = db.execute(text("SELECT module FROM user_permissions WHERE user_id = :uid"), {"uid": row.id}).fetchall()
modules = {r.module for r in perms}
if modules == {"quickwin"}:
redirect_url = "/quickwin"
else:
redirect_url = "/dashboard"
redirect_url = "/quickwin" if modules == {"quickwin"} else "/dashboard"
response = RedirectResponse(url=redirect_url, status_code=303)
response.set_cookie(key="access_token", value=token, httponly=True, samesite="lax", max_age=3600)
return response
finally:
db.close()
@router.get("/logout")
async def logout(request: Request, db=Depends(get_db)):
async def logout(request: Request):
user = get_current_user(request)
if user:
mode = user.get("mode", "reel")
factory = SessionLocalDemo if mode == "demo" else SessionLocal
db = factory()
try:
log_logout(db, request, user)
db.commit()
finally:
db.close()
response = RedirectResponse(url="/login", status_code=302)
response.delete_cookie("access_token")
return response

View File

@ -20,6 +20,17 @@
<label class="text-xs text-gray-400 block mb-1">Mot de passe</label>
<input type="password" name="password" required class="w-full" placeholder="password">
</div>
<div>
<label class="text-xs text-gray-400 block mb-1">Mode</label>
<div class="flex gap-4">
<label class="flex items-center gap-1 text-sm text-gray-300 cursor-pointer">
<input type="radio" name="mode" value="reel" checked class="accent-cyan-500"> Production
</label>
<label class="flex items-center gap-1 text-sm text-gray-300 cursor-pointer">
<input type="radio" name="mode" value="demo" class="accent-yellow-500"> Demo
</label>
</div>
</div>
<button type="submit" class="btn-primary w-full py-2 rounded-md">Connexion</button>
</form>
<p class="text-center text-xs text-gray-600 mt-4">SANEF — Direction des Systèmes d'Information</p>