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>
86 lines
2.8 KiB
Python
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
|