Files
ERitors-Scribe-Desktop/electron/database/LocalSystem.ts
natreex dde4683c38 Migrate from electron-store to OS-level secure storage (getSecureStorage)
- Replace `electron-store` with OS-level encrypted storage for secure token, userId, and language management in `LocalSystem` and `keyManager`.
- Add `init-user` IPC handler to initialize user data and manage encryption keys.
- Update login process to handle encrypted storage saving with fallback for macOS issues.
- Add offline warning component to `login/page.tsx` to handle first-time sync requirements.
- Remove `electron-store` and associated dependencies from `package.json` and `package-lock.json`.
2025-11-19 19:10:12 -05:00

106 lines
3.4 KiB
TypeScript

import type { IpcMainInvokeEvent } from 'electron';
import { getSecureStorage } from '../storage/SecureStorage.js';
// ============================================================
// SESSION MANAGEMENT - Auto-inject userId and lang
// ============================================================
/**
* Get userId from secure storage (OS-encrypted)
* Set during login via 'login-success' event
*/
function getUserIdFromSession(): string | null {
const storage = getSecureStorage();
return storage.get<string>('userId', null);
}
/**
* Get lang from secure storage
* Set via 'set-lang' handler, defaults to 'fr'
*/
function getLangFromSession(): 'fr' | 'en' {
const storage = getSecureStorage();
return storage.get<'fr' | 'en'>('userLang', 'fr') as 'fr' | 'en';
}
// ============================================================
// UNIVERSAL HANDLER - Like a Fastify route
// Automatically injects: userId, lang
// Optional body parameter (for GET, POST, PUT, DELETE)
// Generic return type (void, object, etc.)
// ============================================================
/**
* Universal IPC handler - works like a Fastify route
* Automatically injects: userId, lang from session
*
* @template TBody - Request body type (use void for no params)
* @template TReturn - Response type (use void for no return)
*
* @example
* // GET with no params
* ipcMain.handle('db:books:getAll',
* createHandler<void, BookProps[]>(
* async (userId, body, lang) => {
* return await Book.getBooks(userId, lang);
* }
* )
* );
* // Frontend: invoke('db:books:getAll')
*
* @example
* // GET with 1 param
* ipcMain.handle('db:book:get',
* createHandler<string, BookProps>(
* async (userId, bookId, lang) => {
* return await Book.getBook(bookId, userId, lang);
* }
* )
* );
* // Frontend: invoke('db:book:get', bookId)
*
* @example
* // POST with object body
* ipcMain.handle('db:book:create',
* createHandler<CreateBookData, string>(
* async (userId, data, lang) => {
* return await Book.addBook(userId, data, lang);
* }
* )
* );
* // Frontend: invoke('db:book:create', { title: '...', ... })
*
* @example
* // DELETE with void return
* ipcMain.handle('db:book:delete',
* createHandler<string, void>(
* async (userId, bookId, lang) => {
* await Book.deleteBook(bookId, userId, lang);
* }
* )
* );
* // Frontend: invoke('db:book:delete', bookId)
*/
export function createHandler<TBody = void, TReturn = void>(
handler: (userId: string, body: TBody, lang: 'fr' | 'en') => TReturn | Promise<TReturn>
): (event: IpcMainInvokeEvent, body?: TBody) => Promise<TReturn> {
return async function(event: IpcMainInvokeEvent, body?: TBody): Promise<TReturn> {
const userId = getUserIdFromSession();
const lang = getLangFromSession();
if (!userId) {
throw new Error('User not authenticated');
}
try {
return await handler(userId, body as TBody, lang);
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[DB] ${error.message}`);
throw error;
}
throw new Error('An unknown error occurred.');
}
};
}