feat(login-ui): dedupe submissions and surface guard error codes

Collapse rapid double-submits into a single in-flight request across
admin-web, ops-web and the desktop client so users cannot fire parallel
login attempts and trip the new in-progress guard. Map the new
login_rate_limited / login_locked / login_in_progress / login_guard_unavailable
error codes to localized messages in every login surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 01:07:32 +08:00
parent 19037a9e4b
commit 5216a4847c
5 changed files with 104 additions and 31 deletions
+20 -7
View File
@@ -106,6 +106,25 @@ onMounted(() => {
}
});
function formatLoginError(error: unknown): string {
if (error instanceof OpsApiError) {
const messages: Record<string, string> = {
invalid_credentials: "账号或密码错误",
account_disabled: "账号已停用",
login_rate_limited: "登录请求过于频繁",
login_locked: "登录已被临时保护",
login_in_progress: "登录请求正在处理中",
login_guard_unavailable: "登录保护暂时不可用",
};
const message = messages[error.message] ?? error.message;
return error.detail ? `${message}${error.detail}` : message;
}
if (error instanceof Error) {
return error.message;
}
return "登录失败,请稍后重试";
}
async function handleSubmit(): Promise<void> {
if (submitting.value) {
return;
@@ -124,13 +143,7 @@ async function handleSubmit(): Promise<void> {
const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/admin-users";
await router.replace(redirect);
} catch (error) {
if (error instanceof OpsApiError) {
errorMessage.value = error.detail || error.message;
} else if (error instanceof Error) {
errorMessage.value = error.message;
} else {
errorMessage.value = "登录失败,请稍后重试";
}
errorMessage.value = formatLoginError(error);
} finally {
submitting.value = false;
}