Files
planet/backend/app/api/v1/users.py
linkong 8c204717cd
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.70.0
2026-06-04 17:16:23 +08:00

295 lines
9.4 KiB
Python

import json
from typing import List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text
from app.core.enums import UserRole
from app.core.security import get_current_user, get_password_hash
from app.db.session import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
router = APIRouter()
VALID_GATEKEEPER_GROUPS = {"docs_user", "docs_developer", "docs_admin"}
ADMIN_ROLES = [UserRole.SUPER_ADMIN.value, UserRole.ADMIN.value]
SUPER_ADMIN_ROLES = [UserRole.SUPER_ADMIN.value]
def check_permission(current_user: User, required_roles: List[str]) -> bool:
user_role_value = (
current_user.role.value if hasattr(current_user.role, "value") else current_user.role
)
return user_role_value in required_roles
@router.get("", response_model=dict)
async def list_users(
page: int = 1,
page_size: int = 20,
role: str = None,
is_active: bool = None,
search: str = None,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
if not check_permission(current_user, ADMIN_ROLES):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
# Build WHERE clause
where_clauses = []
params = {}
if role:
where_clauses.append("role = :role")
params["role"] = role
if is_active is not None:
where_clauses.append("is_active = :is_active")
params["is_active"] = is_active
if search:
where_clauses.append("(username ILIKE :search OR email ILIKE :search)")
params["search"] = f"%{search}%"
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
offset = (page - 1) * page_size
query = text(
f"SELECT id, username, email, role, is_active, last_login_at, created_at, gatekeeper_groups FROM users WHERE {where_sql} ORDER BY created_at DESC LIMIT {page_size} OFFSET {offset}"
)
count_query = text(f"SELECT COUNT(*) FROM users WHERE {where_sql}")
result = await db.execute(query, params)
users = result.fetchall()
count_result = await db.execute(count_query, params)
total = count_result.scalar()
return {
"total": total,
"page": page,
"page_size": page_size,
"data": [
{
"id": u[0],
"username": u[1],
"email": u[2],
"role": u[3],
"is_active": u[4],
"last_login_at": u[5],
"created_at": u[6],
"gatekeeper_groups": u[7] or [],
}
for u in users
],
}
@router.get("/{user_id}", response_model=dict)
async def get_user(
user_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
if not check_permission(current_user, ADMIN_ROLES) and current_user.id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
result = await db.execute(
text(
"SELECT id, username, email, role, is_active, last_login_at, created_at, gatekeeper_groups FROM users WHERE id = :id"
),
{"id": user_id},
)
user = result.fetchone()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return {
"id": user[0],
"username": user[1],
"email": user[2],
"role": user[3],
"is_active": user[4],
"last_login_at": user[5],
"created_at": user[6],
"gatekeeper_groups": user[7] or [],
}
@router.post("", response_model=dict, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
if not check_permission(current_user, SUPER_ADMIN_ROLES):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only super_admin can create users",
)
invalid_groups = sorted(set(user_data.gatekeeper_groups) - VALID_GATEKEEPER_GROUPS)
if invalid_groups:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported Gatekeeper groups: {', '.join(invalid_groups)}",
)
result = await db.execute(
text("SELECT id FROM users WHERE username = :username OR email = :email"),
{"username": user_data.username, "email": user_data.email},
)
if result.fetchone():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username or email already exists",
)
hashed_password = get_password_hash(user_data.password)
await db.execute(
text("""INSERT INTO users (username, email, password_hash, role, gatekeeper_groups, is_active, created_at, updated_at)
VALUES (:username, :email, :password_hash, :role, CAST(:gatekeeper_groups AS jsonb), :is_active, NOW(), NOW())"""),
{
"username": user_data.username,
"email": user_data.email,
"password_hash": hashed_password,
"role": user_data.role,
"gatekeeper_groups": json.dumps(user_data.gatekeeper_groups),
"is_active": True,
},
)
await db.commit()
# Get the inserted user ID
result = await db.execute(
text("SELECT id FROM users WHERE username = :username"),
{"username": user_data.username},
)
new_user = result.fetchone()
if new_user is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to create user",
)
return {
"id": new_user[0],
"username": user_data.username,
"email": user_data.email,
"role": user_data.role,
"gatekeeper_groups": user_data.gatekeeper_groups,
"is_active": True,
}
@router.put("/{user_id}")
async def update_user(
user_id: int,
user_data: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
if not check_permission(current_user, ADMIN_ROLES) and current_user.id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
if not check_permission(current_user, SUPER_ADMIN_ROLES) and user_data.role is not None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only super_admin can change user role",
)
if not check_permission(current_user, SUPER_ADMIN_ROLES) and user_data.gatekeeper_groups is not None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only super_admin can change Gatekeeper groups",
)
if user_data.gatekeeper_groups is not None:
invalid_groups = sorted(set(user_data.gatekeeper_groups) - VALID_GATEKEEPER_GROUPS)
if invalid_groups:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported Gatekeeper groups: {', '.join(invalid_groups)}",
)
result = await db.execute(
text("SELECT id FROM users WHERE id = :id"),
{"id": user_id},
)
if not result.fetchone():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
update_fields = []
params = {"id": user_id}
if user_data.email is not None:
update_fields.append("email = :email")
params["email"] = user_data.email
if user_data.role is not None:
update_fields.append("role = :role")
params["role"] = user_data.role
if user_data.gatekeeper_groups is not None:
update_fields.append("gatekeeper_groups = CAST(:gatekeeper_groups AS jsonb)")
params["gatekeeper_groups"] = json.dumps(user_data.gatekeeper_groups)
if user_data.is_active is not None:
update_fields.append("is_active = :is_active")
params["is_active"] = user_data.is_active
if update_fields:
update_fields.append("updated_at = NOW()")
query = text(f"UPDATE users SET {', '.join(update_fields)} WHERE id = :id")
await db.execute(query, params)
await db.commit()
return {"message": "User updated successfully"}
@router.delete("/{user_id}")
async def delete_user(
user_id: int,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
if not check_permission(current_user, SUPER_ADMIN_ROLES):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only super_admin can delete users",
)
if current_user.id == user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete yourself",
)
result = await db.execute(
text("SELECT id FROM users WHERE id = :id"),
{"id": user_id},
)
if not result.fetchone():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
await db.execute(
text("DELETE FROM users WHERE id = :id"),
{"id": user_id},
)
await db.commit()
return {"message": "User deleted successfully"}