Files
NexaPantry/backend/app/api/routes/admin.py
nessi 5ed613d441
Some checks failed
CI / backend (push) Failing after 17s
CI / frontend (push) Successful in 31s
CI / docker (push) Has been skipped
fix: improve SMTP configuration and error handling
- Change default use_tls from True to False to match typical STARTTLS setup
- Add MailDeliveryError exception for mail delivery failures
- Wrap send_mail calls in try-catch blocks to handle errors gracefully
- Return 502 status code with error details when mail delivery fails
- Add SMTP security mode selector in frontend (STARTTLS/TLS/None)
- Add test mail form to admin panel
- Handle empty SMTP credentials properly in update_mail_settings
- Catch
2026-06-04 11:00:11 +02:00

154 lines
5.8 KiB
Python

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"}