feat: create user api

This commit is contained in:
tao.chen
2026-08-06 10:42:17 +08:00
parent bf2cd9bd69
commit d813508461
2 changed files with 74 additions and 3 deletions
+23 -1
View File
@@ -500,6 +500,7 @@ Base 前缀 `/api/v1/admin`。
| 方法 | 路径 | 说明 | | 方法 | 路径 | 说明 |
|---|---|---| |---|---|---|
| `GET` | `/api/v1/platform/employees` | 列全平台未软删员工;包含停用、锁定及无平台角色用户 | | `GET` | `/api/v1/platform/employees` | 列全平台未软删员工;包含停用、锁定及无平台角色用户 |
| `POST` | `/api/v1/platform/employees` | 创建平台员工账号(返回 201);不自动加入任何 workspace |
| `GET` | `/api/v1/platform/workspaces` | 列 workspace(`active`/`archived`);已软删的过滤掉 | | `GET` | `/api/v1/platform/workspaces` | 列 workspace(`active`/`archived`);已软删的过滤掉 |
| `POST` | `/api/v1/platform/workspaces` | 创建 workspace(返回 201);创建者自动成为 admin 成员 | | `POST` | `/api/v1/platform/workspaces` | 创建 workspace(返回 201);创建者自动成为 admin 成员 |
| `GET` | `/api/v1/platform/workspaces/{workspace_id}` | 单个 workspace(含已 disabled 的,用于恢复) | | `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 角色) | | `role_code` | string | 是 | `admin` \| `developer` | **不可填 `system_admin`**(那是用户级身份,不是 workspace 角色) |
- 服务端默认 `member_status='active'`。 - 服务端默认 `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}` ### 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 路由 ## 八、Jupyter 路由
+51 -2
View File
@@ -28,6 +28,7 @@ Workspace membership CRUD::
Platform employee roster:: Platform employee roster::
GET /employees — list all non-deleted users GET /employees — list all non-deleted users
POST /employees — create a user without workspace membership
Invariants Invariants
---------- ----------
@@ -50,10 +51,11 @@ from typing import Any, Literal
from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, ConfigDict, Field 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 sqlalchemy.ext.asyncio import AsyncSession
from backend.dependencies import current_user, database_session 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.db.models import Roles, Users, WorkspaceMembers, Workspaces
from common.ids import new_ulid from common.ids import new_ulid
@@ -111,6 +113,15 @@ class MemberUpdate(BaseModel):
member_status: Literal["active", "disabled", "locked"] | None = None 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 # 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 # Workspace CRUD
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -502,8 +546,13 @@ async def add_member(
"""Add a user to a workspace. The new row starts with member_status='active'.""" """Add a user to a workspace. The new row starts with member_status='active'."""
await _load_workspace(session, workspace_id) await _load_workspace(session, workspace_id)
user = await session.get(Users, payload.user_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, "用户不存在") 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) role = await _load_role_by_code(session, payload.role_code)
duplicate = await session.scalar( duplicate = await session.scalar(
select(WorkspaceMembers.user_id).where( select(WorkspaceMembers.user_id).where(