app backend prima

This commit is contained in:
2025-10-20 19:10:08 +02:00
commit 438255d27b
42 changed files with 4622 additions and 0 deletions

0
app/api/__init__.py Normal file
View File

171
app/api/allarmi.py Normal file
View File

@@ -0,0 +1,171 @@
from typing import Annotated, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc
from app.core.database import get_db
from app.models import Allarme, Utente, Sito
from app.schemas.allarme import AllarmeResponse, AllarmeList, AllarmeUpdate
from app.api.auth import get_current_user
router = APIRouter(prefix="/allarmi", tags=["Allarmi"])
@router.get("", response_model=AllarmeList)
async def get_allarmi(
current_user: Annotated[Utente, Depends(get_current_user)],
db: Session = Depends(get_db),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
sito_id: Optional[int] = None,
severita: Optional[str] = None,
stato: Optional[str] = None,
):
"""
Recupera gli allarmi per il cliente dell'utente autenticato.
Supporta filtri e paginazione.
"""
# Query base: solo allarmi dei siti del cliente dell'utente
query = (
db.query(Allarme)
.join(Sito)
.filter(Sito.cliente_id == current_user.cliente_id)
)
# Applica filtri opzionali
if sito_id:
query = query.filter(Allarme.sito_id == sito_id)
if severita:
query = query.filter(Allarme.severita == severita)
if stato:
query = query.filter(Allarme.stato == stato)
# Conta totale
total = query.count()
# Applica paginazione e ordinamento
allarmi = (
query.order_by(desc(Allarme.timestamp_rilevamento))
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return AllarmeList(
total=total,
items=allarmi,
page=page,
page_size=page_size,
)
@router.get("/{allarme_id}", response_model=AllarmeResponse)
async def get_allarme(
allarme_id: int,
current_user: Annotated[Utente, Depends(get_current_user)],
db: Session = Depends(get_db),
):
"""Recupera un singolo allarme per ID"""
allarme = (
db.query(Allarme)
.join(Sito)
.filter(
Allarme.id == allarme_id,
Sito.cliente_id == current_user.cliente_id
)
.first()
)
if not allarme:
raise HTTPException(status_code=404, detail="Allarme non trovato")
return allarme
@router.patch("/{allarme_id}", response_model=AllarmeResponse)
async def update_allarme(
allarme_id: int,
update_data: AllarmeUpdate,
current_user: Annotated[Utente, Depends(get_current_user)],
db: Session = Depends(get_db),
):
"""Aggiorna lo stato o le note di un allarme"""
allarme = (
db.query(Allarme)
.join(Sito)
.filter(
Allarme.id == allarme_id,
Sito.cliente_id == current_user.cliente_id
)
.first()
)
if not allarme:
raise HTTPException(status_code=404, detail="Allarme non trovato")
# Aggiorna campi se forniti
if update_data.stato is not None:
allarme.stato = update_data.stato
# Se viene risolto, registra chi e quando
if update_data.stato == "risolto":
from datetime import datetime, timezone
allarme.risolto_da = f"{current_user.nome} {current_user.cognome}"
allarme.timestamp_risoluzione = datetime.now(timezone.utc)
if update_data.note is not None:
allarme.note = update_data.note
if update_data.risolto_da is not None:
allarme.risolto_da = update_data.risolto_da
db.commit()
db.refresh(allarme)
return allarme
@router.get("/sito/{sito_id}", response_model=AllarmeList)
async def get_allarmi_by_sito(
sito_id: int,
current_user: Annotated[Utente, Depends(get_current_user)],
db: Session = Depends(get_db),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=100),
):
"""Recupera tutti gli allarmi per un sito specifico"""
# Verifica che il sito appartenga al cliente dell'utente
sito = (
db.query(Sito)
.filter(
Sito.id == sito_id,
Sito.cliente_id == current_user.cliente_id
)
.first()
)
if not sito:
raise HTTPException(status_code=404, detail="Sito non trovato")
query = db.query(Allarme).filter(Allarme.sito_id == sito_id)
total = query.count()
allarmi = (
query.order_by(desc(Allarme.timestamp_rilevamento))
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return AllarmeList(
total=total,
items=allarmi,
page=page,
page_size=page_size,
)

130
app/api/auth.py Normal file
View File

@@ -0,0 +1,130 @@
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.database import get_db
from app.core.security import verify_password, create_access_token, decode_access_token
from app.models import Utente
from app.schemas.auth import Token, LoginRequest, RegisterFCMToken
router = APIRouter(prefix="/auth", tags=["Authentication"])
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Session = Depends(get_db)
) -> Utente:
"""Dependency per ottenere l'utente corrente dal token JWT"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenziali non valide",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_access_token(token)
if payload is None:
raise credentials_exception
email: str = payload.get("sub")
if email is None:
raise credentials_exception
user = db.query(Utente).filter(Utente.email == email).first()
if user is None:
raise credentials_exception
if not user.attivo:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Utente non attivo"
)
return user
@router.post("/token", response_model=Token)
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
db: Session = Depends(get_db)
):
"""Endpoint per login con OAuth2 password flow"""
user = db.query(Utente).filter(Utente.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email o password non corretti",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.attivo:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Utente non attivo"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email, "cliente_id": user.cliente_id},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/login", response_model=Token)
async def login_json(login_data: LoginRequest, db: Session = Depends(get_db)):
"""Endpoint per login con JSON"""
user = db.query(Utente).filter(Utente.email == login_data.email).first()
if not user or not verify_password(login_data.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email o password non corretti"
)
if not user.attivo:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Utente non attivo"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.email, "cliente_id": user.cliente_id},
expires_delta=access_token_expires
)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/register-fcm-token")
async def register_fcm_token(
token_data: RegisterFCMToken,
current_user: Annotated[Utente, Depends(get_current_user)],
db: Session = Depends(get_db)
):
"""Registra o aggiorna il FCM token per l'utente corrente"""
current_user.fcm_token = token_data.fcm_token
db.commit()
return {"message": "FCM token registrato con successo"}
@router.get("/me")
async def get_me(current_user: Annotated[Utente, Depends(get_current_user)]):
"""Restituisce le informazioni dell'utente corrente"""
return {
"id": current_user.id,
"email": current_user.email,
"nome": current_user.nome,
"cognome": current_user.cognome,
"ruolo": current_user.ruolo,
"cliente_id": current_user.cliente_id,
}

