234 lines
7.4 KiB
Python
234 lines
7.4 KiB
Python
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"}
|
|
|