feat: add user api
This commit is contained in:
@@ -29,6 +29,8 @@ Platform employee roster::
|
||||
|
||||
GET /employees — list all non-deleted users
|
||||
POST /employees — create a user without workspace membership
|
||||
PATCH /employees/{user_id} — update a user's profile, status or platform role
|
||||
DELETE /employees/{user_id} — soft delete a user (cascades to workspace memberships)
|
||||
|
||||
Invariants
|
||||
----------
|
||||
@@ -122,6 +124,15 @@ class PlatformEmployeeCreate(BaseModel):
|
||||
password: str = Field(min_length=8, max_length=72)
|
||||
|
||||
|
||||
class PlatformEmployeeUpdate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
display_name: str | None = Field(default=None, min_length=1, max_length=100)
|
||||
email: str | None = Field(default=None, max_length=255)
|
||||
status: Literal["active", "disabled", "locked"] | None = None
|
||||
role_code: Literal["admin", "developer"] | None = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# System-admin context dependency
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -279,6 +290,30 @@ async def _count_active_admins(
|
||||
return int(await session.scalar(stmt) or 0)
|
||||
|
||||
|
||||
async def _count_active_system_admins(
|
||||
session: AsyncSession,
|
||||
exclude_user_id: str | None = None,
|
||||
) -> int:
|
||||
"""Count active system admins across the platform.
|
||||
|
||||
Pass ``exclude_user_id`` when checking "would X be the last admin?"
|
||||
before mutating X.
|
||||
"""
|
||||
admin_role = await _load_role_by_code(session, "admin")
|
||||
stmt = (
|
||||
select(func.count())
|
||||
.select_from(Users)
|
||||
.where(
|
||||
Users.status == "active",
|
||||
Users.is_deleted == 0,
|
||||
Users.platform_role_id == admin_role.role_id,
|
||||
)
|
||||
)
|
||||
if exclude_user_id is not None:
|
||||
stmt = stmt.where(Users.user_id != exclude_user_id)
|
||||
return int(await session.scalar(stmt) or 0)
|
||||
|
||||
|
||||
def _envelope(request_id: str, data: Any, meta: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
return {
|
||||
"request_id": request_id,
|
||||
@@ -346,6 +381,131 @@ async def create_platform_employee(
|
||||
return _envelope(context.request_id, platform_employee_payload(user, None))
|
||||
|
||||
|
||||
@router.patch("/employees/{user_id}")
|
||||
async def update_platform_employee(
|
||||
user_id: str,
|
||||
payload: PlatformEmployeeUpdate,
|
||||
context: SystemAdminContext = Depends(system_admin_context),
|
||||
session: AsyncSession = Depends(database_session),
|
||||
) -> dict[str, Any]:
|
||||
"""Update a platform user's profile, status, or platform role."""
|
||||
user = await session.get(Users, user_id)
|
||||
if user is None or user.is_deleted != 0:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "用户不存在")
|
||||
|
||||
is_self = user_id == context.user.user_id
|
||||
if (
|
||||
is_self
|
||||
and payload.status is not None
|
||||
and payload.status != "active"
|
||||
):
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "不能停用当前登录账号")
|
||||
|
||||
current_role: Roles | None = None
|
||||
if user.platform_role_id is not None:
|
||||
current_role = await session.scalar(
|
||||
select(Roles).where(Roles.role_id == user.platform_role_id)
|
||||
)
|
||||
is_current_system_admin = (
|
||||
user.status == "active"
|
||||
and current_role is not None
|
||||
and current_role.role_code == "admin"
|
||||
)
|
||||
|
||||
new_role: Roles | None = None
|
||||
if payload.role_code is not None:
|
||||
new_role = await _load_role_by_code(session, payload.role_code)
|
||||
next_status = payload.status if payload.status is not None else user.status
|
||||
|
||||
leaves_admin_pool = (
|
||||
is_current_system_admin
|
||||
and (
|
||||
next_status != "active"
|
||||
or (new_role is not None and new_role.role_code != "admin")
|
||||
)
|
||||
)
|
||||
if leaves_admin_pool:
|
||||
remaining = await _count_active_system_admins(
|
||||
session, exclude_user_id=user_id,
|
||||
)
|
||||
if remaining == 0:
|
||||
raise HTTPException(
|
||||
status.HTTP_409_CONFLICT,
|
||||
"platform 必须保留至少一个 active 系统管理员",
|
||||
)
|
||||
|
||||
if is_self and new_role is not None and new_role.role_code != "admin":
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "不能降级自身管理员角色")
|
||||
|
||||
if payload.display_name is not None:
|
||||
user.display_name = payload.display_name.strip()
|
||||
if payload.email is not None:
|
||||
user.email = payload.email.strip() or None
|
||||
if payload.status is not None:
|
||||
user.status = payload.status
|
||||
if new_role is not None:
|
||||
user.platform_role_id = new_role.role_id
|
||||
|
||||
await session.flush()
|
||||
await session.refresh(user)
|
||||
|
||||
response_role: Roles | None = None
|
||||
if user.platform_role_id is not None:
|
||||
response_role = await session.scalar(
|
||||
select(Roles).where(Roles.role_id == user.platform_role_id)
|
||||
)
|
||||
return _envelope(
|
||||
context.request_id, platform_employee_payload(user, response_role),
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/employees/{user_id}")
|
||||
async def delete_platform_employee(
|
||||
user_id: str,
|
||||
context: SystemAdminContext = Depends(system_admin_context),
|
||||
session: AsyncSession = Depends(database_session),
|
||||
) -> dict[str, Any]:
|
||||
"""Soft delete a platform user and cascade-soft-delete workspace memberships."""
|
||||
user = await session.get(Users, user_id)
|
||||
if user is None or user.is_deleted != 0:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "用户不存在")
|
||||
|
||||
if user_id == context.user.user_id:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "不能删除当前登录账号")
|
||||
|
||||
if user.status == "active" and user.platform_role_id is not None:
|
||||
current_admin_role = await session.scalar(
|
||||
select(Roles).where(Roles.role_id == user.platform_role_id)
|
||||
)
|
||||
if current_admin_role is not None and current_admin_role.role_code == "admin":
|
||||
remaining = await _count_active_system_admins(
|
||||
session, exclude_user_id=user_id,
|
||||
)
|
||||
if remaining == 0:
|
||||
raise HTTPException(
|
||||
status.HTTP_409_CONFLICT,
|
||||
"platform 必须保留至少一个 active 系统管理员",
|
||||
)
|
||||
|
||||
now = datetime.datetime.utcnow()
|
||||
user.status = "disabled"
|
||||
user.is_deleted = 1
|
||||
user.deleted_at = now
|
||||
await session.execute(
|
||||
update(WorkspaceMembers)
|
||||
.where(
|
||||
WorkspaceMembers.user_id == user_id,
|
||||
WorkspaceMembers.is_deleted == 0,
|
||||
)
|
||||
.values(is_deleted=1, deleted_at=now)
|
||||
)
|
||||
await session.flush()
|
||||
return _envelope(
|
||||
context.request_id,
|
||||
{"user_id": user_id, "deleted": True},
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Workspace CRUD
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user