mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-28 11:01:28 +02:00
137 lines
4.3 KiB
TypeScript
137 lines
4.3 KiB
TypeScript
import type { TFunction } from "i18next";
|
|
|
|
/**
|
|
* Backend error codes returned from Rust Tauri commands.
|
|
* Keep this list in sync with the codes used in `src-tauri/src/profile/password.rs`.
|
|
*/
|
|
export type BackendErrorCode =
|
|
| "INCORRECT_PASSWORD"
|
|
| "LOCKED_OUT"
|
|
| "PROFILE_NOT_FOUND"
|
|
| "PROFILE_NOT_PROTECTED"
|
|
| "PROFILE_ALREADY_PROTECTED"
|
|
| "PROFILE_RUNNING"
|
|
| "PROFILE_EPHEMERAL"
|
|
| "PROFILE_MISSING_SALT"
|
|
| "PROFILE_LOCKED"
|
|
| "INVALID_PROFILE_ID"
|
|
| "PASSWORD_TOO_SHORT"
|
|
| "INVALID_LAUNCH_HOOK_URL"
|
|
| "COOKIE_DB_LOCKED"
|
|
| "COOKIE_DB_UNAVAILABLE"
|
|
| "SELF_HOSTED_REQUIRES_LOGOUT"
|
|
| "INTERNAL_ERROR";
|
|
|
|
export interface BackendError {
|
|
code: BackendErrorCode;
|
|
params?: Record<string, string>;
|
|
}
|
|
|
|
/**
|
|
* Try to parse a backend error string as a structured `{code, params}` payload.
|
|
* Returns null if the string isn't structured (e.g. raw error from a command
|
|
* that doesn't yet emit codes — caller should fall back to showing the raw text).
|
|
*/
|
|
export function parseBackendError(err: unknown): BackendError | null {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
if (!message.startsWith("{")) return null;
|
|
try {
|
|
const parsed = JSON.parse(message);
|
|
if (
|
|
parsed &&
|
|
typeof parsed === "object" &&
|
|
typeof parsed.code === "string"
|
|
) {
|
|
return parsed as BackendError;
|
|
}
|
|
} catch {
|
|
// not JSON
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Translate a backend error to a localized string. Falls back to the raw
|
|
* message if the error isn't a structured backend error.
|
|
*/
|
|
export function translateBackendError(t: TFunction, err: unknown): string {
|
|
const parsed = parseBackendError(err);
|
|
if (!parsed) {
|
|
return err instanceof Error ? err.message : String(err);
|
|
}
|
|
switch (parsed.code) {
|
|
case "INCORRECT_PASSWORD":
|
|
return t("backendErrors.incorrectPassword");
|
|
case "LOCKED_OUT": {
|
|
const seconds = Number.parseInt(parsed.params?.seconds ?? "0", 10);
|
|
return t("backendErrors.lockedOut", {
|
|
duration: formatLockoutDuration(t, seconds),
|
|
});
|
|
}
|
|
case "PROFILE_NOT_FOUND":
|
|
return t("backendErrors.profileNotFound");
|
|
case "PROFILE_NOT_PROTECTED":
|
|
return t("backendErrors.profileNotProtected");
|
|
case "PROFILE_ALREADY_PROTECTED":
|
|
return t("backendErrors.profileAlreadyProtected");
|
|
case "PROFILE_RUNNING":
|
|
return t("backendErrors.profileRunning");
|
|
case "PROFILE_EPHEMERAL":
|
|
return t("backendErrors.profileEphemeral");
|
|
case "PROFILE_MISSING_SALT":
|
|
return t("backendErrors.profileMissingSalt");
|
|
case "PROFILE_LOCKED":
|
|
return t("backendErrors.profileLocked");
|
|
case "INVALID_PROFILE_ID":
|
|
return t("backendErrors.invalidProfileId");
|
|
case "PASSWORD_TOO_SHORT": {
|
|
const min = Number.parseInt(parsed.params?.min ?? "8", 10);
|
|
return t("backendErrors.passwordTooShort", { min });
|
|
}
|
|
case "INVALID_LAUNCH_HOOK_URL":
|
|
return t("backendErrors.invalidLaunchHookUrl");
|
|
case "COOKIE_DB_LOCKED":
|
|
return t("backendErrors.cookieDbLocked");
|
|
case "COOKIE_DB_UNAVAILABLE":
|
|
return t("backendErrors.cookieDbUnavailable");
|
|
case "SELF_HOSTED_REQUIRES_LOGOUT":
|
|
return t("backendErrors.selfHostedRequiresLogout");
|
|
case "INTERNAL_ERROR":
|
|
return t("backendErrors.internal", {
|
|
detail: parsed.params?.detail ?? "",
|
|
});
|
|
default:
|
|
return err instanceof Error ? err.message : String(err);
|
|
}
|
|
}
|
|
|
|
export function formatLockoutDuration(t: TFunction, seconds: number): string {
|
|
if (seconds < 60)
|
|
return t("backendErrors.lockedOutDuration.seconds", { seconds });
|
|
const minutes = Math.ceil(seconds / 60);
|
|
if (minutes < 60)
|
|
return t("backendErrors.lockedOutDuration.minutes", { minutes });
|
|
const hours = Math.ceil(minutes / 60);
|
|
return t("backendErrors.lockedOutDuration.hours", { hours });
|
|
}
|
|
|
|
/**
|
|
* Extract the lockout countdown in seconds from a backend error, or null.
|
|
*/
|
|
export function extractLockoutSeconds(err: unknown): number | null {
|
|
const parsed = parseBackendError(err);
|
|
if (parsed?.code !== "LOCKED_OUT") return null;
|
|
const secs = Number.parseInt(parsed.params?.seconds ?? "0", 10);
|
|
return Number.isFinite(secs) && secs > 0 ? secs : null;
|
|
}
|
|
|
|
/**
|
|
* True if the error is a known structured backend error code.
|
|
*/
|
|
export function isBackendErrorCode(
|
|
err: unknown,
|
|
code: BackendErrorCode,
|
|
): boolean {
|
|
return parseBackendError(err)?.code === code;
|
|
}
|