fix(panel): 本人与所有者账户的禁用和删除按钮提前锁定并说明原因,角色改为只读,后端拒绝改用专用错误码 self_protected 与 owner_protected

This commit is contained in:
Lemon-miaow committed 2026-09-25 13:20:01 +08:00
1 parent dee4d87fd1
commit fde677c07e
13 files changed
+269 -27

No files matched your search

+10
View File
@@ -392,6 +392,16 @@ describe("api access-control wire shapes", () => {
expect(humanizeError({ code: "console_unavailable" })).toMatch(/console/i);
});
it("says why a user change was refused for the caller's own or the owner account", async () => {
const { humanizeError } = await import("./api");
expect(humanizeError({ status: 403, code: "self_protected" })).toBe(
"You can't do that to the account you're signed in with.",
);
expect(humanizeError({ status: 403, code: "owner_protected" })).toBe(
"The owner account can't be demoted, disabled or deleted from the panel. Only the host's break-glass console (sudo felis breakGlass) manages it.",
);
});
it("maps the backup rationing codes to their own copy", async () => {
const { humanizeError } = await import("./api");
expect(humanizeError({ code: "backup_cooldown" })).toMatch(/cooldown/i);
+6
View File
@@ -822,6 +822,12 @@ export function humanizeError(e: unknown): string {
return t("passkey_unavailable");
case "last_passkey":
return t("last_passkey");
// User admin (internal/api/handlers_users.go): the caller's own account and
// the owner account are refused, each for its own reason.
case "self_protected":
return t("self_protected");
case "owner_protected":
return t("owner_protected");
case "quota_exceeded":
return t("quota_exceeded");
case "already_claimed":
+27 -3
View File
@@ -5205,7 +5205,15 @@ export interface operations {
};
};
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
/** @description Not an owner (forbidden); the caller's own account (self_protected); or the owner account (owner_protected), which only the host's break-glass console (sudo felis breakGlass) may remove. */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
404: components["responses"]["NotFound"];
};
};
@@ -5241,7 +5249,15 @@ export interface operations {
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
/** @description Not an owner (forbidden); a change to the caller's own role (self_protected); or a role change on the owner account (owner_protected), which only the host's break-glass console (sudo felis breakGlass) may make. */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
404: components["responses"]["NotFound"];
/** @description Username conflict. */
409: {
@@ -5284,7 +5300,15 @@ export interface operations {
};
};
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
/** @description Not an owner (forbidden); the caller's own account (self_protected); or disabling the owner account (owner_protected). Re-enabling the owner is allowed. */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
404: components["responses"]["NotFound"];
};
};