50
app/api/siti.py Normal file
View File

@@ -0,0 +1,50 @@
"""
Endpoint API per la gestione dei siti monitorati
"""
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models import Sito, Utente
from app.api.auth import get_current_user
from app.schemas.sito import SitoResponse, SitoListResponse
router = APIRouter(prefix="/siti", tags=["siti"])
@router.get("/{sito_id}", response_model=SitoResponse)
async def get_sito(
sito_id: int,
current_user: Utente = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Recupera dettagli di un sito specifico"""
sito = db.query(Sito).filter(
Sito.id == sito_id,
Sito.cliente_id == current_user.cliente_id,
).first()
if not sito:
raise HTTPException(status_code=404, detail="Sito non trovato")
return sito
@router.get("", response_model=SitoListResponse)
async def get_siti(
skip: int = 0,
limit: int = 100,
tipo: str | None = None,
current_user: Utente = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Recupera lista siti del cliente"""
query = db.query(Sito).filter(Sito.cliente_id == current_user.cliente_id)
if tipo:
query = query.filter(Sito.tipo == tipo)
total = query.count()
siti = query.offset(skip).limit(limit).all()
return {"total": total, "items": siti}

127
app/api/statistiche.py Normal file
View File

@@ -0,0 +1,127 @@
"""
Endpoint API per statistiche e dashboard
"""
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models import Allarme, Sito, Utente
from app.api.auth import get_current_user
from app.schemas.statistiche import StatisticheResponse, AllarmiPerGiornoResponse
router = APIRouter(prefix="/statistiche", tags=["statistiche"])
@router.get("", response_model=StatisticheResponse)
async def get_statistiche(
current_user: Utente = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Recupera statistiche generali per il dashboard"""
# Totali
totale_allarmi = db.query(Allarme).join(Sito).filter(
Sito.cliente_id == current_user.cliente_id
).count()
totale_siti = db.query(Sito).filter(
Sito.cliente_id == current_user.cliente_id
).count()
# Allarmi per severità
allarmi_per_severita = db.query(
Allarme.severita,
func.count(Allarme.id).label('count')
).join(Sito).filter(
Sito.cliente_id == current_user.cliente_id
).group_by(Allarme.severita).all()
severita_dict = {s: c for s, c in allarmi_per_severita}
# Allarmi per stato
allarmi_per_stato = db.query(
Allarme.stato,
func.count(Allarme.id).label('count')
).join(Sito).filter(
Sito.cliente_id == current_user.cliente_id
).group_by(Allarme.stato).all()
stato_dict = {s: c for s, c in allarmi_per_stato}
# Allarmi aperti (non risolti)
allarmi_aperti = db.query(Allarme).join(Sito).filter(
Sito.cliente_id == current_user.cliente_id,
Allarme.stato != 'risolto'
).count()
# Allarmi ultimi 7 giorni
seven_days_ago = datetime.now() - timedelta(days=7)
allarmi_recenti = db.query(Allarme).join(Sito).filter(
Sito.cliente_id == current_user.cliente_id,
Allarme.created_at >= seven_days_ago
).count()
# Siti per tipo
siti_per_tipo = db.query(
Sito.tipo,
func.count(Sito.id).label('count')
).filter(
Sito.cliente_id == current_user.cliente_id
).group_by(Sito.tipo).all()
tipo_dict = {t: c for t, c in siti_per_tipo}
return {
"totale_allarmi": totale_allarmi,
"totale_siti": totale_siti,
"allarmi_aperti": allarmi_aperti,
"allarmi_recenti_7gg": allarmi_recenti,
"allarmi_critical": severita_dict.get('critical', 0),
"allarmi_warning": severita_dict.get('warning', 0),
"allarmi_info": severita_dict.get('info', 0),
"allarmi_nuovo": stato_dict.get('nuovo', 0),
"allarmi_in_gestione": stato_dict.get('in_gestione', 0),
"allarmi_risolto": stato_dict.get('risolto', 0),
"siti_ponte": tipo_dict.get('ponte', 0),
"siti_galleria": tipo_dict.get('galleria', 0),
"siti_diga": tipo_dict.get('diga', 0),
"siti_frana": tipo_dict.get('frana', 0),
"siti_versante": tipo_dict.get('versante', 0),
"siti_edificio": tipo_dict.get('edificio', 0),
}
@router.get("/allarmi-per-giorno", response_model=AllarmiPerGiornoResponse)
async def get_allarmi_per_giorno(
giorni: int = 30,
current_user: Utente = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Recupera statistiche allarmi per giorno (per grafici temporali)"""
start_date = datetime.now() - timedelta(days=giorni)
# Query allarmi raggruppati per giorno
allarmi_giornalieri = db.query(
func.date(Allarme.created_at).label('data'),
func.count(Allarme.id).label('count')
).join(Sito).filter(
Sito.cliente_id == current_user.cliente_id,
Allarme.created_at >= start_date
).group_by(
func.date(Allarme.created_at)
).order_by(
func.date(Allarme.created_at)
).all()
# Converti in lista di dict
dati = []
for data, count in allarmi_giornalieri:
dati.append({
"data": data.isoformat(),
"count": count
})
return {"dati": dati}