chore: initial project setup with backend, frontend, and infrastructure
Add complete NexaPantry application structure including: - Docker Compose configuration with PostgreSQL, Redis, FastAPI backend, worker, frontend and Caddy - Environment configuration template with database, auth, and service settings - GitHub Actions CI workflow for backend/frontend linting, testing, auditing and Docker builds - AGPL-3.0 license and comprehensive README with setup, development, and security documentation - Backend
This commit is contained in:
143
backend/app/api/routes/admin.py
Normal file
143
backend/app/api/routes/admin.py
Normal file
@@ -0,0 +1,143 @@
|
||||
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 (
|
||||
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:
|
||||
send_invitation(db, user)
|
||||
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)
|
||||
send_mail(db, user.email, "NexaPantry password reset", f"Reset token: {token}")
|
||||
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:
|
||||
send_mail(db, str(payload.to), "NexaPantry test mail", "SMTP is configured correctly.")
|
||||
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"}
|
||||
|
||||
Reference in New Issue
Block a user