firebase WIP

This commit is contained in:
samtiz 2025-07-09 04:56:20 +09:00
parent 9977387fbc
commit fae6962297
31 changed files with 1189 additions and 162 deletions

51
PLAN.md Normal file
View File

@ -0,0 +1,51 @@
# Project Plan: Firebase Integration & Encryption
This document outlines the plan to integrate Firebase Firestore as a remote database for logged-in users and implement end-to-end encryption for user data.
## Phase 1: `encryptionService` and Secure Key Management
The goal of this phase is to create a centralized service for data encryption and decryption, with secure management of encryption keys.
1. **Install `keytar`**: Add the `keytar` package to the project to securely store encryption keys in the OS keychain.
2. **Create `encryptionService.js`**:
- Location: `src/common/services/encryptionService.js`
- Implement `encrypt(text)` and `decrypt(encrypted)` functions using Node.js `crypto` with `AES-256-GCM`.
3. **Implement Key Management**:
- Create an `initializeKey(userId)` function within the service.
- This function will first attempt to retrieve the encryption key from `keytar`.
- If `keytar` fails or no key is found, it will generate a secure, session-only key in memory as a fallback. It will **not** save the key to an insecure location like `electron-store`.
## Phase 2: Automatic Encryption/Decryption via Firestore Converter
This phase aims to abstract away the encryption/decryption logic from the repository layer, making it automatic.
1. **Create `firestoreConverter.js`**:
- Location: `src/common/repositories/firestoreConverter.js`
- Implement a factory function `createEncryptedConverter(fieldsToEncrypt = [])`.
- This function will return a Firestore converter object with `toFirestore` and `fromFirestore` methods.
- `toFirestore`: Will automatically encrypt the specified fields before writing data to Firestore.
- `fromFirestore`: Will automatically decrypt the specified fields after reading data from Firestore.
## Phase 3: Implement Firebase Repositories
With the encryption layer ready, we will create the Firebase equivalents of the existing SQLite repositories.
1. **Create `session/firebase.repository.js`**:
- Location: `src/common/repositories/session/firebase.repository.js`
- Use the `createEncryptedConverter` to encrypt fields like `title`.
- Implement all functions from the SQLite counterpart (`create`, `getById`, `getOrCreateActive`, etc.) using Firestore APIs.
2. **Create `ask/repositories/firebase.repository.js`**:
- Location: `src/features/ask/repositories/firebase.repository.js`
- Use the `createEncryptedConverter` to encrypt the `content` field of AI messages.
- Implement all functions from the SQLite counterpart (`addAiMessage`, `getAllAiMessagesBySessionId`).
## Phase 4: Integrate Repository Strategy Pattern
This final phase will activate the logic that switches between local and remote databases based on user authentication status.
1. **Update `getRepository()` functions**:
- Modify `src/common/repositories/session/index.js` and `src/features/ask/repositories/index.js`.
- In the `getRepository()` function, use `authService.getCurrentUser()` to check if the user is logged in (`user.isLoggedIn`).
- If logged in, return the `firebaseRepository`.
- Otherwise, return the `sqliteRepository`.
- Uncomment the `require` statements for the newly created Firebase repositories.

18
package-lock.json generated
View File

@ -24,6 +24,7 @@
"firebase": "^11.10.0",
"firebase-admin": "^13.4.0",
"jsonwebtoken": "^9.0.2",
"keytar": "^7.9.0",
"node-fetch": "^2.7.0",
"openai": "^4.70.0",
"react-hot-toast": "^2.5.2",
@ -8090,6 +8091,23 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/keytar": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
"integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^4.3.0",
"prebuild-install": "^7.0.1"
}
},
"node_modules/keytar/node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==",
"license": "MIT"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",

View File

