update: auth
update auth api
This commit is contained in:
+16
-16
@@ -163,17 +163,17 @@ async def login(
|
||||
if default_workspace_id is None:
|
||||
default_workspace_id = workspace.workspace_id
|
||||
|
||||
# Pick a default role_code for the user payload: prefer admin if
|
||||
# the user has it in any workspace, otherwise use the first one
|
||||
# returned. This is only for UI greeting; access control checks
|
||||
# run on a per-request basis via the chosen workspace_id.
|
||||
# Workspace role is always inherited from the user's platform role
|
||||
# (``Users.platform_role_id``); the iteration over rows above was
|
||||
# a legacy way to find the "highest" workspace role and is no
|
||||
# longer correct now that admin/developer are platform-only.
|
||||
user_role_code: str | None = None
|
||||
for workspace, role, _ in rows:
|
||||
if role.role_code == "admin":
|
||||
user_role_code = "admin"
|
||||
break
|
||||
if user_role_code is None:
|
||||
user_role_code = rows[0][1].role_code
|
||||
if user.platform_role_id is not None:
|
||||
platform_role_row = await session.scalar(
|
||||
select(Roles).where(Roles.role_id == user.platform_role_id)
|
||||
)
|
||||
if platform_role_row is not None:
|
||||
user_role_code = platform_role_row.role_code
|
||||
|
||||
token = issue_jwt(user.user_id, ttl_seconds=COOKIE_TTL_SECONDS)
|
||||
_set_session_cookie(request, response, token)
|
||||
@@ -256,12 +256,12 @@ async def me(
|
||||
workspaces = [_workspace_payload(ws, role) for ws, role, _ in rows]
|
||||
default_workspace_id = workspaces[0]["workspace_id"] if workspaces else None
|
||||
user_role_code: str | None = None
|
||||
for _ws, role, _ in rows:
|
||||
if role.role_code == "admin":
|
||||
user_role_code = "admin"
|
||||
break
|
||||
if user_role_code is None and rows:
|
||||
user_role_code = rows[0][1].role_code
|
||||
if user.platform_role_id is not None:
|
||||
platform_role_row = await session.scalar(
|
||||
select(Roles).where(Roles.role_id == user.platform_role_id)
|
||||
)
|
||||
if platform_role_row is not None:
|
||||
user_role_code = platform_role_row.role_code
|
||||
|
||||
is_system_admin = await resolve_is_system_admin(session, user)
|
||||
permissions = await load_user_permissions(session, user)
|
||||
|
||||
@@ -109,16 +109,22 @@ class WorkspaceUpdate(BaseModel):
|
||||
|
||||
|
||||
class MemberCreate(BaseModel):
|
||||
"""Add a user to a workspace. Role is inherited from the user's
|
||||
platform role (Users.platform_role_id) — not set here."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
user_id: str = Field(min_length=26, max_length=26)
|
||||
role_code: Literal["admin", "developer"]
|
||||
|
||||
|
||||
class MemberUpdate(BaseModel):
|
||||
"""Update a workspace membership's status. Role cannot be changed
|
||||
via this endpoint — workspace role is always inherited from the
|
||||
user's platform role. To change a member's role, PATCH
|
||||
/platform/employees/{user_id} instead."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
role_code: Literal["admin", "developer"] | None = None
|
||||
member_status: Literal["active", "disabled", "locked"] | None = None
|
||||
|
||||
|
||||
@@ -129,6 +135,7 @@ class PlatformEmployeeCreate(BaseModel):
|
||||
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)
|
||||
role_code: Literal["admin", "developer"] | None = None
|
||||
|
||||
|
||||
class PlatformEmployeeUpdate(BaseModel):
|
||||
@@ -387,6 +394,10 @@ async def create_platform_employee(
|
||||
if duplicate is not None:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "用户名或邮箱已存在")
|
||||
|
||||
new_role: Roles | None = None
|
||||
if payload.role_code is not None:
|
||||
new_role = await _load_role_by_code(session, payload.role_code)
|
||||
|
||||
user = Users(
|
||||
user_id=new_ulid(),
|
||||
username=username,
|
||||
@@ -394,12 +405,15 @@ async def create_platform_employee(
|
||||
email=payload.email.strip() if payload.email else None,
|
||||
password_hash=hash_password(payload.password),
|
||||
status="active",
|
||||
platform_role_id=None,
|
||||
platform_role_id=new_role.role_id if new_role is not None else None,
|
||||
)
|
||||
session.add(user)
|
||||
await session.flush()
|
||||
await session.refresh(user)
|
||||
return _envelope(context.request_id, platform_employee_payload(user, None))
|
||||
return _envelope(
|
||||
context.request_id,
|
||||
platform_employee_payload(user, new_role),
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/employees/{user_id}")
|
||||
@@ -724,7 +738,12 @@ async def add_member(
|
||||
context: SystemAdminContext = Depends(system_admin_context),
|
||||
session: AsyncSession = Depends(database_session),
|
||||
) -> dict[str, Any]:
|
||||
"""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'.
|
||||
|
||||
The role is inherited from the target user's ``platform_role_id``;
|
||||
the request body does NOT take a ``role_code``. To change a member's
|
||||
role, PATCH ``/api/v1/platform/employees/{user_id}`` instead.
|
||||
"""
|
||||
await _load_workspace(session, workspace_id)
|
||||
user = await session.get(Users, payload.user_id)
|
||||
if user is None or user.is_deleted != 0:
|
||||
@@ -734,7 +753,20 @@ async def add_member(
|
||||
status.HTTP_409_CONFLICT,
|
||||
f"用户状态为 {user.status},无法加入 workspace",
|
||||
)
|
||||
role = await _load_role_by_code(session, payload.role_code)
|
||||
if user.platform_role_id is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_409_CONFLICT,
|
||||
"目标用户尚未分配平台角色,无法加入 workspace;"
|
||||
"请先 PATCH /api/v1/platform/employees/{user_id} 设置 role_code",
|
||||
)
|
||||
role = await session.scalar(
|
||||
select(Roles).where(Roles.role_id == user.platform_role_id)
|
||||
)
|
||||
if role is None:
|
||||
raise HTTPException(
|
||||
status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
"用户的平台角色行不存在",
|
||||
)
|
||||
duplicate = await session.scalar(
|
||||
select(WorkspaceMembers.user_id).where(
|
||||
WorkspaceMembers.workspace_id == workspace_id,
|
||||
@@ -747,12 +779,6 @@ async def add_member(
|
||||
status.HTTP_409_CONFLICT,
|
||||
"用户已是该 workspace 成员",
|
||||
)
|
||||
# 不变量: 给用户授予 workspace admin 时同步设置 platform_role_id,
|
||||
# 否则前端 role_code === "admin" 与后端 is_system_admin 会给出
|
||||
# 不同的结论。降级路径不在此处处理(用户可能在其他 workspace
|
||||
# 仍是 admin), 升级路径必须在此处理。
|
||||
if role.role_code == "admin" and user.platform_role_id is None:
|
||||
user.platform_role_id = role.role_id
|
||||
membership = WorkspaceMembers(
|
||||
workspace_id=workspace_id,
|
||||
user_id=payload.user_id,
|
||||
@@ -773,7 +799,16 @@ async def update_member(
|
||||
context: SystemAdminContext = Depends(system_admin_context),
|
||||
session: AsyncSession = Depends(database_session),
|
||||
) -> dict[str, Any]:
|
||||
"""Update a member's role and/or status. Last-admin guard applies."""
|
||||
"""Update a workspace membership's status. Role is not editable here.
|
||||
|
||||
Workspace role is always inherited from the user's platform role
|
||||
(``Users.platform_role_id``). To change role, PATCH
|
||||
``/api/v1/platform/employees/{user_id}`` instead.
|
||||
|
||||
Last-admin guard still applies to ``member_status`` changes: setting
|
||||
the only active admin to ``disabled``/``locked`` would leave the
|
||||
workspace without admin coverage.
|
||||
"""
|
||||
await _load_workspace(session, workspace_id)
|
||||
row = (
|
||||
await session.execute(
|
||||
@@ -794,30 +829,6 @@ async def update_member(
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "成员不存在")
|
||||
user, role, membership = row
|
||||
|
||||
next_role = role
|
||||
if payload.role_code is not None and payload.role_code != role.role_code:
|
||||
if (
|
||||
role.role_code == "admin"
|
||||
and payload.role_code != "admin"
|
||||
and membership.member_status == "active"
|
||||
):
|
||||
remaining = await _count_active_admins(
|
||||
session, workspace_id, exclude_user_id=user_id,
|
||||
)
|
||||
if remaining == 0:
|
||||
raise HTTPException(
|
||||
status.HTTP_409_CONFLICT,
|
||||
"workspace 必须保留至少一个 admin",
|
||||
)
|
||||
next_role = await _load_role_by_code(session, payload.role_code)
|
||||
membership.role_id = next_role.role_id
|
||||
# 不变量: 升级到 workspace admin 时强制覆盖 platform_role_id。
|
||||
# (用户可能之前是 developer, platform_role_id 指向 developer role,
|
||||
# 现在变成 admin 必须提升到 admin role。) 降级路径不动 (用户
|
||||
# 可能在其他 workspace 仍是 admin, 端点看不到全局)。
|
||||
if next_role.role_code == "admin":
|
||||
user.platform_role_id = next_role.role_id
|
||||
|
||||
if payload.member_status is not None and payload.member_status != membership.member_status:
|
||||
if (
|
||||
role.role_code == "admin"
|
||||
@@ -835,7 +846,7 @@ async def update_member(
|
||||
|
||||
await session.flush()
|
||||
await session.refresh(membership)
|
||||
return _envelope(context.request_id, member_payload(user, next_role, membership))
|
||||
return _envelope(context.request_id, member_payload(user, role, membership))
|
||||
|
||||
|
||||
@router.delete("/workspaces/{workspace_id}/members/{user_id}")
|
||||
|
||||
Reference in New Issue
Block a user