Spaces:
Running
Running
| """ | |
| Admin Router | |
| God-mode onboarding and identity management for leader/authority accounts. | |
| """ | |
| from datetime import datetime, timezone | |
| from typing import List, Optional | |
| import re | |
| from fastapi import APIRouter, Depends, HTTPException, Query | |
| from pydantic import BaseModel, Field | |
| from sqlalchemy import func, or_ | |
| from sqlalchemy.orm import Session | |
| from database import get_db | |
| from models.user import User | |
| from routers.auth import get_current_admin, hash_password | |
| router = APIRouter(prefix="/admin", tags=["Admin"]) | |
| ALLOWED_ROLES = {"authority", "leader", "admin", "citizen"} | |
| ONBOARD_ROLES = {"authority", "leader", "admin"} | |
| class UserCreateRequest(BaseModel): | |
| name: str = Field(..., min_length=2, max_length=120) | |
| email: str = Field(..., min_length=5, max_length=200) | |
| phone: Optional[str] = Field(default=None, max_length=32) | |
| state: str = Field(..., min_length=2, max_length=120) | |
| district: str = Field(..., min_length=2, max_length=120) | |
| password: str = Field(..., min_length=8, max_length=128) | |
| role: str = Field(..., description="authority|leader|admin") | |
| class BulkUserCreateRequest(BaseModel): | |
| users: List[UserCreateRequest] = Field(default_factory=list, max_length=250) | |
| class UpdateRoleRequest(BaseModel): | |
| role: str = Field(..., description="authority|leader|admin|citizen") | |
| class ResetPasswordRequest(BaseModel): | |
| new_password: str = Field(..., min_length=8, max_length=128) | |
| class ActivationRequest(BaseModel): | |
| is_active: bool | |
| def _normalized_email(raw: str) -> str: | |
| email = (raw or "").strip().lower() | |
| if not re.match(r"^[^@\s]+@[^@\s]+\.[^@\s]+$", email): | |
| raise HTTPException(status_code=400, detail="Invalid email format") | |
| return email | |
| def _serialize_user(row: User) -> dict: | |
| return { | |
| "id": row.id, | |
| "name": row.name, | |
| "email": row.email, | |
| "phone": row.phone, | |
| "state": row.state, | |
| "district": row.district, | |
| "role": row.role or "citizen", | |
| "is_active": bool(row.is_active if row.is_active is not None else True), | |
| "created_at": row.created_at.isoformat() if row.created_at else None, | |
| "last_login_at": row.last_login_at.isoformat() if row.last_login_at else None, | |
| } | |
| def onboarding_stats( | |
| db: Session = Depends(get_db), | |
| _: User = Depends(get_current_admin), | |
| ): | |
| total_users = db.query(func.count(User.id)).scalar() or 0 | |
| active_users = db.query(func.count(User.id)).filter(User.is_active.isnot(False)).scalar() or 0 | |
| role_rows = ( | |
| db.query(User.role, func.count(User.id)) | |
| .group_by(User.role) | |
| .all() | |
| ) | |
| role_counts = {"citizen": 0, "authority": 0, "leader": 0, "admin": 0} | |
| for role, count in role_rows: | |
| normalized = (role or "citizen").strip().lower() | |
| role_counts[normalized] = int(count) | |
| recent = ( | |
| db.query(User) | |
| .order_by(User.created_at.desc()) | |
| .limit(8) | |
| .all() | |
| ) | |
| return { | |
| "total_users": int(total_users), | |
| "active_users": int(active_users), | |
| "inactive_users": int(total_users) - int(active_users), | |
| "role_counts": role_counts, | |
| "recent_onboarded": [_serialize_user(u) for u in recent], | |
| } | |
| def list_users( | |
| role: Optional[str] = Query(default=None), | |
| search: Optional[str] = Query(default=None), | |
| limit: int = Query(default=120, ge=1, le=500), | |
| db: Session = Depends(get_db), | |
| _: User = Depends(get_current_admin), | |
| ): | |
| query = db.query(User) | |
| if role: | |
| normalized_role = role.strip().lower() | |
| if normalized_role not in ALLOWED_ROLES: | |
| raise HTTPException(status_code=400, detail=f"Invalid role: {role}") | |
| query = query.filter(func.lower(User.role) == normalized_role) | |
| if search: | |
| pattern = f"%{search.strip()}%" | |
| query = query.filter( | |
| or_( | |
| User.name.ilike(pattern), | |
| User.email.ilike(pattern), | |
| User.phone.ilike(pattern), | |
| User.role.ilike(pattern), | |
| ) | |
| ) | |
| rows = query.order_by(User.created_at.desc()).limit(limit).all() | |
| return { | |
| "count": len(rows), | |
| "users": [_serialize_user(row) for row in rows], | |
| } | |
| def create_user( | |
| req: UserCreateRequest, | |
| db: Session = Depends(get_db), | |
| admin: User = Depends(get_current_admin), | |
| ): | |
| requested_role = req.role.strip().lower() | |
| if requested_role not in ONBOARD_ROLES: | |
| raise HTTPException(status_code=400, detail="Role must be authority, leader, or admin") | |
| email = _normalized_email(req.email) | |
| existing = db.query(User).filter(func.lower(User.email) == email).first() | |
| if existing: | |
| raise HTTPException(status_code=400, detail="Email already registered") | |
| user = User( | |
| name=req.name.strip(), | |
| email=email, | |
| phone=(req.phone or "").strip() or None, | |
| state=req.state.strip(), | |
| district=req.district.strip(), | |
| role=requested_role, | |
| hashed_password=hash_password(req.password), | |
| is_active=True, | |
| last_login_at=None, | |
| ) | |
| db.add(user) | |
| db.commit() | |
| db.refresh(user) | |
| return { | |
| "message": f"{requested_role.title()} onboarded successfully by {admin.name}.", | |
| "user": _serialize_user(user), | |
| } | |
| def bulk_create_users( | |
| req: BulkUserCreateRequest, | |
| db: Session = Depends(get_db), | |
| admin: User = Depends(get_current_admin), | |
| ): | |
| if not req.users: | |
| raise HTTPException(status_code=400, detail="No users provided") | |
| created = [] | |
| skipped = [] | |
| for item in req.users: | |
| role = item.role.strip().lower() | |
| if role not in ONBOARD_ROLES: | |
| skipped.append({"email": item.email, "reason": f"invalid_role:{role}"}) | |
| continue | |
| try: | |
| email = _normalized_email(item.email) | |
| except HTTPException as exc: | |
| skipped.append({"email": item.email, "reason": f"invalid_email:{exc.detail}"}) | |
| continue | |
| exists = db.query(User).filter(func.lower(User.email) == email).first() | |
| if exists: | |
| skipped.append({"email": item.email, "reason": "email_exists"}) | |
| continue | |
| user = User( | |
| name=item.name.strip(), | |
| email=email, | |
| phone=(item.phone or "").strip() or None, | |
| state=item.state.strip(), | |
| district=item.district.strip(), | |
| role=role, | |
| hashed_password=hash_password(item.password), | |
| is_active=True, | |
| ) | |
| db.add(user) | |
| created.append(user) | |
| db.commit() | |
| for user in created: | |
| db.refresh(user) | |
| return { | |
| "message": f"Bulk onboarding complete by {admin.name}.", | |
| "created_count": len(created), | |
| "skipped_count": len(skipped), | |
| "created": [_serialize_user(user) for user in created], | |
| "skipped": skipped, | |
| } | |
| def update_user_role( | |
| user_id: int, | |
| req: UpdateRoleRequest, | |
| db: Session = Depends(get_db), | |
| admin: User = Depends(get_current_admin), | |
| ): | |
| new_role = req.role.strip().lower() | |
| if new_role not in ALLOWED_ROLES: | |
| raise HTTPException(status_code=400, detail="Invalid role") | |
| user = db.query(User).filter(User.id == user_id).first() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| if user.id == admin.id and new_role != "admin": | |
| raise HTTPException(status_code=400, detail="Admin cannot demote own account") | |
| user.role = new_role | |
| db.add(user) | |
| db.commit() | |
| db.refresh(user) | |
| return { | |
| "message": f"Role updated to {new_role}", | |
| "user": _serialize_user(user), | |
| } | |
| def reset_user_password( | |
| user_id: int, | |
| req: ResetPasswordRequest, | |
| db: Session = Depends(get_db), | |
| _: User = Depends(get_current_admin), | |
| ): | |
| user = db.query(User).filter(User.id == user_id).first() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| user.hashed_password = hash_password(req.new_password) | |
| user.last_login_at = None | |
| db.add(user) | |
| db.commit() | |
| return {"message": "Password reset completed"} | |
| def set_user_activation( | |
| user_id: int, | |
| req: ActivationRequest, | |
| db: Session = Depends(get_db), | |
| admin: User = Depends(get_current_admin), | |
| ): | |
| user = db.query(User).filter(User.id == user_id).first() | |
| if not user: | |
| raise HTTPException(status_code=404, detail="User not found") | |
| if user.id == admin.id and not req.is_active: | |
| raise HTTPException(status_code=400, detail="Admin cannot deactivate own account") | |
| user.is_active = bool(req.is_active) | |
| db.add(user) | |
| db.commit() | |
| db.refresh(user) | |
| return { | |
| "message": f"User {'activated' if req.is_active else 'deactivated'}", | |
| "user": _serialize_user(user), | |
| } | |