from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import func, select from sqlalchemy.orm import Session from app.api.deps import current_admin from app.api.routes.auth import create_reset_token, send_invitation from app.db.session import get_db from app.models.entities import AppSetting, AuditLog, Home, Product, User from app.schemas.common import ( MailSettingsIn, MailSettingsOut, Message, TestMailIn, UserCreate, UserOut, UserUpdate, ) from app.services.audit import audit from app.services.mail import ( MailDeliveryError, get_mail_settings, send_mail, serialize_mail_settings, update_mail_settings, ) router = APIRouter(dependencies=[Depends(current_admin)]) @router.get("/dashboard") def dashboard(db: Session = Depends(get_db)) -> dict: return { "users": db.scalar(select(func.count(User.id))), "homes": db.scalar(select(func.count(Home.id))), "products": db.scalar(select(func.count(Product.id))), "active_users": db.scalar(select(func.count(User.id)).where(User.is_active)), } @router.get("/users", response_model=list[UserOut]) def list_users(db: Session = Depends(get_db)) -> list[User]: return list(db.scalars(select(User).order_by(User.created_at.desc())).all()) @router.post("/users", response_model=UserOut, status_code=201) def create_user(payload: UserCreate, admin: User = Depends(current_admin), db: Session = Depends(get_db)) -> User: if db.scalar(select(User).where(User.email == str(payload.email).lower())): raise HTTPException(status_code=409, detail="E-mail already exists") user = User(email=str(payload.email).lower(), name=payload.name, instance_role=payload.role, is_active=True) db.add(user) db.flush() if payload.send_invite: try: send_invitation(db, user) except MailDeliveryError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc audit(db, admin, "admin.user.create", "user", user.id) db.commit() db.refresh(user) return user @router.patch("/users/{user_id}", response_model=UserOut) def update_user(user_id: str, payload: UserUpdate, admin: User = Depends(current_admin), db: Session = Depends(get_db)) -> User: user = db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(user, key, value) audit(db, admin, "admin.user.update", "user", user.id) db.commit() db.refresh(user) return user @router.delete("/users/{user_id}", response_model=Message) def delete_user(user_id: str, admin: User = Depends(current_admin), db: Session = Depends(get_db)) -> Message: user = db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") db.delete(user) audit(db, admin, "admin.user.delete", "user", user_id) db.commit() return Message(message="User deleted") @router.post("/users/{user_id}/reset-password", response_model=Message) def reset_password(user_id: str, admin: User = Depends(current_admin), db: Session = Depends(get_db)) -> Message: user = db.get(User, user_id) if not user: raise HTTPException(status_code=404, detail="User not found") token = create_reset_token(db, user) try: send_mail(db, user.email, "NexaPantry password reset", f"Reset token: {token}") except MailDeliveryError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc audit(db, admin, "admin.user.reset_password", "user", user.id) db.commit() return Message(message="Password reset mail sent") @router.get("/homes") def homes(db: Session = Depends(get_db)) -> list[dict]: return [{"id": h.id, "name": h.name, "members": len(h.memberships), "products": len(h.products)} for h in db.scalars(select(Home)).all()] @router.get("/mail", response_model=MailSettingsOut) def mail_settings(db: Session = Depends(get_db)) -> MailSettingsOut: return serialize_mail_settings(get_mail_settings(db)) @router.put("/mail", response_model=MailSettingsOut) def save_mail_settings(payload: MailSettingsIn, admin: User = Depends(current_admin), db: Session = Depends(get_db)) -> MailSettingsOut: settings = update_mail_settings(db, payload) audit(db, admin, "admin.mail.update", "mail_settings", "1") db.commit() return serialize_mail_settings(settings) @router.post("/mail/test", response_model=Message) def test_mail(payload: TestMailIn, db: Session = Depends(get_db)) -> Message: try: send_mail(db, str(payload.to), "NexaPantry test mail", "SMTP is configured correctly.") except MailDeliveryError as exc: raise HTTPException(status_code=502, detail=str(exc)) from exc return Message(message="Test mail sent") @router.get("/settings") def get_settings(db: Session = Depends(get_db)) -> dict: return {s.key: s.value for s in db.scalars(select(AppSetting)).all()} @router.put("/settings/{key}") def set_setting(key: str, value: dict, admin: User = Depends(current_admin), db: Session = Depends(get_db)) -> dict: setting = db.get(AppSetting, key) or AppSetting(key=key) setting.value = value db.merge(setting) audit(db, admin, "admin.setting.update", "setting", key) db.commit() return setting.value @router.get("/logs") def logs(db: Session = Depends(get_db)) -> list[dict]: rows = db.scalars(select(AuditLog).order_by(AuditLog.created_at.desc()).limit(200)).all() return [{"created_at": r.created_at, "action": r.action, "target_type": r.target_type, "target_id": r.target_id, "metadata": r.metadata_json} for r in rows] @router.get("/system") def system_info() -> dict: return {"app": "NexaPantry", "version": "0.1.0", "runtime": "FastAPI", "database": "PostgreSQL"}