Files
sproutly/backend/routers/admin.py
derekc 84e7b13575 Add last login tracking, batch date auto-fill, and bug fixes
- Track last_login_at on User model, updated on every successful login
- Show last login date in admin panel user table
- Fix admin/garden date display (datetime strings already contain T separator)
- Fix My Garden Internal Server Error (MySQL does not support NULLS LAST syntax)
- Fix Log Batch infinite loop when user has zero varieties
- Auto-fill batch dates from today when creating a new batch, calculated
  from selected variety's week offsets (germination, greenhouse, garden)
- Update README with new features and batch date auto-fill formula table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 00:48:04 -07:00

89 lines
3.4 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from auth import get_admin_user, hash_password
from database import get_db
from models import Batch, User, Variety
from schemas import AdminResetPassword, AdminUserOut, BatchOut, VarietyOut
router = APIRouter(prefix="/admin", tags=["admin"])
def _user_stats(db: Session, user_id: int) -> dict:
return {
"variety_count": db.query(Variety).filter(Variety.user_id == user_id).count(),
"batch_count": db.query(Batch).filter(Batch.user_id == user_id).count(),
}
@router.get("/users", response_model=List[AdminUserOut])
def list_users(db: Session = Depends(get_db), _: User = Depends(get_admin_user)):
users = db.query(User).order_by(User.created_at).all()
return [
AdminUserOut(
id=u.id, email=u.email, is_admin=u.is_admin, is_disabled=u.is_disabled,
created_at=u.created_at, last_login_at=u.last_login_at, **_user_stats(db, u.id)
)
for u in users
]
@router.get("/users/{user_id}/varieties", response_model=List[VarietyOut])
def user_varieties(user_id: int, db: Session = Depends(get_db), _: User = Depends(get_admin_user)):
_require_user(db, user_id)
return db.query(Variety).filter(Variety.user_id == user_id).order_by(Variety.category, Variety.name).all()
@router.get("/users/{user_id}/batches", response_model=List[BatchOut])
def user_batches(user_id: int, db: Session = Depends(get_db), _: User = Depends(get_admin_user)):
from sqlalchemy.orm import joinedload
_require_user(db, user_id)
return (
db.query(Batch)
.options(joinedload(Batch.variety))
.filter(Batch.user_id == user_id)
.order_by(Batch.created_at.desc())
.all()
)
@router.post("/users/{user_id}/reset-password")
def reset_password(user_id: int, data: AdminResetPassword, db: Session = Depends(get_db), admin: User = Depends(get_admin_user)):
user = _require_user(db, user_id)
if not data.new_password or len(data.new_password) < 8:
raise HTTPException(status_code=400, detail="Password must be at least 8 characters")
user.hashed_password = hash_password(data.new_password)
db.commit()
return {"status": "ok"}
@router.post("/users/{user_id}/disable")
def toggle_disable(user_id: int, db: Session = Depends(get_db), admin: User = Depends(get_admin_user)):
user = _require_user(db, user_id)
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot disable your own account")
if user.is_admin:
raise HTTPException(status_code=400, detail="Cannot disable another admin account")
user.is_disabled = not user.is_disabled
db.commit()
return {"is_disabled": user.is_disabled}
@router.delete("/users/{user_id}", status_code=204)
def delete_user(user_id: int, db: Session = Depends(get_db), admin: User = Depends(get_admin_user)):
user = _require_user(db, user_id)
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot delete your own account")
if user.is_admin:
raise HTTPException(status_code=400, detail="Cannot delete another admin account")
db.delete(user)
db.commit()
def _require_user(db: Session, user_id: int) -> User:
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user