mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-18 18:37:27 +02:00
feat: password protected profiles
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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_MISSING_SALT"
|
||||
| "PROFILE_LOCKED"
|
||||
| "INVALID_PROFILE_ID"
|
||||
| "PASSWORD_TOO_SHORT"
|
||||
| "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_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 "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;
|
||||
}
|
||||
Reference in New Issue
Block a user