firebase WIP
This commit is contained in:
parent
9977387fbc
commit
fae6962297
51
PLAN.md
Normal file
51
PLAN.md
Normal 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
18
package-lock.json
generated
@ -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",
|
||||
|
@ -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
77
repository_api_report.md
Normal 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)`
|
45
src/common/repositories/firestoreConverter.js
Normal file
45
src/common/repositories/firestoreConverter.js
Normal 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,
|
||||
};
|
94
src/common/repositories/preset/firebase.repository.js
Normal file
94
src/common/repositories/preset/firebase.repository.js
Normal 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,
|
||||
};
|
@ -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;
|
155
src/common/repositories/session/firebase.repository.js
Normal file
155
src/common/repositories/session/firebase.repository.js
Normal 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,
|
||||
};
|
@ -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;
|
@ -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);
|
||||
|
89
src/common/repositories/user/firebase.repository.js
Normal file
89
src/common/repositories/user/firebase.repository.js
Normal 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,
|
||||
};
|
@ -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;
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
|
140
src/common/services/encryptionService.js
Normal file
140
src/common/services/encryptionService.js
Normal 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,
|
||||
};
|
@ -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*,
|
||||
|
@ -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}`);
|
||||
|
37
src/features/ask/repositories/firebase.repository.js
Normal file
37
src/features/ask/repositories/firebase.repository.js
Normal 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,
|
||||
};
|
@ -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;
|
@ -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);
|
||||
|
@ -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
|
||||
|
35
src/features/listen/stt/repositories/firebase.repository.js
Normal file
35
src/features/listen/stt/repositories/firebase.repository.js
Normal 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,
|
||||
};
|
@ -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;
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
@ -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;
|
@ -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();
|
||||
|
94
src/features/settings/repositories/firebase.repository.js
Normal file
94
src/features/settings/repositories/firebase.repository.js
Normal 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,
|
||||
};
|
@ -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;
|
@ -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
|
||||
|
80
src/index.js
80
src/index.js
@ -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;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user