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:
@@ -33,21 +33,36 @@ export const useAuthStore = defineStore("ops-auth", () => {
|
||||
initialized.value = true;
|
||||
}
|
||||
|
||||
let loginPromise: Promise<void> | null = null;
|
||||
|
||||
async function login(username: string, password: string, remember: boolean): Promise<void> {
|
||||
const result = await http.post<LoginResponse>("/auth/login", { username, password });
|
||||
accessToken.value = result.access_token;
|
||||
expiresAt.value = result.expires_at;
|
||||
operator.value = result.operator;
|
||||
persistent.value = remember;
|
||||
writeStoredSession(
|
||||
{
|
||||
accessToken: result.access_token,
|
||||
expiresAt: result.expires_at,
|
||||
operator: result.operator,
|
||||
},
|
||||
remember,
|
||||
);
|
||||
writeLoginPreference({ remember, lastUsername: username });
|
||||
if (loginPromise) {
|
||||
return loginPromise;
|
||||
}
|
||||
|
||||
const payload = { username, password, remember };
|
||||
loginPromise = http.post<LoginResponse>("/auth/login", {
|
||||
username: payload.username,
|
||||
password: payload.password,
|
||||
}).then((result) => {
|
||||
accessToken.value = result.access_token;
|
||||
expiresAt.value = result.expires_at;
|
||||
operator.value = result.operator;
|
||||
persistent.value = payload.remember;
|
||||
writeStoredSession(
|
||||
{
|
||||
accessToken: result.access_token,
|
||||
expiresAt: result.expires_at,
|
||||
operator: result.operator,
|
||||
},
|
||||
payload.remember,
|
||||
);
|
||||
writeLoginPreference({ remember: payload.remember, lastUsername: payload.username });
|
||||
}).finally(() => {
|
||||
loginPromise = null;
|
||||
});
|
||||
|
||||
return loginPromise;
|
||||
}
|
||||
|
||||
async function refreshSelf(): Promise<void> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user