@ -1,9 +1,7 @@
{
"name": "pickle-glass",
"productName": "Glass",
"version": "0.2.2",
"description": "Cl*ely for Free",
"main": "src/index.js",
"scripts": {
@ -48,6 +46,7 @@
"firebase": "^11.10.0",
"firebase-admin": "^13.4.0",
"jsonwebtoken": "^9.0.2",
"keytar": "^7.9.0",
"node-fetch": "^2.7.0",
"openai": "^4.70.0",
"react-hot-toast": "^2.5.2",

77
repository_api_report.md Normal file
View File

@ -0,0 +1,77 @@
# Repository API Report
이 문서는 각 리포지토리 모듈의 공개 API 명세를 정리합니다. 모든 서비스 레이어는 여기에 명시된 함수 시그니처를 따라야 합니다. `uid`는 어댑터 레이어에서 자동으로 주입되므로 서비스 레이어에서 전달해서는 안 됩니다.
---
### Session Repository
**Path:** `src/common/repositories/session/`
- `getById(id: string)`
- `create(type: 'ask' | 'listen' = 'ask')`
- `getAllByUserId()`
- `updateTitle(id: string, title: string)`
- `deleteWithRelatedData(id:string)`
- `end(id: string)`
- `updateType(id: string, type: 'ask' | 'listen')`
- `touch(id: string)`
- `getOrCreateActive(requestedType: 'ask' | 'listen' = 'ask')`
- `endAllActiveSessions()`
---
### User Repository
**Path:** `src/common/repositories/user/`
- `findOrCreate(user: object)`
- `getById()`
- `saveApiKey(apiKey: string, provider: string)`
- `update(updateData: object)`
- `deleteById()`
---
### Preset Repository
**Path:** `src/common/repositories/preset/`
- `getPresets()`
- `getPresetTemplates()`
- `create(options: { title: string, prompt: string })`
- `update(id: string, options: { title: string, prompt: string })`
- `delete(id: string)`
---
### Ask Repository (AI Messages)
**Path:** `src/features/ask/repositories/`
- `addAiMessage(options: { sessionId: string, role: string, content: string, model?: string })`
- `getAllAiMessagesBySessionId(sessionId: string)`
---
### STT Repository (Transcripts)
**Path:** `src/features/listen/stt/repositories/`
- `addTranscript(options: { sessionId: string, speaker: string, text: string })`
- `getAllTranscriptsBySessionId(sessionId: string)`
---
### Summary Repository
**Path:** `src/features/listen/summary/repositories/`
- `saveSummary(options: { sessionId: string, tldr: string, text: string, bullet_json: string, action_json: string, model?: string })`
- `getSummaryBySessionId(sessionId: string)`
---
### Settings Repository (Presets)
**Path:** `src/features/settings/repositories/`
*(Note: This is largely a duplicate of the main Preset Repository and might be a candidate for future refactoring.)*
- `getPresets()`
- `getPresetTemplates()`
- `createPreset(options: { title: string, prompt: string })`
- `updatePreset(id: string, options: { title: string, prompt: string })`
- `deletePreset(id: string)`

View File

@ -0,0 +1,45 @@
const encryptionService = require('../services/encryptionService');
/**
* Creates a Firestore converter that automatically encrypts and decrypts specified fields.
* @param {string[]} fieldsToEncrypt - An array of field names to encrypt.
* @returns {import('@firebase/firestore').FirestoreDataConverter<T>} A Firestore converter.
* @template T
*/
function createEncryptedConverter(fieldsToEncrypt = []) {
return {
/**
* @param {import('@firebase/firestore').DocumentData} appObject
*/
toFirestore: (appObject) => {
const firestoreData = { ...appObject };
for (const field of fieldsToEncrypt) {
if (Object.prototype.hasOwnProperty.call(firestoreData, field) && firestoreData[field] != null) {
firestoreData[field] = encryptionService.encrypt(firestoreData[field]);
}
}
// Ensure there's a timestamp for the last modification
firestoreData.updated_at = Math.floor(Date.now() / 1000);
return firestoreData;
},
/**
* @param {import('@firebase/firestore').QueryDocumentSnapshot} snapshot
* @param {import('@firebase/firestore').SnapshotOptions} options
*/
fromFirestore: (snapshot, options) => {
const firestoreData = snapshot.data(options);
const appObject = { ...firestoreData, id: snapshot.id }; // include the document ID
for (const field of fieldsToEncrypt) {
if (Object.prototype.hasOwnProperty.call(appObject, field) && appObject[field] != null) {
appObject[field] = encryptionService.decrypt(appObject[field]);
}
}
return appObject;
}
};
}
module.exports = {
createEncryptedConverter,
};

View File

@ -0,0 +1,94 @@
const { getFirestore, collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy } = require('firebase/firestore');
const { createEncryptedConverter } = require('../firestoreConverter');
const userPresetConverter = createEncryptedConverter(['prompt', 'title']);
const defaultPresetConverter = {
toFirestore: (data) => data,
fromFirestore: (snapshot, options) => {
const data = snapshot.data(options);
return { ...data, id: snapshot.id };
}
};
function userPresetsCol() {
const db = getFirestore();
return collection(db, 'prompt_presets').withConverter(userPresetConverter);
}
function defaultPresetsCol() {
const db = getFirestore();
return collection(db, 'defaults/prompt_presets').withConverter(defaultPresetConverter);
}
async function getPresets(uid) {
const userPresetsQuery = query(userPresetsCol(), where('uid', '==', uid));
const defaultPresetsQuery = query(defaultPresetsCol()); // Defaults have no owner
const [userSnapshot, defaultSnapshot] = await Promise.all([
getDocs(userPresetsQuery),
getDocs(defaultPresetsQuery)
]);
const presets = [
...defaultSnapshot.docs.map(d => d.data()),
...userSnapshot.docs.map(d => d.data())
];
return presets.sort((a, b) => {
if (a.is_default && !b.is_default) return -1;
if (!a.is_default && b.is_default) return 1;
return a.title.localeCompare(b.title);
});
}
async function getPresetTemplates() {
const q = query(defaultPresetsCol(), orderBy('title', 'asc'));
const snapshot = await getDocs(q);
return snapshot.docs.map(doc => doc.data());
}
async function create({ uid, title, prompt }) {
const now = Math.floor(Date.now() / 1000);
const newPreset = {
uid: uid,
title,
prompt,
is_default: false,
created_at: now,
};
const docRef = await addDoc(userPresetsCol(), newPreset);
return { id: docRef.id };
}
async function update(id, { title, prompt }, uid) {
const docRef = doc(userPresetsCol(), id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
throw new Error("Preset not found or permission denied to update.");
}
await updateDoc(docRef, { title, prompt });
return { changes: 1 };
}
async function del(id, uid) {
const docRef = doc(userPresetsCol(), id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
throw new Error("Preset not found or permission denied to delete.");
}
await deleteDoc(docRef);
return { changes: 1 };
}
module.exports = {
getPresets,
getPresetTemplates,
create,
update,
delete: del,
};

View File

@ -1,19 +1,39 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository');
const firebaseRepository = require('./firebase.repository');
const authService = require('../../../common/services/authService');
function getRepository() {
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
function getBaseRepository() {
const user = authService.getCurrentUser();
if (user && user.isLoggedIn) {
return firebaseRepository;
}
return sqliteRepository;
}
module.exports = {
getPresets: (...args) => getRepository().getPresets(...args),
getPresetTemplates: (...args) => getRepository().getPresetTemplates(...args),
create: (...args) => getRepository().create(...args),
update: (...args) => getRepository().update(...args),
delete: (...args) => getRepository().delete(...args),
const presetRepositoryAdapter = {
getPresets: () => {
const uid = authService.getCurrentUserId();
return getBaseRepository().getPresets(uid);
},
getPresetTemplates: () => {
return getBaseRepository().getPresetTemplates();
},
create: (options) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().create({ uid, ...options });
},
update: (id, options) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().update(id, options, uid);
},
delete: (id) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().delete(id, uid);
},
};
module.exports = presetRepositoryAdapter;

View File

@ -0,0 +1,155 @@
const { getFirestore, doc, getDoc, collection, addDoc, query, where, getDocs, writeBatch, orderBy, limit, runTransaction, updateDoc } = require('firebase/firestore');
const { createEncryptedConverter } = require('../firestoreConverter');
const sessionConverter = createEncryptedConverter(['title']);
function sessionsCol() {
const db = getFirestore();
return collection(db, 'sessions').withConverter(sessionConverter);
}
// Sub-collection references are now built from the top-level
function subCollections(sessionId) {
const db = getFirestore();
const sessionPath = `sessions/${sessionId}`;
return {
transcripts: collection(db, `${sessionPath}/transcripts`),
ai_messages: collection(db, `${sessionPath}/ai_messages`),
summary: collection(db, `${sessionPath}/summary`),
}
}
async function getById(id) {
const docRef = doc(sessionsCol(), id);
const docSnap = await getDoc(docRef);
return docSnap.exists() ? docSnap.data() : null;
}
async function create(uid, type = 'ask') {
const now = Math.floor(Date.now() / 1000);
const newSession = {
uid: uid,
members: [uid], // For future sharing functionality
title: `Session @ ${new Date().toLocaleTimeString()}`,
session_type: type,
started_at: now,
updated_at: now,
ended_at: null,
};
const docRef = await addDoc(sessionsCol(), newSession);
console.log(`Firebase: Created session ${docRef.id} for user ${uid}`);
return docRef.id;
}
async function getAllByUserId(uid) {
const q = query(sessionsCol(), where('members', 'array-contains', uid), orderBy('started_at', 'desc'));
const querySnapshot = await getDocs(q);
return querySnapshot.docs.map(doc => doc.data());
}
async function updateTitle(id, title) {
const docRef = doc(sessionsCol(), id);
await updateDoc(docRef, { title });
return { changes: 1 };
}
async function deleteWithRelatedData(id) {
const db = getFirestore();
const batch = writeBatch(db);
const { transcripts, ai_messages, summary } = subCollections(id);
const [transcriptsSnap, aiMessagesSnap, summarySnap] = await Promise.all([
getDocs(query(transcripts)),
getDocs(query(ai_messages)),
getDocs(query(summary)),
]);
transcriptsSnap.forEach(d => batch.delete(d.ref));
aiMessagesSnap.forEach(d => batch.delete(d.ref));
summarySnap.forEach(d => batch.delete(d.ref));
const sessionRef = doc(sessionsCol(), id);
batch.delete(sessionRef);
await batch.commit();
return { success: true };
}
async function end(id) {
const docRef = doc(sessionsCol(), id);
await updateDoc(docRef, { ended_at: Math.floor(Date.now() / 1000) });
return { changes: 1 };
}
async function updateType(id, type) {
const docRef = doc(sessionsCol(), id);
await updateDoc(docRef, { session_type: type });
return { changes: 1 };
}
async function touch(id) {
const docRef = doc(sessionsCol(), id);
await updateDoc(docRef, { updated_at: Math.floor(Date.now() / 1000) });
return { changes: 1 };
}
async function getOrCreateActive(uid, requestedType = 'ask') {
const findQuery = query(
sessionsCol(),
where('uid', '==', uid),
where('ended_at', '==', null),
orderBy('session_type', 'desc'),
limit(1)
);
const activeSessionSnap = await getDocs(findQuery);
if (!activeSessionSnap.empty) {
const activeSessionDoc = activeSessionSnap.docs[0];
const sessionRef = doc(sessionsCol(), activeSessionDoc.id);
const activeSession = activeSessionDoc.data();
console.log(`[Repo] Found active Firebase session ${activeSession.id}`);
const updates = { updated_at: Math.floor(Date.now() / 1000) };
if (activeSession.session_type === 'ask' && requestedType === 'listen') {
updates.session_type = 'listen';
console.log(`[Repo] Promoted Firebase session ${activeSession.id} to 'listen' type.`);
}
await updateDoc(sessionRef, updates);
return activeSessionDoc.id;
} else {
console.log(`[Repo] No active Firebase session for user ${uid}. Creating new.`);
return create(uid, requestedType);
}
}
async function endAllActiveSessions(uid) {
const q = query(sessionsCol(), where('uid', '==', uid), where('ended_at', '==', null));
const snapshot = await getDocs(q);
if (snapshot.empty) return { changes: 0 };
const batch = writeBatch(getFirestore());
snapshot.forEach(d => {
batch.update(d.ref, { ended_at: Math.floor(Date.now() / 1000) });
});
await batch.commit();
console.log(`[Repo] Ended ${snapshot.size} active session(s) for user ${uid}.`);
return { changes: snapshot.size };
}
module.exports = {
getById,
create,
getAllByUserId,
updateTitle,
deleteWithRelatedData,
end,
updateType,
touch,
getOrCreateActive,
endAllActiveSessions,
};

View File

@ -1,26 +1,48 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository'); // Future implementation
const firebaseRepository = require('./firebase.repository');
const authService = require('../../../common/services/authService');
function getRepository() {
// In the future, we can check the user's login status from authService
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
function getBaseRepository() {
const user = authService.getCurrentUser();
if (user && user.isLoggedIn) {
return firebaseRepository;
}
return sqliteRepository;
}
// Directly export functions for ease of use, decided by the strategy
module.exports = {
getById: (...args) => getRepository().getById(...args),
create: (...args) => getRepository().create(...args),
getAllByUserId: (...args) => getRepository().getAllByUserId(...args),
updateTitle: (...args) => getRepository().updateTitle(...args),
deleteWithRelatedData: (...args) => getRepository().deleteWithRelatedData(...args),
end: (...args) => getRepository().end(...args),
updateType: (...args) => getRepository().updateType(...args),
touch: (...args) => getRepository().touch(...args),
getOrCreateActive: (...args) => getRepository().getOrCreateActive(...args),
endAllActiveSessions: (...args) => getRepository().endAllActiveSessions(...args),
// The adapter layer that injects the UID
const sessionRepositoryAdapter = {
getById: (id) => getBaseRepository().getById(id),
create: (type = 'ask') => {
const uid = authService.getCurrentUserId();
return getBaseRepository().create(uid, type);
},
getAllByUserId: () => {
const uid = authService.getCurrentUserId();
return getBaseRepository().getAllByUserId(uid);
},
updateTitle: (id, title) => getBaseRepository().updateTitle(id, title),
deleteWithRelatedData: (id) => getBaseRepository().deleteWithRelatedData(id),
end: (id) => getBaseRepository().end(id),
updateType: (id, type) => getBaseRepository().updateType(id, type),
touch: (id) => getBaseRepository().touch(id),
getOrCreateActive: (requestedType = 'ask') => {
const uid = authService.getCurrentUserId();
return getBaseRepository().getOrCreateActive(uid, requestedType);
},
endAllActiveSessions: () => {
const uid = authService.getCurrentUserId();
return getBaseRepository().endAllActiveSessions(uid);
},
};
module.exports = sessionRepositoryAdapter;

View File

@ -108,14 +108,15 @@ function getOrCreateActive(uid, requestedType = 'ask') {
}
}
function endAllActiveSessions() {
function endAllActiveSessions(uid) {
const db = sqliteClient.getDb();
const now = Math.floor(Date.now() / 1000);
const query = `UPDATE sessions SET ended_at = ?, updated_at = ? WHERE ended_at IS NULL`;
// Filter by uid to match the Firebase repository's behavior.
const query = `UPDATE sessions SET ended_at = ?, updated_at = ? WHERE ended_at IS NULL AND uid = ?`;
try {
const result = db.prepare(query).run(now, now);
console.log(`[Repo] Ended ${result.changes} active session(s).`);
const result = db.prepare(query).run(now, now, uid);
console.log(`[Repo] Ended ${result.changes} active SQLite session(s) for user ${uid}.`);
return { changes: result.changes };
} catch (err) {
console.error('SQLite: Failed to end all active sessions:', err);

View File

@ -0,0 +1,89 @@
const { getFirestore, doc, getDoc, setDoc, deleteDoc, writeBatch, query, where, getDocs, collection } = require('firebase/firestore');
const { createEncryptedConverter } = require('../firestoreConverter');
const userConverter = createEncryptedConverter(['api_key']);
function usersCol() {
const db = getFirestore();
return collection(db, 'users').withConverter(userConverter);
}
// These functions are mostly correct as they already operate on a top-level collection.
// We just need to ensure the signatures are consistent.
async function findOrCreate(user) {
if (!user || !user.uid) throw new Error('User object and uid are required');
const { uid, displayName, email } = user;
const now = Math.floor(Date.now() / 1000);
const docRef = doc(usersCol(), uid);
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
await setDoc(docRef, {
display_name: displayName || docSnap.data().display_name || 'User',
email: email || docSnap.data().email || 'no-email@example.com'
}, { merge: true });
} else {
await setDoc(docRef, { uid, display_name: displayName || 'User', email: email || 'no-email@example.com', created_at: now });
}
const finalDoc = await getDoc(docRef);
return finalDoc.data();
}
async function getById(uid) {
const docRef = doc(usersCol(), uid);
const docSnap = await getDoc(docRef);
return docSnap.exists() ? docSnap.data() : null;
}
async function saveApiKey(uid, apiKey, provider = 'openai') {
const docRef = doc(usersCol(), uid);
await setDoc(docRef, { api_key: apiKey, provider }, { merge: true });
return { changes: 1 };
}
async function update({ uid, displayName }) {
const docRef = doc(usersCol(), uid);
await setDoc(docRef, { display_name: displayName }, { merge: true });
return { changes: 1 };
}
async function deleteById(uid) {
const db = getFirestore();
const batch = writeBatch(db);
// 1. Delete all sessions owned by the user
const sessionsQuery = query(collection(db, 'sessions'), where('uid', '==', uid));
const sessionsSnapshot = await getDocs(sessionsQuery);
for (const sessionDoc of sessionsSnapshot.docs) {
// Recursively delete sub-collections
const subcollectionsToDelete = ['transcripts', 'ai_messages', 'summary'];
for (const sub of subcollectionsToDelete) {
const subColPath = `sessions/${sessionDoc.id}/${sub}`;
const subSnapshot = await getDocs(query(collection(db, subColPath)));
subSnapshot.forEach(d => batch.delete(d.ref));
}
batch.delete(sessionDoc.ref);
}
// 2. Delete all presets owned by the user
const presetsQuery = query(collection(db, 'prompt_presets'), where('uid', '==', uid));
const presetsSnapshot = await getDocs(presetsQuery);
presetsSnapshot.forEach(doc => batch.delete(doc.ref));
// 3. Delete the user document itself
const userRef = doc(usersCol(), uid);
batch.delete(userRef);
await batch.commit();
return { success: true };
}
module.exports = {
findOrCreate,
getById,
saveApiKey,
update,
deleteById,
};

View File

@ -1,19 +1,40 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository');
const authService = require('../../../common/services/authService');
const firebaseRepository = require('./firebase.repository');
const authService = require('../../services/authService');
function getRepository() {
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
function getBaseRepository() {
const user = authService.getCurrentUser();
if (user && user.isLoggedIn) {
return firebaseRepository;
}
return sqliteRepository;
}
module.exports = {
findOrCreate: (...args) => getRepository().findOrCreate(...args),
getById: (...args) => getRepository().getById(...args),
saveApiKey: (...args) => getRepository().saveApiKey(...args),
update: (...args) => getRepository().update(...args),
deleteById: (...args) => getRepository().deleteById(...args),
const userRepositoryAdapter = {
findOrCreate: (user) => {
// This function receives the full user object, which includes the uid. No need to inject.
return getBaseRepository().findOrCreate(user);
},
getById: () => {
const uid = authService.getCurrentUserId();
return getBaseRepository().getById(uid);
},
saveApiKey: (apiKey, provider) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().saveApiKey(uid, apiKey, provider);
},
update: (updateData) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().update({ uid, ...updateData });
},
deleteById: () => {
const uid = authService.getCurrentUserId();
return getBaseRepository().deleteById(uid);
}
};
module.exports = userRepositoryAdapter;

View File

@ -40,7 +40,7 @@ function getById(uid) {
return db.prepare('SELECT * FROM users WHERE uid = ?').get(uid);
}
function saveApiKey(apiKey, uid, provider = 'openai') {
function saveApiKey(uid, apiKey, provider = 'openai') {
const db = sqliteClient.getDb();
try {
const result = db.prepare('UPDATE users SET api_key = ?, provider = ? WHERE uid = ?').run(apiKey, provider, uid);

View File

@ -1,8 +1,8 @@
const { onAuthStateChanged, signInWithCustomToken, signOut } = require('firebase/auth');
const { BrowserWindow } = require('electron');
const { getFirebaseAuth } = require('./firebaseClient');
const userRepository = require('../repositories/user');
const fetch = require('node-fetch');
const encryptionService = require('./encryptionService');
async function getVirtualKeyByEmail(email, idToken) {
if (!idToken) {
@ -37,6 +37,10 @@ class AuthService {
this.currentUserMode = 'local'; // 'local' or 'firebase'
this.currentUser = null;
this.isInitialized = false;
// Initialize immediately for the default local user on startup.
// This ensures the key is ready before any login/logout state change.
encryptionService.initializeKey(this.currentUserId);
}
initialize() {
@ -53,6 +57,10 @@ class AuthService {
this.currentUserId = user.uid;
this.currentUserMode = 'firebase';
// ** Initialize encryption key for the logged-in user **
await encryptionService.initializeKey(user.uid);
// Start background task to fetch and save virtual key
(async () => {
try {
@ -81,6 +89,9 @@ class AuthService {
this.currentUser = null;
this.currentUserId = 'default_user';
this.currentUserMode = 'local';
// ** Initialize encryption key for the default/local user **
await encryptionService.initializeKey(this.currentUserId);
}
this.broadcastUserState();
});

View File

@ -0,0 +1,140 @@
const crypto = require('crypto');
let keytar;
// Dynamically import keytar, as it's an optional dependency.
try {
keytar = require('keytar');
} catch (error) {
console.warn('[EncryptionService] keytar is not available. Will use in-memory key for this session. Restarting the app might be required for data persistence after login.');
keytar = null;
}
const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
let sessionKey = null; // In-memory fallback key
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16; // For AES, this is always 16
const AUTH_TAG_LENGTH = 16;
/**
* Initializes the encryption key for a given user.
* It first tries to get the key from the OS keychain.
* If that fails, it generates a new key.
* If keytar is available, it saves the new key.
* Otherwise, it uses an in-memory key for the session.
*
* @param {string} userId - The unique identifier for the user (e.g., Firebase UID).
*/
async function initializeKey(userId) {
if (!userId) {
throw new Error('A user ID must be provided to initialize the encryption key.');
}
if (keytar) {
try {
let key = await keytar.getPassword(SERVICE_NAME, userId);
if (!key) {
console.log(`[EncryptionService] No key found for ${userId}. Creating a new one.`);
key = crypto.randomBytes(32).toString('hex');
await keytar.setPassword(SERVICE_NAME, userId, key);
console.log(`[EncryptionService] New key securely stored in keychain for ${userId}.`);
} else {
console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
}
sessionKey = key;
} catch (error) {
console.error('[EncryptionService] keytar failed. Falling back to in-memory key for this session.', error);
keytar = null; // Disable keytar for the rest of the session to avoid repeated errors
sessionKey = crypto.randomBytes(32).toString('hex');
}
} else {
// keytar is not available
if (!sessionKey) {
console.warn('[EncryptionService] Using in-memory session key. Data will not persist across restarts without keytar.');
sessionKey = crypto.randomBytes(32).toString('hex');
}
}
if (!sessionKey) {
throw new Error('Failed to initialize encryption key.');
}
}
/**
* Encrypts a given text using AES-256-GCM.
* @param {string} text The text to encrypt.
* @returns {string | null} The encrypted data, as a base64 string containing iv, authTag, and content, or the original value if it cannot be encrypted.
*/
function encrypt(text) {
if (!sessionKey) {
console.error('[EncryptionService] Encryption key is not initialized. Cannot encrypt.');
return text; // Return original if key is missing
}
if (text == null) { // checks for null or undefined
return text;
}
try {
const key = Buffer.from(sessionKey, 'hex');
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(String(text), 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
// Prepend IV and AuthTag to the encrypted content, then encode as base64.
return Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]).toString('base64');
} catch (error) {
console.error('[EncryptionService] Encryption failed:', error);
return text; // Return original on error
}
}
/**
* Decrypts a given encrypted string.
* @param {string} encryptedText The base64 encrypted text.
* @returns {string | null} The decrypted text, or the original value if it cannot be decrypted.
*/
function decrypt(encryptedText) {
if (!sessionKey) {
console.error('[EncryptionService] Encryption key is not initialized. Cannot decrypt.');
return encryptedText; // Return original if key is missing
}
if (encryptedText == null || typeof encryptedText !== 'string') {
return encryptedText;
}
try {
const data = Buffer.from(encryptedText, 'base64');
if (data.length < IV_LENGTH + AUTH_TAG_LENGTH) {
// This is not a valid encrypted string, likely plain text.
return encryptedText;
}
const key = Buffer.from(sessionKey, 'hex');
const iv = data.slice(0, IV_LENGTH);
const authTag = data.slice(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
const encryptedContent = data.slice(IV_LENGTH + AUTH_TAG_LENGTH);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedContent, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
} catch (error) {
// It's common for this to fail if the data is not encrypted (e.g., legacy data).
// In that case, we return the original value.
return encryptedText;
}
}
module.exports = {
initializeKey,
encrypt,
decrypt,
};

View File

@ -1,6 +1,9 @@
const { initializeApp } = require('firebase/app');
const { initializeAuth } = require('firebase/auth');
const Store = require('electron-store');
const { setLogLevel } = require('firebase/firestore');
setLogLevel('debug');
/**
* Firebase Auth expects the `persistence` option passed to `initializeAuth()` to be *classes*,

View File

@ -101,9 +101,8 @@ async function sendMessage(userPrompt) {
// Save to DB
try {
const uid = authService.getCurrentUserId();
if (!uid) throw new Error("User not logged in, cannot save message.");
const sessionId = await sessionRepository.getOrCreateActive(uid, 'ask');
// The repository adapter will now handle the UID internally.
const sessionId = await sessionRepository.getOrCreateActive('ask');
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
console.log(`[AskService] DB: Saved ask/answer pair to session ${sessionId}`);

View File

@ -0,0 +1,37 @@
const { getFirestore, collection, addDoc, query, getDocs, orderBy } = require('firebase/firestore');
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
const aiMessageConverter = createEncryptedConverter(['content']);
function aiMessagesCol(sessionId) {
if (!sessionId) throw new Error("Session ID is required to access AI messages.");
const db = getFirestore();
return collection(db, `sessions/${sessionId}/ai_messages`).withConverter(aiMessageConverter);
}
async function addAiMessage({ uid, sessionId, role, content, model = 'unknown' }) {
const now = Math.floor(Date.now() / 1000);
const newMessage = {
uid, // To identify the author of the message
session_id: sessionId,
sent_at: now,
role,
content,
model,
created_at: now,
};
const docRef = await addDoc(aiMessagesCol(sessionId), newMessage);
return { id: docRef.id };
}
async function getAllAiMessagesBySessionId(sessionId) {
const q = query(aiMessagesCol(sessionId), orderBy('sent_at', 'asc'));
const querySnapshot = await getDocs(q);
return querySnapshot.docs.map(doc => doc.data());
}
module.exports = {
addAiMessage,
getAllAiMessagesBySessionId,
};

View File

@ -1,18 +1,25 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository'); // Future implementation
const firebaseRepository = require('./firebase.repository');
const authService = require('../../../common/services/authService');
function getRepository() {
// In the future, we can check the user's login status from authService
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
function getBaseRepository() {
const user = authService.getCurrentUser();
if (user && user.isLoggedIn) {
return firebaseRepository;
}
return sqliteRepository;
}
// Directly export functions for ease of use, decided by the strategy
module.exports = {
addAiMessage: (...args) => getRepository().addAiMessage(...args),
getAllAiMessagesBySessionId: (...args) => getRepository().getAllAiMessagesBySessionId(...args),
// The adapter layer that injects the UID
const askRepositoryAdapter = {
addAiMessage: ({ sessionId, role, content, model }) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().addAiMessage({ uid, sessionId, role, content, model });
},
getAllAiMessagesBySessionId: (sessionId) => {
// This function does not require a UID at the service level.
return getBaseRepository().getAllAiMessagesBySessionId(sessionId);
}
};
module.exports = askRepositoryAdapter;

View File

@ -1,6 +1,7 @@
const sqliteClient = require('../../../common/services/sqliteClient');
function addAiMessage({ sessionId, role, content, model = 'gpt-4.1' }) {
function addAiMessage({ uid, sessionId, role, content, model = 'unknown' }) {
// uid is ignored in the SQLite implementation
const db = sqliteClient.getDb();
const messageId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);

View File

@ -77,12 +77,15 @@ class ListenService {
async initializeNewSession() {
try {
const uid = authService.getCurrentUserId();
if (!uid) {
throw new Error("Cannot initialize session: user not logged in.");
// The UID is no longer passed to the repository method directly.
// The adapter layer handles UID injection. We just ensure a user is available.
const user = authService.getCurrentUser();
if (!user) {
// This case should ideally not happen as authService initializes a default user.
throw new Error("Cannot initialize session: auth service not ready.");
}
this.currentSessionId = await sessionRepository.getOrCreateActive(uid, 'listen');
this.currentSessionId = await sessionRepository.getOrCreateActive('listen');
console.log(`[DB] New listen session ensured: ${this.currentSessionId}`);
// Set session ID for summary service

View File

@ -0,0 +1,35 @@
const { getFirestore, collection, addDoc, query, getDocs, orderBy } = require('firebase/firestore');
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
const transcriptConverter = createEncryptedConverter(['text']);
function transcriptsCol(sessionId) {
if (!sessionId) throw new Error("Session ID is required to access transcripts.");
const db = getFirestore();
return collection(db, `sessions/${sessionId}/transcripts`).withConverter(transcriptConverter);
}
async function addTranscript({ uid, sessionId, speaker, text }) {
const now = Math.floor(Date.now() / 1000);
const newTranscript = {
uid, // To identify the author/source of the transcript
session_id: sessionId,
start_at: now,
speaker,
text,
created_at: now,
};
const docRef = await addDoc(transcriptsCol(sessionId), newTranscript);
return { id: docRef.id };
}
async function getAllTranscriptsBySessionId(sessionId) {
const q = query(transcriptsCol(sessionId), orderBy('start_at', 'asc'));
const querySnapshot = await getDocs(q);
return querySnapshot.docs.map(doc => doc.data());
}
module.exports = {
addTranscript,
getAllTranscriptsBySessionId,
};

View File

@ -1,5 +1,23 @@
const sttRepository = require('./sqlite.repository');
const sqliteRepository = require('./sqlite.repository');
const firebaseRepository = require('./firebase.repository');
const authService = require('../../../../common/services/authService');
module.exports = {
...sttRepository,
function getBaseRepository() {
const user = authService.getCurrentUser();
if (user && user.isLoggedIn) {
return firebaseRepository;
}
return sqliteRepository;
}
const sttRepositoryAdapter = {
addTranscript: ({ sessionId, speaker, text }) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().addTranscript({ uid, sessionId, speaker, text });
},
getAllTranscriptsBySessionId: (sessionId) => {
return getBaseRepository().getAllTranscriptsBySessionId(sessionId);
}
};
module.exports = sttRepositoryAdapter;

View File

@ -1,6 +1,7 @@
const sqliteClient = require('../../../../common/services/sqliteClient');
function addTranscript({ sessionId, speaker, text }) {
function addTranscript({ uid, sessionId, speaker, text }) {
// uid is ignored in the SQLite implementation
const db = sqliteClient.getDb();
const transcriptId = require('crypto').randomUUID();
const now = Math.floor(Date.now() / 1000);

View File

@ -0,0 +1,42 @@
const { getFirestore, collection, doc, setDoc, getDoc } = require('firebase/firestore');
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
const fieldsToEncrypt = ['tldr', 'text', 'bullet_json', 'action_json'];
const summaryConverter = createEncryptedConverter(fieldsToEncrypt);
function summaryDocRef(sessionId) {
if (!sessionId) throw new Error("Session ID is required to access summary.");
const db = getFirestore();
const path = `sessions/${sessionId}/summary`;
return doc(collection(db, path).withConverter(summaryConverter), 'data');
}
async function saveSummary({ uid, sessionId, tldr, text, bullet_json, action_json, model = 'unknown' }) {
const now = Math.floor(Date.now() / 1000);
const summaryData = {
uid, // To know who generated the summary
generated_at: now,
model,
text,
tldr,
bullet_json,
action_json,
updated_at: now,
};
const docRef = summaryDocRef(sessionId);
await setDoc(docRef, summaryData, { merge: true });
return { changes: 1 };
}
async function getSummaryBySessionId(sessionId) {
const docRef = summaryDocRef(sessionId);
const docSnap = await getDoc(docRef);
return docSnap.exists() ? docSnap.data() : null;
}
module.exports = {
saveSummary,
getSummaryBySessionId,
};

View File

@ -1,5 +1,23 @@
const summaryRepository = require('./sqlite.repository');
const sqliteRepository = require('./sqlite.repository');
const firebaseRepository = require('./firebase.repository');
const authService = require('../../../../common/services/authService');
module.exports = {
...summaryRepository,
function getBaseRepository() {
const user = authService.getCurrentUser();
if (user && user.isLoggedIn) {
return firebaseRepository;
}
return sqliteRepository;
}
const summaryRepositoryAdapter = {
saveSummary: ({ sessionId, tldr, text, bullet_json, action_json, model }) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().saveSummary({ uid, sessionId, tldr, text, bullet_json, action_json, model });
},
getSummaryBySessionId: (sessionId) => {
return getBaseRepository().getSummaryBySessionId(sessionId);
}
};
module.exports = summaryRepositoryAdapter;

View File

@ -1,6 +1,7 @@
const sqliteClient = require('../../../../common/services/sqliteClient');
function saveSummary({ sessionId, tldr, text, bullet_json, action_json, model = 'gpt-4.1' }) {
function saveSummary({ uid, sessionId, tldr, text, bullet_json, action_json, model = 'unknown' }) {
// uid is ignored in the SQLite implementation
return new Promise((resolve, reject) => {
try {
const db = sqliteClient.getDb();

View File

@ -0,0 +1,94 @@
const { getFirestore, collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy } = require('firebase/firestore');
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
const userPresetConverter = createEncryptedConverter(['prompt']);
const defaultPresetConverter = {
toFirestore: (data) => data,
fromFirestore: (snapshot, options) => {
const data = snapshot.data(options);
return { ...data, id: snapshot.id };
}
};
function userPresetsCol() {
const db = getFirestore();
return collection(db, 'prompt_presets').withConverter(userPresetConverter);
}
function defaultPresetsCol() {
const db = getFirestore();
return collection(db, 'defaults/prompt_presets').withConverter(defaultPresetConverter);
}
async function getPresets(uid) {
const userPresetsQuery = query(userPresetsCol(), where('uid', '==', uid));
const defaultPresetsQuery = query(defaultPresetsCol());
const [userSnapshot, defaultSnapshot] = await Promise.all([
getDocs(userPresetsQuery),
getDocs(defaultPresetsQuery)
]);
const presets = [
...defaultSnapshot.docs.map(d => d.data()),
...userSnapshot.docs.map(d => d.data())
];
return presets.sort((a, b) => {
if (a.is_default && !b.is_default) return -1;
if (!a.is_default && b.is_default) return 1;
return a.title.localeCompare(b.title);
});
}
async function getPresetTemplates() {
const q = query(defaultPresetsCol(), orderBy('title', 'asc'));
const snapshot = await getDocs(q);
return snapshot.docs.map(doc => doc.data());
}
async function createPreset({ uid, title, prompt }) {
const now = Math.floor(Date.now() / 1000);
const newPreset = {
uid: uid,
title,
prompt,
is_default: false,
created_at: now,
};
const docRef = await addDoc(userPresetsCol(), newPreset);
return { id: docRef.id };
}
async function updatePreset(id, { title, prompt }, uid) {
const docRef = doc(userPresetsCol(), id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
throw new Error("Preset not found or permission denied to update.");
}
await updateDoc(docRef, { title, prompt });
return { changes: 1 };
}
async function deletePreset(id, uid) {
const docRef = doc(userPresetsCol(), id);
const docSnap = await getDoc(docRef);
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
throw new Error("Preset not found or permission denied to delete.");
}
await deleteDoc(docRef);
return { changes: 1 };
}
module.exports = {
getPresets,
getPresetTemplates,
createPreset,
updatePreset,
deletePreset,
};

View File

@ -1,21 +1,39 @@
const sqliteRepository = require('./sqlite.repository');
// const firebaseRepository = require('./firebase.repository'); // Future implementation
const firebaseRepository = require('./firebase.repository');
const authService = require('../../../common/services/authService');
function getRepository() {
// In the future, we can check the user's login status from authService
// const user = authService.getCurrentUser();
// if (user.isLoggedIn) {
// return firebaseRepository;
// }
function getBaseRepository() {
const user = authService.getCurrentUser();
if (user && user.isLoggedIn) {
return firebaseRepository;
}
return sqliteRepository;
}
// Directly export functions for ease of use, decided by the strategy
module.exports = {
getPresets: (...args) => getRepository().getPresets(...args),
getPresetTemplates: (...args) => getRepository().getPresetTemplates(...args),
createPreset: (...args) => getRepository().createPreset(...args),
updatePreset: (...args) => getRepository().updatePreset(...args),
deletePreset: (...args) => getRepository().deletePreset(...args),
const settingsRepositoryAdapter = {
getPresets: () => {
const uid = authService.getCurrentUserId();
return getBaseRepository().getPresets(uid);
},
getPresetTemplates: () => {
return getBaseRepository().getPresetTemplates();
},
createPreset: (options) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().createPreset({ uid, ...options });
},
updatePreset: (id, options) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().updatePreset(id, options, uid);
},
deletePreset: (id) => {
const uid = authService.getCurrentUserId();
return getBaseRepository().deletePreset(id, uid);
},
};
module.exports = settingsRepositoryAdapter;

View File

@ -208,13 +208,8 @@ async function saveSettings(settings) {
async function getPresets() {
try {
const uid = authService.getCurrentUserId();
if (!uid) {
// Logged out users only see default presets
return await settingsRepository.getPresetTemplates();
}
const presets = await settingsRepository.getPresets(uid);
// The adapter now handles which presets to return based on login state.
const presets = await settingsRepository.getPresets();
return presets;
} catch (error) {
console.error('[SettingsService] Error getting presets:', error);
@ -234,12 +229,8 @@ async function getPresetTemplates() {
async function createPreset(title, prompt) {
try {
const uid = authService.getCurrentUserId();
if (!uid) {
throw new Error("User not logged in, cannot create preset.");
}
const result = await settingsRepository.createPreset({ uid, title, prompt });
// The adapter injects the UID.
const result = await settingsRepository.createPreset({ title, prompt });
windowNotificationManager.notifyRelevantWindows('presets-updated', {
action: 'created',
@ -256,12 +247,8 @@ async function createPreset(title, prompt) {
async function updatePreset(id, title, prompt) {
try {
const uid = authService.getCurrentUserId();
if (!uid) {
throw new Error("User not logged in, cannot update preset.");
}
await settingsRepository.updatePreset(id, { title, prompt }, uid);
// The adapter injects the UID.
await settingsRepository.updatePreset(id, { title, prompt });
windowNotificationManager.notifyRelevantWindows('presets-updated', {
action: 'updated',
@ -278,12 +265,8 @@ async function updatePreset(id, title, prompt) {
async function deletePreset(id) {
try {
const uid = authService.getCurrentUserId();
if (!uid) {
throw new Error("User not logged in, cannot delete preset.");
}
await settingsRepository.deletePreset(id, uid);
// The adapter injects the UID.
await settingsRepository.deletePreset(id);
windowNotificationManager.notifyRelevantWindows('presets-updated', {
action: 'deleted',
@ -299,10 +282,9 @@ async function deletePreset(id) {
async function saveApiKey(apiKey, provider = 'openai') {
try {
const uid = authService.getCurrentUserId();
if (!uid) {
const user = authService.getCurrentUser();
if (!user.isLoggedIn) {
// For non-logged-in users, save to local storage
const { app } = require('electron');
const Store = require('electron-store');
const store = new Store();
store.set('apiKey', apiKey);
@ -318,8 +300,8 @@ async function saveApiKey(apiKey, provider = 'openai') {
return { success: true };
}
// For logged-in users, save to database
await userRepository.saveApiKey(apiKey, uid, provider);
// For logged-in users, use the repository adapter which injects the UID.
await userRepository.saveApiKey(apiKey, provider);
// Notify windows
BrowserWindow.getAllWindows().forEach(win => {
@ -337,17 +319,16 @@ async function saveApiKey(apiKey, provider = 'openai') {
async function removeApiKey() {
try {
const uid = authService.getCurrentUserId();
if (!uid) {
const user = authService.getCurrentUser();
if (!user.isLoggedIn) {
// For non-logged-in users, remove from local storage
const { app } = require('electron');
const Store = require('electron-store');
const store = new Store();
store.delete('apiKey');
store.delete('provider');
} else {
// For logged-in users, remove from database
await userRepository.saveApiKey(null, uid, null);
// For logged-in users, use the repository adapter.
await userRepository.saveApiKey(null, null);
}
// Notify windows

View File

@ -251,7 +251,9 @@ function setupGeneralIpcHandlers() {
ipcMain.handle('save-api-key', (event, apiKey) => {
try {
userRepository.saveApiKey(apiKey, authService.getCurrentUserId());
// The adapter injects the UID and handles local/firebase logic.
// Assuming a default provider if not specified.
userRepository.saveApiKey(apiKey, 'openai');
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('api-key-updated');
});
@ -263,7 +265,8 @@ function setupGeneralIpcHandlers() {
});
ipcMain.handle('get-user-presets', () => {
return presetRepository.getPresets(authService.getCurrentUserId());
// The adapter injects the UID.
return presetRepository.getPresets();
});
ipcMain.handle('get-preset-templates', () => {
@ -302,89 +305,112 @@ function setupWebDataHandlers() {
const userRepository = require('./common/repositories/user');
const presetRepository = require('./common/repositories/preset');
const handleRequest = (channel, responseChannel, payload) => {
const handleRequest = async (channel, responseChannel, payload) => {
let result;
const currentUserId = authService.getCurrentUserId();
// const currentUserId = authService.getCurrentUserId(); // No longer needed here
try {
switch (channel) {
// SESSION
case 'get-sessions':
result = sessionRepository.getAllByUserId(currentUserId);
// Adapter injects UID
result = await sessionRepository.getAllByUserId();
break;
case 'get-session-details':
const session = sessionRepository.getById(payload);
const session = await sessionRepository.getById(payload);
if (!session) {
result = null;
break;
}
const transcripts = sttRepository.getAllTranscriptsBySessionId(payload);
const ai_messages = askRepository.getAllAiMessagesBySessionId(payload);
const summary = summaryRepository.getSummaryBySessionId(payload);
const [transcripts, ai_messages, summary] = await Promise.all([
sttRepository.getAllTranscriptsBySessionId(payload),
askRepository.getAllAiMessagesBySessionId(payload),
summaryRepository.getSummaryBySessionId(payload)
]);
result = { session, transcripts, ai_messages, summary };
break;
case 'delete-session':
result = sessionRepository.deleteWithRelatedData(payload);
result = await sessionRepository.deleteWithRelatedData(payload);
break;
case 'create-session':
const id = sessionRepository.create(currentUserId, 'ask');
if (payload.title) {
sessionRepository.updateTitle(id, payload.title);
// Adapter injects UID
const id = await sessionRepository.create('ask');
if (payload && payload.title) {
await sessionRepository.updateTitle(id, payload.title);
}
result = { id };
break;
// USER
case 'get-user-profile':
result = userRepository.getById(currentUserId);
// Adapter injects UID
result = await userRepository.getById();
break;
case 'update-user-profile':
result = userRepository.update({ uid: currentUserId, ...payload });
// Adapter injects UID
result = await userRepository.update(payload);
break;
case 'find-or-create-user':
result = userRepository.findOrCreate(payload);
result = await userRepository.findOrCreate(payload);
break;
case 'save-api-key':
result = userRepository.saveApiKey(payload, currentUserId);
// Assuming payload is { apiKey, provider }
result = await userRepository.saveApiKey(payload.apiKey, payload.provider);
break;
case 'check-api-key-status':
const user = userRepository.getById(currentUserId);
// Adapter injects UID
const user = await userRepository.getById();
result = { hasApiKey: !!user?.api_key && user.api_key.length > 0 };
break;
case 'delete-account':
result = userRepository.deleteById(currentUserId);
// Adapter injects UID
result = await userRepository.deleteById();
break;
// PRESET
case 'get-presets':
result = presetRepository.getPresets(currentUserId);
// Adapter injects UID
result = await presetRepository.getPresets();
break;
case 'create-preset':
result = presetRepository.create({ ...payload, uid: currentUserId });
// Adapter injects UID
result = await presetRepository.create(payload);
settingsService.notifyPresetUpdate('created', result.id, payload.title);
break;
case 'update-preset':
result = presetRepository.update(payload.id, payload.data, currentUserId);
// Adapter injects UID
result = await presetRepository.update(payload.id, payload.data);
settingsService.notifyPresetUpdate('updated', payload.id, payload.data.title);
break;
case 'delete-preset':
result = presetRepository.delete(payload, currentUserId);
// Adapter injects UID
result = await presetRepository.delete(payload);
settingsService.notifyPresetUpdate('deleted', payload);
break;
// BATCH
case 'get-batch-data':
const includes = payload ? payload.split(',').map(item => item.trim()) : ['profile', 'presets', 'sessions'];
const batchResult = {};
const promises = {};
if (includes.includes('profile')) {
batchResult.profile = userRepository.getById(currentUserId);
// Adapter injects UID
promises.profile = userRepository.getById();
}
if (includes.includes('presets')) {
batchResult.presets = presetRepository.getPresets(currentUserId);
// Adapter injects UID
promises.presets = presetRepository.getPresets();
}
if (includes.includes('sessions')) {
batchResult.sessions = sessionRepository.getAllByUserId(currentUserId);
// Adapter injects UID
promises.sessions = sessionRepository.getAllByUserId();
}
const batchResult = {};
const promiseResults = await Promise.all(Object.values(promises));
Object.keys(promises).forEach((key, index) => {
batchResult[key] = promiseResults[index];
});
result = batchResult;
break;