Files
sejeteralo/backend/app/routers/auth.py
Yvv 5dc42af33e Add interactive citizen page with sidebar, display settings, and adaptive CSS
Major rework of the citizen-facing page:
- Chart + sidebar layout (auth/vote/countdown in right sidebar)
- DisplaySettings component (font size, chart density, color palettes)
- Adaptive CSS with clamp() throughout, responsive breakpoints at 480/768/1024
- Baseline charts zoomed on first tier for small consumption detail
- Marginal price chart with dual Y-axes (foyers left, €/m³ right)
- Key metrics banner (5 columns: recettes, palier, prix palier, prix médian, mon prix)
- Client-side p0/impacts computation, draggable median price bar
- Household dots toggle, vote overlay curves
- Auth returns volume_m3, vote captures submitted_at
- Cleaned header nav (removed Accueil/Super Admin for public visitors)
- Terminology: foyer for bills, électeur for votes
- 600m³ added to impact reference volumes
- Realistic seed votes (50 votes, 3 profiles)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 21:00:22 +01:00

86 lines
2.8 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import AdminUser, Household, Commune
from app.schemas import AdminLogin, CitizenVerify, Token, AdminUserCreate, AdminUserOut
from app.services.auth_service import (
verify_password, create_admin_token, create_citizen_token,
hash_password, require_super_admin,
)
router = APIRouter()
@router.post("/admin/login", response_model=Token)
async def admin_login(data: AdminLogin, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(AdminUser)
.options(selectinload(AdminUser.communes))
.where(AdminUser.email == data.email)
)
admin = result.scalar_one_or_none()
if not admin or not verify_password(data.password, admin.hashed_password):
raise HTTPException(status_code=401, detail="Identifiants invalides")
# For commune_admin, include their first commune slug
commune_slug = None
if admin.communes:
commune_slug = admin.communes[0].slug
return Token(
access_token=create_admin_token(admin),
role=admin.role,
commune_slug=commune_slug,
)
@router.post("/citizen/verify", response_model=Token)
async def citizen_verify(data: CitizenVerify, db: AsyncSession = Depends(get_db)):
result = await db.execute(
select(Household)
.join(Commune)
.where(Commune.slug == data.commune_slug, Household.auth_code == data.auth_code)
)
household = result.scalar_one_or_none()
if not household:
raise HTTPException(status_code=401, detail="Code invalide ou commune introuvable")
return Token(
access_token=create_citizen_token(household, data.commune_slug),
role="citizen",
commune_slug=data.commune_slug,
volume_m3=household.volume_m3,
)
@router.post("/admin/create", response_model=AdminUserOut)
async def create_admin(
data: AdminUserCreate,
db: AsyncSession = Depends(get_db),
current: AdminUser = Depends(require_super_admin),
):
existing = await db.execute(select(AdminUser).where(AdminUser.email == data.email))
if existing.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Email déjà utilisé")
admin = AdminUser(
email=data.email,
hashed_password=hash_password(data.password),
full_name=data.full_name,
role=data.role,
)
if data.commune_slugs:
for slug in data.commune_slugs:
result = await db.execute(select(Commune).where(Commune.slug == slug))
commune = result.scalar_one_or_none()
if commune:
admin.communes.append(commune)
db.add(admin)
await db.commit()
await db.refresh(admin)
return admin