Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
Winnie
2026-08-05 17:49:29 +08:00
6 changed files with 76 additions and 3 deletions
+29 -1
View File
@@ -137,6 +137,11 @@ async def create_employee(
email=payload.email.strip() if payload.email else None,
password_hash=hash_password(payload.password),
status="active",
# 不变量: workspace admin == 系统管理员, 因此 role_code == "admin"
# 时必须同步把 platform_role_id 指向 admin role, 否则
# resolve_is_system_admin 会返回 False, 但前端的 role_code
# 判断仍然命中, 两端出现分歧。
platform_role_id=role.role_id if role.role_code == "admin" else None,
)
session.add(user)
session.add(
@@ -169,9 +174,32 @@ async def update_employee(
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:
if payload.status is not None and payload.status != user.status:
if user.user_id == context.user.user_id and payload.status != "active":
raise HTTPException(
status.HTTP_409_CONFLICT,
"不能停用当前登录账号",
)
if role.role_code == "admin" and payload.status in ("disabled", "locked"):
raise HTTPException(
status.HTTP_409_CONFLICT,
"不能停用或锁定管理员账号",
)
user.status = payload.status
if payload.role_code is not None and payload.role_code != role.role_code:
if role.role_code == "admin" and payload.role_code != "admin":
raise HTTPException(
status.HTTP_409_CONFLICT,
"不能降级管理员账号",
)
if (
user.user_id == context.user.user_id
and payload.role_code != "admin"
):
raise HTTPException(
status.HTTP_409_CONFLICT,
"不能降级自身管理员角色",
)
next_role = await session.scalar(
select(Roles).where(Roles.role_code == payload.role_code)
)
+12
View File
@@ -471,6 +471,12 @@ 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,
@@ -529,6 +535,12 @@ async def update_member(
)
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 (
@@ -79,6 +79,27 @@ export function UserManagementPage({
onNotify({ tone: "error", message: "请输入密码" });
return;
}
// 编辑自身或管理员时,前端二次拦截禁用 status / role_code 的修改
if (editing) {
const editingSelf = editing.user_id === user?.user_id;
const targetIsAdmin = editing.role_code === "admin";
if (editingSelf && form.status !== editing.status) {
onNotify({ tone: "error", message: "不能停用当前登录账号" });
return;
}
if (targetIsAdmin && form.status !== editing.status) {
onNotify({ tone: "error", message: "不能停用或锁定管理员账号" });
return;
}
if (targetIsAdmin && form.role_code !== editing.role_code) {
onNotify({ tone: "error", message: "不能降级管理员账号" });
return;
}
if (editingSelf && form.role_code !== editing.role_code) {
onNotify({ tone: "error", message: "不能降级自身管理员角色" });
return;
}
}
// 密码长度校验 8~72 字符
if (form.password && (form.password.length < 8 || form.password.length > 72)) {
onNotify({ tone: "error", message: "密码长度必须在 8~72 字符之间" });
@@ -245,7 +266,7 @@ export function UserManagementPage({
</label>
<label className="form-field">
<span></span>
<select value={form.role_code} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}>
<select value={form.role_code} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}>
<option value="developer"></option>
<option value="admin"></option>
</select>
@@ -253,7 +274,7 @@ export function UserManagementPage({
{editing && (
<label className="form-field">
<span></span>
<select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}>
<select value={form.status} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}>
<option value="active"></option>
<option value="disabled"></option>
<option value="locked"></option>
+1
View File
@@ -19,6 +19,7 @@ export type AuthUser = {
email: string | null;
status: string;
role_code: string | null;
is_system_admin: boolean;
};
export type AuthWorkspace = {
+8
View File
@@ -12,5 +12,13 @@ RUN mkdir -p /root/.jupyter/custom /app/.venv/share/jupyter/custom && \
echo '#top-panel, #top-panel-wrapper, .jp-Notebook-header, .jp-FileEditorHeader {display: none !important; height: 0 !important; min-height: 0 !important; margin: 0 !important; padding: 0 !important; border: none !important; position: absolute !important; pointer-events: none !important; }' > /root/.jupyter/custom/custom.css && \
cp /root/.jupyter/custom/custom.css /app/.venv/share/jupyter/custom/custom.css
RUN uv venv --python 3.8 /opt/venv/python3.8 && \
UV_DEFAULT_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple/" uv pip install --python /opt/venv/python3.8/bin/python ipykernel && \
/opt/venv/python3.8/bin/python -m ipykernel install --prefix=/app/.venv --name=python38 --display-name="python3.8" \
RUN uv venv --python 3.10 /opt/venv/python3.10 && \
UV_DEFAULT_INDEX="https://pypi.tuna.tsinghua.edu.cn/simple/" uv pip install --python /opt/venv/python3.10/bin/python ipykernel && \
/opt/venv/python3.10/bin/python -m ipykernel install --prefix=/app/.venv --name=python310 --display-name="python3.10"
EXPOSE 8000
CMD ["uv", "run", "--frozen", "--package", "runtime", "gunicorn", "--config", "runtime/gunicorn.conf.py", "runtime.main:app"]
+3
View File
@@ -17,6 +17,9 @@ WORKDIR /app
# uv 包管理器(多个业务镜像都需要)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
RUN uv python install 3.8
RUN uv python install 3.10
# 替换 apt 源为阿里云镜像(兼容 debian.sources / sources.list 两种格式)
RUN ( \
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources 2>/dev/null || \