from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from sqlalchemy import select, func from typing import List, Optional from pydantic import BaseModel, EmailStr from app.database import get_db from app.models.db import User from app.services.auth import hash_password, verify_password from app.services.auth_deps import get_current_user, require_admin from app.services.system_settings_service import system_settings_service router = APIRouter(prefix="/users", tags=["users"]) # Pydantic models class UserListItem(BaseModel): id: int username: str email: str | None full_name: str | None is_active: bool roles: list[str] last_login_at: str | None created_at: str class Config: from_attributes = True class UserStatusUpdate(BaseModel): is_active: bool class ProfileUpdateRequest(BaseModel): full_name: Optional[str] = None email: Optional[EmailStr] = None class PasswordChangeRequest(BaseModel): old_password: str new_password: str @router.get("/list") async def get_user_list( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) # Protect this route ): """Get a list of all users""" # Ensure only admins can see all users if "admin" not in [role.name for role in current_user.roles]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized") result = await db.execute( select(User).options(selectinload(User.roles)).order_by(User.id) ) users = result.scalars().all() users_list = [] for user in users: users_list.append({ "id": user.id, "username": user.username, "email": user.email, "full_name": user.full_name, "is_active": user.is_active, "roles": [role.name for role in user.roles], "last_login_at": user.last_login_at.isoformat() if user.last_login_at else None, "created_at": user.created_at.isoformat() }) return {"users": users_list} @router.put("/{user_id}/status") async def update_user_status( user_id: int, status_update: UserStatusUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update a user's active status""" if "admin" not in [role.name for role in current_user.roles]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized") result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") user.is_active = status_update.is_active await db.commit() return {"message": "User status updated successfully"} @router.post("/{user_id}/reset-password") async def reset_user_password( user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Reset a user's password to the system default""" if "admin" not in [role.name for role in current_user.roles]: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized") result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found") # Get default password from system settings default_password = await system_settings_service.get_setting_value( "default_password", db, default="cosmo" # Fallback if setting doesn't exist ) user.password_hash = hash_password(default_password) await db.commit() return { "message": f"Password for user {user.username} has been reset to system default.", "default_password": default_password } @router.get("/count", response_model=dict) async def get_user_count( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) # All authenticated users can access ): """ Get the total count of registered users. Available to all authenticated users. """ result = await db.execute(select(func.count(User.id))) total_users = result.scalar_one() return {"total_users": total_users} @router.get("/me") async def get_current_user_profile( current_user: User = Depends(get_current_user) ): """ Get current user's profile information """ return { "id": current_user.id, "username": current_user.username, "email": current_user.email, "full_name": current_user.full_name, "is_active": current_user.is_active, "roles": [role.name for role in current_user.roles], "created_at": current_user.created_at.isoformat(), "last_login_at": current_user.last_login_at.isoformat() if current_user.last_login_at else None } @router.put("/me/profile") async def update_current_user_profile( profile_update: ProfileUpdateRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Update current user's profile information (nickname/full_name and email) """ # Check if email is being changed and if it's already taken if profile_update.email and profile_update.email != current_user.email: # Check if email is already in use by another user result = await db.execute( select(User).where(User.email == profile_update.email, User.id != current_user.id) ) existing_user = result.scalar_one_or_none() if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already in use by another user" ) current_user.email = profile_update.email # Update full_name (nickname) if profile_update.full_name is not None: current_user.full_name = profile_update.full_name await db.commit() await db.refresh(current_user) return { "message": "Profile updated successfully", "user": { "id": current_user.id, "username": current_user.username, "email": current_user.email, "full_name": current_user.full_name } } @router.put("/me/password") async def change_current_user_password( password_change: PasswordChangeRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """ Change current user's password """ # Verify old password if not verify_password(password_change.old_password, current_user.password_hash): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Current password is incorrect" ) # Validate new password if len(password_change.new_password) < 6: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="New password must be at least 6 characters long" ) if password_change.old_password == password_change.new_password: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="New password must be different from the old password" ) # Update password current_user.password_hash = hash_password(password_change.new_password) await db.commit() return {"message": "Password changed successfully"}