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:
parent
e96d79aae3
commit
c2f3d669eb
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user