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
+29 -14
View File
@@ -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> {