161 lines
5.5 KiB
TypeScript
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();
|
|
};
|