import { useDbContext } from "@/context/db/db"; import { deriveDeviceKey, getDeviceId } from "@/util/deviceId"; import { useCallback } from "react"; export interface RawLocalAccount { accountId: string; label: string; email: string; profilePicture: string; access: { data: number[]; iv: number[] }; refresh: { data: number[]; iv: number[] }; updatedAt: string; } export interface LocalAccount { accountId: string; label: string; email: string; profilePicture: string; access: string; refresh: string; updatedAt: string; } export interface CreateAccountRequest { accountId: string; label: string; email: string; profilePicture: string; access: string; refresh: string; } export const useAccountRepo = () => { const { db } = useDbContext(); const getDeviceKey = useCallback(async () => { const deviceId = await getDeviceId(); const deviceKey = await deriveDeviceKey(deviceId); return deviceKey; }, []); const encryptToken = useCallback( async (token: string) => { const encoder = new TextEncoder(); const iv = crypto.getRandomValues(new Uint8Array(12)); const deviceKey = await getDeviceKey(); const cipherText = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, deviceKey, encoder.encode(token) ); return { data: Array.from(new Uint8Array(cipherText)), iv: Array.from(iv), }; }, [getDeviceKey] ); const decryptToken = useCallback( async (encrypted: { data: number[]; iv: number[] }) => { const decoder = new TextDecoder(); const deviceKey = await getDeviceKey(); const decrypted = await crypto.subtle.decrypt( { name: "AES-GCM", iv: new Uint8Array(encrypted.iv), }, deviceKey, new Uint8Array(encrypted.data) ); return decoder.decode(decrypted); }, [getDeviceKey] ); const save = useCallback( async (req: CreateAccountRequest) => { if (!db) throw new Error("No database connection"); const access = await encryptToken(req.access); const refresh = await encryptToken(req.refresh); await db?.put?.("accounts", { accountId: req.accountId, label: req.label, email: req.email, profilePicture: req.profilePicture, access, refresh, updatedAt: new Date().toISOString(), }); }, [db, encryptToken] ); const loadAll = useCallback(async () => { if (!db) throw new Error("No database connection"); const tx = db.transaction("accounts", "readonly"); const store = tx.objectStore("accounts"); const accounts: RawLocalAccount[] = await store.getAll(); const results: LocalAccount[] = ( await Promise.all( accounts.map(async (account) => { try { const accessToken = await decryptToken(account.access); const refreshToken = await decryptToken(account.refresh); return { accountId: account.accountId, label: account.label, email: account.email, profilePicture: account.profilePicture, access: accessToken, refresh: refreshToken, updatedAt: account.updatedAt, }; } catch (err) { console.warn(`Failed to decrypt account ${account.label}:`, err); } }) ) ).filter((acc) => acc !== undefined); return results; }, [db, decryptToken]); return { save, loadAll }; };