- 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
154 lines
5.8 KiB
Python
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"}
|
|
|