feat: create user api
This commit is contained in:
@@ -500,6 +500,7 @@ Base 前缀 `/api/v1/admin`。
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/v1/platform/employees` | 列全平台未软删员工;包含停用、锁定及无平台角色用户 |
|
||||
| `POST` | `/api/v1/platform/employees` | 创建平台员工账号(返回 201);不自动加入任何 workspace |
|
||||
| `GET` | `/api/v1/platform/workspaces` | 列 workspace(`active`/`archived`);已软删的过滤掉 |
|
||||
| `POST` | `/api/v1/platform/workspaces` | 创建 workspace(返回 201);创建者自动成为 admin 成员 |
|
||||
| `GET` | `/api/v1/platform/workspaces/{workspace_id}` | 单个 workspace(含已 disabled 的,用于恢复) |
|
||||
@@ -596,7 +597,8 @@ Base 前缀 `/api/v1/admin`。
|
||||
| `role_code` | string | 是 | `admin` \| `developer` | **不可填 `system_admin`**(那是用户级身份,不是 workspace 角色) |
|
||||
|
||||
- 服务端默认 `member_status='active'`。
|
||||
- 用户不存在 → 404;用户已是该 workspace 成员 → 409。
|
||||
- 用户不存在或已软删除 → 404;用户状态不是 `active` → 409;用户已是该 workspace 成员 → 409。
|
||||
- 新员工必须先通过 `POST /api/v1/platform/employees` 或现有 workspace 员工创建接口建立用户账号。
|
||||
|
||||
### 7.6 `PATCH /api/v1/platform/workspaces/{workspace_id}/members/{user_id}`
|
||||
|
||||
@@ -647,6 +649,26 @@ Base 前缀 `/api/v1/admin`。
|
||||
}
|
||||
```
|
||||
|
||||
### 7.9 `POST /api/v1/platform/employees`
|
||||
|
||||
只创建平台员工账号,不创建任何 `workspace_members` 记录。调用者必须是系统管理员;已认证但不是系统管理员时返回 `403`。
|
||||
|
||||
- **请求体字段**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 限制 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `username` | string | 是 | 2~64 字符 | 全平台唯一登录名 |
|
||||
| `display_name` | string | 是 | 1~100 字符 | 显示名称 |
|
||||
| `email` | string | 否 | ≤255 字符 | 邮箱,全平台唯一 |
|
||||
| `password` | string | 是 | 8~72 字符 | 登录密码 |
|
||||
|
||||
- 新用户状态固定为 `active`,且不分配平台角色。
|
||||
- 密码使用 bcrypt 哈希保存;响应不包含 `password` 或 `password_hash`。
|
||||
- 用户名或邮箱重复 → 409。
|
||||
- 创建成功后,可调用 `POST /api/v1/platform/workspaces/{workspace_id}/members` 将用户加入指定 workspace。
|
||||
|
||||
- **响应 201**:字段与 §7.8 的员工元素一致,其中 `role_code`、`role_name` 均为 `null`,`meta` 为空对象。
|
||||
|
||||
---
|
||||
|
||||
## 八、Jupyter 路由
|
||||
|
||||
@@ -28,6 +28,7 @@ Workspace membership CRUD::
|
||||
Platform employee roster::
|
||||
|
||||
GET /employees — list all non-deleted users
|
||||
POST /employees — create a user without workspace membership
|
||||
|
||||
Invariants
|
||||
----------
|
||||
@@ -50,10 +51,11 @@ from typing import Any, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import func, select, update
|
||||
from sqlalchemy import func, or_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.dependencies import current_user, database_session
|
||||
from common.auth.passwords import hash_password
|
||||
from common.db.models import Roles, Users, WorkspaceMembers, Workspaces
|
||||
from common.ids import new_ulid
|
||||
|
||||
@@ -111,6 +113,15 @@ class MemberUpdate(BaseModel):
|
||||
member_status: Literal["active", "disabled", "locked"] | None = None
|
||||
|
||||
|
||||
class PlatformEmployeeCreate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
username: str = Field(min_length=2, max_length=64)
|
||||
display_name: str = Field(min_length=1, max_length=100)
|
||||
email: str | None = Field(default=None, max_length=255)
|
||||
password: str = Field(min_length=8, max_length=72)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# System-admin context dependency
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -302,6 +313,39 @@ async def list_platform_employees(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/employees", status_code=status.HTTP_201_CREATED)
|
||||
async def create_platform_employee(
|
||||
payload: PlatformEmployeeCreate,
|
||||
context: SystemAdminContext = Depends(system_admin_context),
|
||||
session: AsyncSession = Depends(database_session),
|
||||
) -> dict[str, Any]:
|
||||
"""Create a platform user without assigning workspace membership."""
|
||||
username = payload.username.strip()
|
||||
display_name = payload.display_name.strip()
|
||||
duplicate_conditions = [Users.username == username]
|
||||
if payload.email:
|
||||
duplicate_conditions.append(Users.email == payload.email.strip())
|
||||
duplicate = await session.scalar(
|
||||
select(Users.user_id).where(or_(*duplicate_conditions))
|
||||
)
|
||||
if duplicate is not None:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "用户名或邮箱已存在")
|
||||
|
||||
user = Users(
|
||||
user_id=new_ulid(),
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
email=payload.email.strip() if payload.email else None,
|
||||
password_hash=hash_password(payload.password),
|
||||
status="active",
|
||||
platform_role_id=None,
|
||||
)
|
||||
session.add(user)
|
||||
await session.flush()
|
||||
await session.refresh(user)
|
||||
return _envelope(context.request_id, platform_employee_payload(user, None))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Workspace CRUD
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -502,8 +546,13 @@ async def add_member(
|
||||
"""Add a user to a workspace. The new row starts with member_status='active'."""
|
||||
await _load_workspace(session, workspace_id)
|
||||
user = await session.get(Users, payload.user_id)
|
||||
if user is None:
|
||||
if user is None or user.is_deleted != 0:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "用户不存在")
|
||||
if user.status != "active":
|
||||
raise HTTPException(
|
||||
status.HTTP_409_CONFLICT,
|
||||
f"用户状态为 {user.status},无法加入 workspace",
|
||||
)
|
||||
role = await _load_role_by_code(session, payload.role_code)
|
||||
duplicate = await session.scalar(
|
||||
select(WorkspaceMembers.user_id).where(
|
||||
|
||||
Reference in New Issue
Block a user