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
|
import os
|
||||||
|
|
||||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://patchcenter:PatchCenter2026!@localhost:5432/patchcenter_db")
|
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")
|
SECRET_KEY = os.getenv("SECRET_KEY", "slpm-patchcenter-secret-key-2026-change-in-production")
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 # 8 heures
|
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 import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
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)
|
engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_size=10)
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
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 fastapi import Request
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from .auth import decode_token
|
from .auth import decode_token
|
||||||
from .database import SessionLocal
|
from .database import SessionLocal, SessionLocalDemo
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db(request: Request = None):
|
||||||
db = SessionLocal()
|
"""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:
|
try:
|
||||||
yield db
|
yield db
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@ -2,7 +2,8 @@ from fastapi import APIRouter, Request, Depends, Form
|
|||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
from sqlalchemy import text
|
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 ..auth import verify_password, create_access_token, hash_password
|
||||||
from ..services.audit_service import log_login, log_logout, log_login_failed
|
from ..services.audit_service import log_login, log_logout, log_login_failed
|
||||||
from ..config import APP_NAME, APP_VERSION
|
from ..config import APP_NAME, APP_VERSION
|
||||||
@ -17,52 +18,63 @@ async def login_page(request: Request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@router.post("/login")
|
@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(...),
|
||||||
row = db.execute(text("SELECT id, username, password_hash, role, is_active FROM users WHERE LOWER(username) = LOWER(:u)"),
|
mode: str = Form("reel")):
|
||||||
{"u": username}).fetchone()
|
# Select DB based on mode
|
||||||
if not row:
|
factory = SessionLocalDemo if mode == "demo" else SessionLocal
|
||||||
log_login_failed(db, request, username)
|
db = factory()
|
||||||
db.commit()
|
|
||||||
return templates.TemplateResponse("login.html", {
|
|
||||||
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Utilisateur inconnu"
|
|
||||||
})
|
|
||||||
if not row.is_active:
|
|
||||||
log_login_failed(db, request, username)
|
|
||||||
db.commit()
|
|
||||||
return templates.TemplateResponse("login.html", {
|
|
||||||
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Compte desactive"
|
|
||||||
})
|
|
||||||
try:
|
try:
|
||||||
ok = verify_password(password, row.password_hash)
|
row = db.execute(text("SELECT id, username, password_hash, role, is_active FROM users WHERE LOWER(username) = LOWER(:u)"),
|
||||||
except Exception:
|
{"u": username}).fetchone()
|
||||||
ok = False
|
if not row:
|
||||||
if not ok:
|
log_login_failed(db, request, username)
|
||||||
log_login_failed(db, request, username)
|
db.commit()
|
||||||
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Utilisateur inconnu"
|
||||||
|
})
|
||||||
|
if not row.is_active:
|
||||||
|
log_login_failed(db, request, username)
|
||||||
|
db.commit()
|
||||||
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Compte desactive"
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
ok = verify_password(password, row.password_hash)
|
||||||
|
except Exception:
|
||||||
|
ok = False
|
||||||
|
if not ok:
|
||||||
|
log_login_failed(db, request, username)
|
||||||
|
db.commit()
|
||||||
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Mot de passe incorrect"
|
||||||
|
})
|
||||||
|
# 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()
|
db.commit()
|
||||||
return templates.TemplateResponse("login.html", {
|
# Redirect
|
||||||
"request": request, "app_name": APP_NAME, "version": APP_VERSION, "error": "Mot de passe incorrect"
|
perms = db.execute(text("SELECT module FROM user_permissions WHERE user_id = :uid"), {"uid": row.id}).fetchall()
|
||||||
})
|
modules = {r.module for r in perms}
|
||||||
token = create_access_token({"sub": row.username, "role": row.role, "uid": row.id})
|
redirect_url = "/quickwin" if modules == {"quickwin"} else "/dashboard"
|
||||||
user = {"sub": row.username, "role": row.role, "uid": row.id}
|
response = RedirectResponse(url=redirect_url, status_code=303)
|
||||||
log_login(db, request, user)
|
response.set_cookie(key="access_token", value=token, httponly=True, samesite="lax", max_age=3600)
|
||||||
db.commit()
|
return response
|
||||||
# Redirect qw_only users to quickwin
|
finally:
|
||||||
perms = db.execute(text("SELECT module FROM user_permissions WHERE user_id = :uid"), {"uid": row.id}).fetchall()
|
db.close()
|
||||||
modules = {r.module for r in perms}
|
|
||||||
if modules == {"quickwin"}:
|
|
||||||
redirect_url = "/quickwin"
|
|
||||||
else:
|
|
||||||
redirect_url = "/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
|
|
||||||
|
|
||||||
@router.get("/logout")
|
@router.get("/logout")
|
||||||
async def logout(request: Request, db=Depends(get_db)):
|
async def logout(request: Request):
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if user:
|
if user:
|
||||||
log_logout(db, request, user)
|
mode = user.get("mode", "reel")
|
||||||
db.commit()
|
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 = RedirectResponse(url="/login", status_code=302)
|
||||||
response.delete_cookie("access_token")
|
response.delete_cookie("access_token")
|
||||||
return response
|
return response
|
||||||
|
|||||||
@ -20,6 +20,17 @@
|
|||||||
<label class="text-xs text-gray-400 block mb-1">Mot de passe</label>
|
<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">
|
<input type="password" name="password" required class="w-full" placeholder="password">
|
||||||
</div>
|
</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>
|
<button type="submit" class="btn-primary w-full py-2 rounded-md">Connexion</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="text-center text-xs text-gray-600 mt-4">SANEF — Direction des Systèmes d'Information</p>
|
<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