Files
ctms/frontend/src/runtime/savedLoginCredentials.ts
T

161 lines
5.5 KiB
TypeScript

import { getDesktopServerUrl } from "./desktopServerConfig";
import { isTauriRuntime } from "./platform";
const SAVED_LOGIN_CREDENTIAL_VERSION = 1;
export interface SavedLoginCredential {
email: string;
password: string;
}
type StoredLoginCredential = {
version?: unknown;
email?: unknown;
password?: unknown;
savedAt?: unknown;
};
const invokeLoginCredential = async <T>(
command: "login_credential_get" | "login_credential_set" | "login_credential_delete",
args: Record<string, string>,
): Promise<T> => {
const { invoke } = await import("@tauri-apps/api/core");
return invoke<T>(command, args);
};
const normalizeEmail = (email: string): string => email.trim().toLowerCase();
const serializeDesktopCredential = (credential: SavedLoginCredential): string =>
JSON.stringify({
version: SAVED_LOGIN_CREDENTIAL_VERSION,
email: normalizeEmail(credential.email),
password: credential.password,
savedAt: Date.now(),
});
const parseDesktopCredential = (stored: string | null): SavedLoginCredential | null => {
if (!stored) return null;
try {
const payload = JSON.parse(stored) as StoredLoginCredential;
if (
payload.version !== SAVED_LOGIN_CREDENTIAL_VERSION ||
typeof payload.email !== "string" ||
typeof payload.password !== "string" ||
typeof payload.savedAt !== "number"
) {
return null;
}
const email = normalizeEmail(payload.email);
if (!email || !payload.password) return null;
return { email, password: payload.password };
} catch {
return null;
}
};
const getDesktopSavedLoginCredential = async (): Promise<SavedLoginCredential | null> => {
const serverOrigin = getDesktopServerUrl();
if (!serverOrigin) return null;
const stored = await invokeLoginCredential<string | null>("login_credential_get", { serverOrigin });
const parsed = parseDesktopCredential(stored);
if (!parsed && stored) {
await invokeLoginCredential<void>("login_credential_delete", { serverOrigin });
}
return parsed;
};
const saveDesktopLoginCredential = async (credential: SavedLoginCredential): Promise<boolean> => {
const serverOrigin = getDesktopServerUrl();
if (!serverOrigin) return false;
await invokeLoginCredential<void>("login_credential_set", {
serverOrigin,
credential: serializeDesktopCredential(credential),
});
return true;
};
const clearDesktopLoginCredential = async (): Promise<boolean> => {
const serverOrigin = getDesktopServerUrl();
if (!serverOrigin) return false;
await invokeLoginCredential<void>("login_credential_delete", { serverOrigin });
return true;
};
const getPasswordCredentialConstructor = (): (new (data: { id: string; name?: string; password: string }) => unknown) | null => {
const ctor = (window as unknown as { PasswordCredential?: unknown }).PasswordCredential;
return typeof ctor === "function"
? (ctor as new (data: { id: string; name?: string; password: string }) => unknown)
: null;
};
const getBrowserCredentialContainer = ():
| {
get?: (options: Record<string, unknown>) => Promise<unknown>;
store?: (credential: unknown) => Promise<unknown>;
preventSilentAccess?: () => Promise<void>;
}
| null => {
const container = (navigator as unknown as { credentials?: unknown }).credentials;
return container && typeof container === "object" ? (container as any) : null;
};
const getBrowserSavedLoginCredential = async (): Promise<SavedLoginCredential | null> => {
const credentials = getBrowserCredentialContainer();
if (!credentials?.get) return null;
try {
const credential = await credentials.get({
password: true,
mediation: "optional",
});
const passwordCredential = credential as { id?: unknown; password?: unknown } | null;
if (typeof passwordCredential?.id !== "string" || typeof passwordCredential.password !== "string") {
return null;
}
const email = normalizeEmail(passwordCredential.id);
if (!email || !passwordCredential.password) return null;
return { email, password: passwordCredential.password };
} catch {
return null;
}
};
const saveBrowserLoginCredential = async (credential: SavedLoginCredential): Promise<boolean> => {
const credentials = getBrowserCredentialContainer();
const PasswordCredential = getPasswordCredentialConstructor();
if (!credentials?.store || !PasswordCredential) return false;
const email = normalizeEmail(credential.email);
if (!email || !credential.password) return false;
await credentials.store(
new PasswordCredential({
id: email,
name: email,
password: credential.password,
}),
);
return true;
};
const clearBrowserLoginCredential = async (): Promise<boolean> => {
const credentials = getBrowserCredentialContainer();
if (!credentials?.preventSilentAccess) return false;
await credentials.preventSilentAccess();
return true;
};
export const getSavedLoginCredential = async (): Promise<SavedLoginCredential | null> => {
if (isTauriRuntime()) return getDesktopSavedLoginCredential();
return getBrowserSavedLoginCredential();
};
export const saveLoginCredential = async (email: string, password: string): Promise<boolean> => {
const credential = { email: normalizeEmail(email), password };
if (!credential.email || !credential.password) return false;
if (isTauriRuntime()) return saveDesktopLoginCredential(credential);
return saveBrowserLoginCredential(credential);
};
export const clearLoginCredential = async (): Promise<boolean> => {
if (isTauriRuntime()) return clearDesktopLoginCredential();
return clearBrowserLoginCredential();
};