firestore + full end-to-end encryption
This commit is contained in:
parent
bf344268e7
commit
c497234230
51
PLAN.md
51
PLAN.md
@ -1,51 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,77 +0,0 @@
|
|||||||
# 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)`
|
|
@ -7,7 +7,8 @@ const LATEST_SCHEMA = {
|
|||||||
{ name: 'created_at', type: 'INTEGER' },
|
{ name: 'created_at', type: 'INTEGER' },
|
||||||
{ name: 'api_key', type: 'TEXT' },
|
{ name: 'api_key', type: 'TEXT' },
|
||||||
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' },
|
{ name: 'provider', type: 'TEXT DEFAULT \'openai\'' },
|
||||||
{ name: 'auto_update_enabled', type: 'INTEGER DEFAULT 1' }
|
{ name: 'auto_update_enabled', type: 'INTEGER DEFAULT 1' },
|
||||||
|
{ name: 'has_migrated_to_firebase', type: 'INTEGER DEFAULT 0' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
sessions: {
|
sessions: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const encryptionService = require('../services/encryptionService');
|
const encryptionService = require('../services/encryptionService');
|
||||||
|
const { Timestamp } = require('firebase/firestore');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Firestore converter that automatically encrypts and decrypts specified fields.
|
* Creates a Firestore converter that automatically encrypts and decrypts specified fields.
|
||||||
@ -19,7 +20,7 @@ function createEncryptedConverter(fieldsToEncrypt = []) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Ensure there's a timestamp for the last modification
|
// Ensure there's a timestamp for the last modification
|
||||||
firestoreData.updated_at = Math.floor(Date.now() / 1000);
|
firestoreData.updated_at = Timestamp.now();
|
||||||
return firestoreData;
|
return firestoreData;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -35,6 +36,14 @@ function createEncryptedConverter(fieldsToEncrypt = []) {
|
|||||||
appObject[field] = encryptionService.decrypt(appObject[field]);
|
appObject[field] = encryptionService.decrypt(appObject[field]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert Firestore Timestamps back to Unix timestamps (seconds) for app-wide consistency
|
||||||
|
for (const key in appObject) {
|
||||||
|
if (appObject[key] instanceof Timestamp) {
|
||||||
|
appObject[key] = appObject[key].seconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return appObject;
|
return appObject;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
const { getFirestore, collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy } = require('firebase/firestore');
|
const { collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy, Timestamp } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../../services/firebaseClient');
|
||||||
const { createEncryptedConverter } = require('../firestoreConverter');
|
const { createEncryptedConverter } = require('../firestoreConverter');
|
||||||
|
const encryptionService = require('../../services/encryptionService');
|
||||||
|
|
||||||
const userPresetConverter = createEncryptedConverter(['prompt', 'title']);
|
const userPresetConverter = createEncryptedConverter(['prompt', 'title']);
|
||||||
|
|
||||||
@ -12,13 +14,14 @@ const defaultPresetConverter = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function userPresetsCol() {
|
function userPresetsCol() {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, 'prompt_presets').withConverter(userPresetConverter);
|
return collection(db, 'prompt_presets').withConverter(userPresetConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultPresetsCol() {
|
function defaultPresetsCol() {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, 'defaults/prompt_presets').withConverter(defaultPresetConverter);
|
// Path must have an odd number of segments. 'v1' is a placeholder document.
|
||||||
|
return collection(db, 'defaults/v1/prompt_presets').withConverter(defaultPresetConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPresets(uid) {
|
async function getPresets(uid) {
|
||||||
@ -49,12 +52,12 @@ async function getPresetTemplates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create({ uid, title, prompt }) {
|
async function create({ uid, title, prompt }) {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Timestamp.now();
|
||||||
const newPreset = {
|
const newPreset = {
|
||||||
uid: uid,
|
uid: uid,
|
||||||
title,
|
title,
|
||||||
prompt,
|
prompt,
|
||||||
is_default: false,
|
is_default: 0,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
};
|
};
|
||||||
const docRef = await addDoc(userPresetsCol(), newPreset);
|
const docRef = await addDoc(userPresetsCol(), newPreset);
|
||||||
@ -68,8 +71,18 @@ async function update(id, { title, prompt }, uid) {
|
|||||||
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
|
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
|
||||||
throw new Error("Preset not found or permission denied to update.");
|
throw new Error("Preset not found or permission denied to update.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateDoc(docRef, { title, prompt });
|
// Encrypt sensitive fields before sending to Firestore because `updateDoc` bypasses converters.
|
||||||
|
const updates = {};
|
||||||
|
if (title !== undefined) {
|
||||||
|
updates.title = encryptionService.encrypt(title);
|
||||||
|
}
|
||||||
|
if (prompt !== undefined) {
|
||||||
|
updates.prompt = encryptionService.encrypt(prompt);
|
||||||
|
}
|
||||||
|
updates.updated_at = Timestamp.now();
|
||||||
|
|
||||||
|
await updateDoc(docRef, updates);
|
||||||
return { changes: 1 };
|
return { changes: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
const { getFirestore, doc, getDoc, collection, addDoc, query, where, getDocs, writeBatch, orderBy, limit, runTransaction, updateDoc } = require('firebase/firestore');
|
const { doc, getDoc, collection, addDoc, query, where, getDocs, writeBatch, orderBy, limit, updateDoc, Timestamp } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../../services/firebaseClient');
|
||||||
const { createEncryptedConverter } = require('../firestoreConverter');
|
const { createEncryptedConverter } = require('../firestoreConverter');
|
||||||
|
const encryptionService = require('../../services/encryptionService');
|
||||||
|
|
||||||
const sessionConverter = createEncryptedConverter(['title']);
|
const sessionConverter = createEncryptedConverter(['title']);
|
||||||
|
|
||||||
function sessionsCol() {
|
function sessionsCol() {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, 'sessions').withConverter(sessionConverter);
|
return collection(db, 'sessions').withConverter(sessionConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sub-collection references are now built from the top-level
|
// Sub-collection references are now built from the top-level
|
||||||
function subCollections(sessionId) {
|
function subCollections(sessionId) {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
const sessionPath = `sessions/${sessionId}`;
|
const sessionPath = `sessions/${sessionId}`;
|
||||||
return {
|
return {
|
||||||
transcripts: collection(db, `${sessionPath}/transcripts`),
|
transcripts: collection(db, `${sessionPath}/transcripts`),
|
||||||
@ -26,7 +28,7 @@ async function getById(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function create(uid, type = 'ask') {
|
async function create(uid, type = 'ask') {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Timestamp.now();
|
||||||
const newSession = {
|
const newSession = {
|
||||||
uid: uid,
|
uid: uid,
|
||||||
members: [uid], // For future sharing functionality
|
members: [uid], // For future sharing functionality
|
||||||
@ -49,12 +51,15 @@ async function getAllByUserId(uid) {
|
|||||||
|
|
||||||
async function updateTitle(id, title) {
|
async function updateTitle(id, title) {
|
||||||
const docRef = doc(sessionsCol(), id);
|
const docRef = doc(sessionsCol(), id);
|
||||||
await updateDoc(docRef, { title });
|
await updateDoc(docRef, {
|
||||||
|
title: encryptionService.encrypt(title),
|
||||||
|
updated_at: Timestamp.now()
|
||||||
|
});
|
||||||
return { changes: 1 };
|
return { changes: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteWithRelatedData(id) {
|
async function deleteWithRelatedData(id) {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
const batch = writeBatch(db);
|
const batch = writeBatch(db);
|
||||||
|
|
||||||
const { transcripts, ai_messages, summary } = subCollections(id);
|
const { transcripts, ai_messages, summary } = subCollections(id);
|
||||||
@ -77,7 +82,7 @@ async function deleteWithRelatedData(id) {
|
|||||||
|
|
||||||
async function end(id) {
|
async function end(id) {
|
||||||
const docRef = doc(sessionsCol(), id);
|
const docRef = doc(sessionsCol(), id);
|
||||||
await updateDoc(docRef, { ended_at: Math.floor(Date.now() / 1000) });
|
await updateDoc(docRef, { ended_at: Timestamp.now() });
|
||||||
return { changes: 1 };
|
return { changes: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +94,7 @@ async function updateType(id, type) {
|
|||||||
|
|
||||||
async function touch(id) {
|
async function touch(id) {
|
||||||
const docRef = doc(sessionsCol(), id);
|
const docRef = doc(sessionsCol(), id);
|
||||||
await updateDoc(docRef, { updated_at: Math.floor(Date.now() / 1000) });
|
await updateDoc(docRef, { updated_at: Timestamp.now() });
|
||||||
return { changes: 1 };
|
return { changes: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,7 +116,7 @@ async function getOrCreateActive(uid, requestedType = 'ask') {
|
|||||||
|
|
||||||
console.log(`[Repo] Found active Firebase session ${activeSession.id}`);
|
console.log(`[Repo] Found active Firebase session ${activeSession.id}`);
|
||||||
|
|
||||||
const updates = { updated_at: Math.floor(Date.now() / 1000) };
|
const updates = { updated_at: Timestamp.now() };
|
||||||
if (activeSession.session_type === 'ask' && requestedType === 'listen') {
|
if (activeSession.session_type === 'ask' && requestedType === 'listen') {
|
||||||
updates.session_type = 'listen';
|
updates.session_type = 'listen';
|
||||||
console.log(`[Repo] Promoted Firebase session ${activeSession.id} to 'listen' type.`);
|
console.log(`[Repo] Promoted Firebase session ${activeSession.id} to 'listen' type.`);
|
||||||
@ -131,9 +136,10 @@ async function endAllActiveSessions(uid) {
|
|||||||
|
|
||||||
if (snapshot.empty) return { changes: 0 };
|
if (snapshot.empty) return { changes: 0 };
|
||||||
|
|
||||||
const batch = writeBatch(getFirestore());
|
const batch = writeBatch(getFirestoreInstance());
|
||||||
|
const now = Timestamp.now();
|
||||||
snapshot.forEach(d => {
|
snapshot.forEach(d => {
|
||||||
batch.update(d.ref, { ended_at: Math.floor(Date.now() / 1000) });
|
batch.update(d.ref, { ended_at: now });
|
||||||
});
|
});
|
||||||
await batch.commit();
|
await batch.commit();
|
||||||
|
|
||||||
|
@ -1,8 +1,18 @@
|
|||||||
const sqliteRepository = require('./sqlite.repository');
|
const sqliteRepository = require('./sqlite.repository');
|
||||||
const firebaseRepository = require('./firebase.repository');
|
const firebaseRepository = require('./firebase.repository');
|
||||||
const authService = require('../../../common/services/authService');
|
|
||||||
|
let authService = null;
|
||||||
|
|
||||||
|
function setAuthService(service) {
|
||||||
|
authService = service;
|
||||||
|
}
|
||||||
|
|
||||||
function getBaseRepository() {
|
function getBaseRepository() {
|
||||||
|
if (!authService) {
|
||||||
|
// Fallback or error if authService is not set, to prevent crashes.
|
||||||
|
// During initial load, it might not be set, so we default to sqlite.
|
||||||
|
return sqliteRepository;
|
||||||
|
}
|
||||||
const user = authService.getCurrentUser();
|
const user = authService.getCurrentUser();
|
||||||
if (user && user.isLoggedIn) {
|
if (user && user.isLoggedIn) {
|
||||||
return firebaseRepository;
|
return firebaseRepository;
|
||||||
@ -12,6 +22,8 @@ function getBaseRepository() {
|
|||||||
|
|
||||||
// The adapter layer that injects the UID
|
// The adapter layer that injects the UID
|
||||||
const sessionRepositoryAdapter = {
|
const sessionRepositoryAdapter = {
|
||||||
|
setAuthService, // Expose the setter
|
||||||
|
|
||||||
getById: (id) => getBaseRepository().getById(id),
|
getById: (id) => getBaseRepository().getById(id),
|
||||||
|
|
||||||
create: (type = 'ask') => {
|
create: (type = 'ask') => {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
const { getFirestore, doc, getDoc, setDoc, deleteDoc, writeBatch, query, where, getDocs, collection } = require('firebase/firestore');
|
const { doc, getDoc, setDoc, deleteDoc, writeBatch, query, where, getDocs, collection, Timestamp } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../../services/firebaseClient');
|
||||||
const { createEncryptedConverter } = require('../firestoreConverter');
|
const { createEncryptedConverter } = require('../firestoreConverter');
|
||||||
|
const encryptionService = require('../../services/encryptionService');
|
||||||
|
|
||||||
const userConverter = createEncryptedConverter(['api_key']);
|
const userConverter = createEncryptedConverter(['api_key']);
|
||||||
|
|
||||||
function usersCol() {
|
function usersCol() {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, 'users').withConverter(userConverter);
|
return collection(db, 'users').withConverter(userConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,7 +16,7 @@ function usersCol() {
|
|||||||
async function findOrCreate(user) {
|
async function findOrCreate(user) {
|
||||||
if (!user || !user.uid) throw new Error('User object and uid are required');
|
if (!user || !user.uid) throw new Error('User object and uid are required');
|
||||||
const { uid, displayName, email } = user;
|
const { uid, displayName, email } = user;
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Timestamp.now();
|
||||||
const docRef = doc(usersCol(), uid);
|
const docRef = doc(usersCol(), uid);
|
||||||
const docSnap = await getDoc(docRef);
|
const docSnap = await getDoc(docRef);
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ async function update({ uid, displayName }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function deleteById(uid) {
|
async function deleteById(uid) {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
const batch = writeBatch(db);
|
const batch = writeBatch(db);
|
||||||
|
|
||||||
// 1. Delete all sessions owned by the user
|
// 1. Delete all sessions owned by the user
|
||||||
|
@ -58,6 +58,16 @@ function update({ uid, displayName }) {
|
|||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMigrationComplete(uid) {
|
||||||
|
const db = sqliteClient.getDb();
|
||||||
|
const stmt = db.prepare('UPDATE users SET has_migrated_to_firebase = 1 WHERE uid = ?');
|
||||||
|
const result = stmt.run(uid);
|
||||||
|
if (result.changes > 0) {
|
||||||
|
console.log(`[Repo] Marked migration as complete for user ${uid}.`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function deleteById(uid) {
|
function deleteById(uid) {
|
||||||
const db = sqliteClient.getDb();
|
const db = sqliteClient.getDb();
|
||||||
const userSessions = db.prepare('SELECT id FROM sessions WHERE uid = ?').all(uid);
|
const userSessions = db.prepare('SELECT id FROM sessions WHERE uid = ?').all(uid);
|
||||||
@ -88,5 +98,6 @@ module.exports = {
|
|||||||
getById,
|
getById,
|
||||||
saveApiKey,
|
saveApiKey,
|
||||||
update,
|
update,
|
||||||
|
setMigrationComplete,
|
||||||
deleteById
|
deleteById
|
||||||
};
|
};
|
@ -3,6 +3,8 @@ const { BrowserWindow } = require('electron');
|
|||||||
const { getFirebaseAuth } = require('./firebaseClient');
|
const { getFirebaseAuth } = require('./firebaseClient');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const encryptionService = require('./encryptionService');
|
const encryptionService = require('./encryptionService');
|
||||||
|
const migrationService = require('./migrationService');
|
||||||
|
const sessionRepository = require('../repositories/session');
|
||||||
|
|
||||||
async function getVirtualKeyByEmail(email, idToken) {
|
async function getVirtualKeyByEmail(email, idToken) {
|
||||||
if (!idToken) {
|
if (!idToken) {
|
||||||
@ -47,6 +49,12 @@ class AuthService {
|
|||||||
initialize() {
|
initialize() {
|
||||||
if (this.isInitialized) return this.initializationPromise;
|
if (this.isInitialized) return this.initializationPromise;
|
||||||
|
|
||||||
|
// --- Break the circular dependency ---
|
||||||
|
// Inject this authService instance into the session repository so it can be used
|
||||||
|
// without a direct `require` cycle.
|
||||||
|
sessionRepository.setAuthService(this);
|
||||||
|
// --- End of dependency injection ---
|
||||||
|
|
||||||
this.initializationPromise = new Promise((resolve) => {
|
this.initializationPromise = new Promise((resolve) => {
|
||||||
const auth = getFirebaseAuth();
|
const auth = getFirebaseAuth();
|
||||||
onAuthStateChanged(auth, async (user) => {
|
onAuthStateChanged(auth, async (user) => {
|
||||||
@ -59,9 +67,16 @@ class AuthService {
|
|||||||
this.currentUserId = user.uid;
|
this.currentUserId = user.uid;
|
||||||
this.currentUserMode = 'firebase';
|
this.currentUserMode = 'firebase';
|
||||||
|
|
||||||
|
// Clean up any zombie sessions from a previous run for this user.
|
||||||
|
await sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
// ** Initialize encryption key for the logged-in user **
|
// ** Initialize encryption key for the logged-in user **
|
||||||
await encryptionService.initializeKey(user.uid);
|
await encryptionService.initializeKey(user.uid);
|
||||||
|
|
||||||
|
// ** Check for and run data migration for the user **
|
||||||
|
// No 'await' here, so it runs in the background without blocking startup.
|
||||||
|
migrationService.checkAndRunMigration(user);
|
||||||
|
|
||||||
|
|
||||||
// Start background task to fetch and save virtual key
|
// Start background task to fetch and save virtual key
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -92,6 +107,9 @@ class AuthService {
|
|||||||
this.currentUserId = 'default_user';
|
this.currentUserId = 'default_user';
|
||||||
this.currentUserMode = 'local';
|
this.currentUserMode = 'local';
|
||||||
|
|
||||||
|
// End active sessions for the local/default user as well.
|
||||||
|
await sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
// ** Initialize encryption key for the default/local user **
|
// ** Initialize encryption key for the default/local user **
|
||||||
await encryptionService.initializeKey(this.currentUserId);
|
await encryptionService.initializeKey(this.currentUserId);
|
||||||
}
|
}
|
||||||
@ -123,6 +141,9 @@ class AuthService {
|
|||||||
async signOut() {
|
async signOut() {
|
||||||
const auth = getFirebaseAuth();
|
const auth = getFirebaseAuth();
|
||||||
try {
|
try {
|
||||||
|
// End all active sessions for the current user BEFORE signing out.
|
||||||
|
await sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
await signOut(auth);
|
await signOut(auth);
|
||||||
console.log('[AuthService] User sign-out initiated successfully.');
|
console.log('[AuthService] User sign-out initiated successfully.');
|
||||||
// onAuthStateChanged will handle the state update and broadcast,
|
// onAuthStateChanged will handle the state update and broadcast,
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
const { initializeApp } = require('firebase/app');
|
const { initializeApp } = require('firebase/app');
|
||||||
const { initializeAuth } = require('firebase/auth');
|
const { initializeAuth } = require('firebase/auth');
|
||||||
const Store = require('electron-store');
|
const Store = require('electron-store');
|
||||||
const { setLogLevel } = require('firebase/firestore');
|
const { getFirestore, setLogLevel } = require('firebase/firestore');
|
||||||
|
|
||||||
setLogLevel('debug');
|
// setLogLevel('debug');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firebase Auth expects the `persistence` option passed to `initializeAuth()` to be *classes*,
|
* Firebase Auth expects the `persistence` option passed to `initializeAuth()` to be *classes*,
|
||||||
@ -69,6 +69,7 @@ const firebaseConfig = {
|
|||||||
|
|
||||||
let firebaseApp = null;
|
let firebaseApp = null;
|
||||||
let firebaseAuth = null;
|
let firebaseAuth = null;
|
||||||
|
let firestoreInstance = null; // To hold the specific DB instance
|
||||||
|
|
||||||
function initializeFirebase() {
|
function initializeFirebase() {
|
||||||
if (firebaseApp) {
|
if (firebaseApp) {
|
||||||
@ -87,7 +88,11 @@ function initializeFirebase() {
|
|||||||
persistence: [ElectronStorePersistence],
|
persistence: [ElectronStorePersistence],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize Firestore with the specific database ID
|
||||||
|
firestoreInstance = getFirestore(firebaseApp, 'pickle-glass');
|
||||||
|
|
||||||
console.log('[FirebaseClient] Firebase initialized successfully with class-based electron-store persistence.');
|
console.log('[FirebaseClient] Firebase initialized successfully with class-based electron-store persistence.');
|
||||||
|
console.log('[FirebaseClient] Firestore instance is targeting the "pickle-glass" database.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[FirebaseClient] Firebase initialization failed:', error);
|
console.error('[FirebaseClient] Firebase initialization failed:', error);
|
||||||
}
|
}
|
||||||
@ -100,7 +105,15 @@ function getFirebaseAuth() {
|
|||||||
return firebaseAuth;
|
return firebaseAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFirestoreInstance() {
|
||||||
|
if (!firestoreInstance) {
|
||||||
|
throw new Error("Firestore has not been initialized. Call initializeFirebase() first.");
|
||||||
|
}
|
||||||
|
return firestoreInstance;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initializeFirebase,
|
initializeFirebase,
|
||||||
getFirebaseAuth,
|
getFirebaseAuth,
|
||||||
|
getFirestoreInstance,
|
||||||
};
|
};
|
192
src/common/services/migrationService.js
Normal file
192
src/common/services/migrationService.js
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
const { doc, writeBatch, Timestamp } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../services/firebaseClient');
|
||||||
|
const encryptionService = require('../services/encryptionService');
|
||||||
|
|
||||||
|
const sqliteSessionRepo = require('../repositories/session/sqlite.repository');
|
||||||
|
const sqlitePresetRepo = require('../repositories/preset/sqlite.repository');
|
||||||
|
const sqliteUserRepo = require('../repositories/user/sqlite.repository');
|
||||||
|
const sqliteSttRepo = require('../../features/listen/stt/repositories/sqlite.repository');
|
||||||
|
const sqliteSummaryRepo = require('../../features/listen/summary/repositories/sqlite.repository');
|
||||||
|
const sqliteAiMessageRepo = require('../../features/ask/repositories/sqlite.repository');
|
||||||
|
|
||||||
|
const MAX_BATCH_OPERATIONS = 500;
|
||||||
|
|
||||||
|
async function checkAndRunMigration(firebaseUser) {
|
||||||
|
if (!firebaseUser || !firebaseUser.uid) {
|
||||||
|
console.log('[Migration] No user, skipping migration check.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Migration] Checking for user ${firebaseUser.uid}...`);
|
||||||
|
|
||||||
|
const localUser = sqliteUserRepo.getById(firebaseUser.uid);
|
||||||
|
if (!localUser || localUser.has_migrated_to_firebase) {
|
||||||
|
console.log(`[Migration] User ${firebaseUser.uid} is not eligible or already migrated.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Migration] Starting data migration for user ${firebaseUser.uid}...`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const db = getFirestoreInstance();
|
||||||
|
|
||||||
|
// --- Phase 1: Migrate Parent Documents (Presets & Sessions) ---
|
||||||
|
console.log('[Migration Phase 1] Migrating parent documents...');
|
||||||
|
let phase1Batch = writeBatch(db);
|
||||||
|
let phase1OpCount = 0;
|
||||||
|
const phase1Promises = [];
|
||||||
|
|
||||||
|
const localPresets = (await sqlitePresetRepo.getPresets(firebaseUser.uid)).filter(p => !p.is_default);
|
||||||
|
console.log(`[Migration Phase 1] Found ${localPresets.length} custom presets.`);
|
||||||
|
for (const preset of localPresets) {
|
||||||
|
const presetRef = doc(db, 'prompt_presets', preset.id);
|
||||||
|
const cleanPreset = {
|
||||||
|
uid: preset.uid,
|
||||||
|
title: encryptionService.encrypt(preset.title ?? ''),
|
||||||
|
prompt: encryptionService.encrypt(preset.prompt ?? ''),
|
||||||
|
is_default: preset.is_default ?? 0,
|
||||||
|
created_at: preset.created_at ? Timestamp.fromMillis(preset.created_at * 1000) : null,
|
||||||
|
updated_at: preset.updated_at ? Timestamp.fromMillis(preset.updated_at * 1000) : null
|
||||||
|
};
|
||||||
|
phase1Batch.set(presetRef, cleanPreset);
|
||||||
|
phase1OpCount++;
|
||||||
|
if (phase1OpCount >= MAX_BATCH_OPERATIONS) {
|
||||||
|
phase1Promises.push(phase1Batch.commit());
|
||||||
|
phase1Batch = writeBatch(db);
|
||||||
|
phase1OpCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const localSessions = await sqliteSessionRepo.getAllByUserId(firebaseUser.uid);
|
||||||
|
console.log(`[Migration Phase 1] Found ${localSessions.length} sessions.`);
|
||||||
|
for (const session of localSessions) {
|
||||||
|
const sessionRef = doc(db, 'sessions', session.id);
|
||||||
|
const cleanSession = {
|
||||||
|
uid: session.uid,
|
||||||
|
members: session.members ?? [session.uid],
|
||||||
|
title: encryptionService.encrypt(session.title ?? ''),
|
||||||
|
session_type: session.session_type ?? 'ask',
|
||||||
|
started_at: session.started_at ? Timestamp.fromMillis(session.started_at * 1000) : null,
|
||||||
|
ended_at: session.ended_at ? Timestamp.fromMillis(session.ended_at * 1000) : null,
|
||||||
|
updated_at: session.updated_at ? Timestamp.fromMillis(session.updated_at * 1000) : null
|
||||||
|
};
|
||||||
|
phase1Batch.set(sessionRef, cleanSession);
|
||||||
|
phase1OpCount++;
|
||||||
|
if (phase1OpCount >= MAX_BATCH_OPERATIONS) {
|
||||||
|
phase1Promises.push(phase1Batch.commit());
|
||||||
|
phase1Batch = writeBatch(db);
|
||||||
|
phase1OpCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase1OpCount > 0) {
|
||||||
|
phase1Promises.push(phase1Batch.commit());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase1Promises.length > 0) {
|
||||||
|
await Promise.all(phase1Promises);
|
||||||
|
console.log(`[Migration Phase 1] Successfully committed ${phase1Promises.length} batches of parent documents.`);
|
||||||
|
} else {
|
||||||
|
console.log('[Migration Phase 1] No parent documents to migrate.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Phase 2: Migrate Child Documents (sub-collections) ---
|
||||||
|
console.log('[Migration Phase 2] Migrating child documents for all sessions...');
|
||||||
|
let phase2Batch = writeBatch(db);
|
||||||
|
let phase2OpCount = 0;
|
||||||
|
const phase2Promises = [];
|
||||||
|
|
||||||
|
for (const session of localSessions) {
|
||||||
|
const transcripts = await sqliteSttRepo.getAllTranscriptsBySessionId(session.id);
|
||||||
|
for (const t of transcripts) {
|
||||||
|
const transcriptRef = doc(db, `sessions/${session.id}/transcripts`, t.id);
|
||||||
|
const cleanTranscript = {
|
||||||
|
uid: firebaseUser.uid,
|
||||||
|
session_id: t.session_id,
|
||||||
|
start_at: t.start_at ? Timestamp.fromMillis(t.start_at * 1000) : null,
|
||||||
|
end_at: t.end_at ? Timestamp.fromMillis(t.end_at * 1000) : null,
|
||||||
|
speaker: t.speaker ?? null,
|
||||||
|
text: encryptionService.encrypt(t.text ?? ''),
|
||||||
|
lang: t.lang ?? 'en',
|
||||||
|
created_at: t.created_at ? Timestamp.fromMillis(t.created_at * 1000) : null
|
||||||
|
};
|
||||||
|
phase2Batch.set(transcriptRef, cleanTranscript);
|
||||||
|
phase2OpCount++;
|
||||||
|
if (phase2OpCount >= MAX_BATCH_OPERATIONS) {
|
||||||
|
phase2Promises.push(phase2Batch.commit());
|
||||||
|
phase2Batch = writeBatch(db);
|
||||||
|
phase2OpCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = await sqliteAiMessageRepo.getAllAiMessagesBySessionId(session.id);
|
||||||
|
for (const m of messages) {
|
||||||
|
const msgRef = doc(db, `sessions/${session.id}/ai_messages`, m.id);
|
||||||
|
const cleanMessage = {
|
||||||
|
uid: firebaseUser.uid,
|
||||||
|
session_id: m.session_id,
|
||||||
|
sent_at: m.sent_at ? Timestamp.fromMillis(m.sent_at * 1000) : null,
|
||||||
|
role: m.role ?? 'user',
|
||||||
|
content: encryptionService.encrypt(m.content ?? ''),
|
||||||
|
tokens: m.tokens ?? null,
|
||||||
|
model: m.model ?? 'unknown',
|
||||||
|
created_at: m.created_at ? Timestamp.fromMillis(m.created_at * 1000) : null
|
||||||
|
};
|
||||||
|
phase2Batch.set(msgRef, cleanMessage);
|
||||||
|
phase2OpCount++;
|
||||||
|
if (phase2OpCount >= MAX_BATCH_OPERATIONS) {
|
||||||
|
phase2Promises.push(phase2Batch.commit());
|
||||||
|
phase2Batch = writeBatch(db);
|
||||||
|
phase2OpCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = await sqliteSummaryRepo.getSummaryBySessionId(session.id);
|
||||||
|
if (summary) {
|
||||||
|
// Reverting to use 'data' as the document ID for summary.
|
||||||
|
const summaryRef = doc(db, `sessions/${session.id}/summary`, 'data');
|
||||||
|
const cleanSummary = {
|
||||||
|
uid: firebaseUser.uid,
|
||||||
|
session_id: summary.session_id,
|
||||||
|
generated_at: summary.generated_at ? Timestamp.fromMillis(summary.generated_at * 1000) : null,
|
||||||
|
model: summary.model ?? 'unknown',
|
||||||
|
tldr: encryptionService.encrypt(summary.tldr ?? ''),
|
||||||
|
text: encryptionService.encrypt(summary.text ?? ''),
|
||||||
|
bullet_json: encryptionService.encrypt(summary.bullet_json ?? '[]'),
|
||||||
|
action_json: encryptionService.encrypt(summary.action_json ?? '[]'),
|
||||||
|
tokens_used: summary.tokens_used ?? null,
|
||||||
|
updated_at: summary.updated_at ? Timestamp.fromMillis(summary.updated_at * 1000) : null
|
||||||
|
};
|
||||||
|
phase2Batch.set(summaryRef, cleanSummary);
|
||||||
|
phase2OpCount++;
|
||||||
|
if (phase2OpCount >= MAX_BATCH_OPERATIONS) {
|
||||||
|
phase2Promises.push(phase2Batch.commit());
|
||||||
|
phase2Batch = writeBatch(db);
|
||||||
|
phase2OpCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase2OpCount > 0) {
|
||||||
|
phase2Promises.push(phase2Batch.commit());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phase2Promises.length > 0) {
|
||||||
|
await Promise.all(phase2Promises);
|
||||||
|
console.log(`[Migration Phase 2] Successfully committed ${phase2Promises.length} batches of child documents.`);
|
||||||
|
} else {
|
||||||
|
console.log('[Migration Phase 2] No child documents to migrate.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 4. Mark migration as complete ---
|
||||||
|
sqliteUserRepo.setMigrationComplete(firebaseUser.uid);
|
||||||
|
console.log(`[Migration] ✅ Successfully marked migration as complete for ${firebaseUser.uid}.`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Migration] 🔥 An error occurred during migration for user ${firebaseUser.uid}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
checkAndRunMigration,
|
||||||
|
};
|
@ -28,9 +28,18 @@ async function sendMessage(userPrompt) {
|
|||||||
askWindow.webContents.send('hide-text-input');
|
askWindow.webContents.send('hide-text-input');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sessionId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
|
console.log(`[AskService] 🤖 Processing message: ${userPrompt.substring(0, 50)}...`);
|
||||||
|
|
||||||
|
// --- Save user's message immediately ---
|
||||||
|
// This ensures the user message is always timestamped before the assistant's response.
|
||||||
|
sessionId = await sessionRepository.getOrCreateActive('ask');
|
||||||
|
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
|
||||||
|
console.log(`[AskService] DB: Saved user prompt to session ${sessionId}`);
|
||||||
|
// --- End of user message saving ---
|
||||||
|
|
||||||
const modelInfo = await getCurrentModelInfo(null, { type: 'llm' });
|
const modelInfo = await getCurrentModelInfo(null, { type: 'llm' });
|
||||||
if (!modelInfo || !modelInfo.apiKey) {
|
if (!modelInfo || !modelInfo.apiKey) {
|
||||||
throw new Error('AI model or API key not configured.');
|
throw new Error('AI model or API key not configured.');
|
||||||
@ -99,15 +108,13 @@ async function sendMessage(userPrompt) {
|
|||||||
if (data === '[DONE]') {
|
if (data === '[DONE]') {
|
||||||
askWin.webContents.send('ask-response-stream-end');
|
askWin.webContents.send('ask-response-stream-end');
|
||||||
|
|
||||||
// Save to DB
|
// Save assistant's message to DB
|
||||||
try {
|
try {
|
||||||
// The repository adapter will now handle the UID internally.
|
// sessionId is already available from when we saved the user prompt
|
||||||
const sessionId = await sessionRepository.getOrCreateActive('ask');
|
|
||||||
await askRepository.addAiMessage({ sessionId, role: 'user', content: userPrompt.trim() });
|
|
||||||
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
|
await askRepository.addAiMessage({ sessionId, role: 'assistant', content: fullResponse });
|
||||||
console.log(`[AskService] DB: Saved ask/answer pair to session ${sessionId}`);
|
console.log(`[AskService] DB: Saved assistant response to session ${sessionId}`);
|
||||||
} catch(dbError) {
|
} catch(dbError) {
|
||||||
console.error("[AskService] DB: Failed to save ask/answer pair:", dbError);
|
console.error("[AskService] DB: Failed to save assistant response:", dbError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, response: fullResponse };
|
return { success: true, response: fullResponse };
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
const { getFirestore, collection, addDoc, query, getDocs, orderBy } = require('firebase/firestore');
|
const { collection, addDoc, query, getDocs, orderBy, Timestamp } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../../../common/services/firebaseClient');
|
||||||
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
||||||
|
|
||||||
const aiMessageConverter = createEncryptedConverter(['content']);
|
const aiMessageConverter = createEncryptedConverter(['content']);
|
||||||
|
|
||||||
function aiMessagesCol(sessionId) {
|
function aiMessagesCol(sessionId) {
|
||||||
if (!sessionId) throw new Error("Session ID is required to access AI messages.");
|
if (!sessionId) throw new Error("Session ID is required to access AI messages.");
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, `sessions/${sessionId}/ai_messages`).withConverter(aiMessageConverter);
|
return collection(db, `sessions/${sessionId}/ai_messages`).withConverter(aiMessageConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addAiMessage({ uid, sessionId, role, content, model = 'unknown' }) {
|
async function addAiMessage({ uid, sessionId, role, content, model = 'unknown' }) {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Timestamp.now();
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
uid, // To identify the author of the message
|
uid, // To identify the author of the message
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
const { getFirestore, collection, addDoc, query, getDocs, orderBy } = require('firebase/firestore');
|
const { collection, addDoc, query, getDocs, orderBy, Timestamp } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../../../../common/services/firebaseClient');
|
||||||
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
|
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
|
||||||
|
|
||||||
const transcriptConverter = createEncryptedConverter(['text']);
|
const transcriptConverter = createEncryptedConverter(['text']);
|
||||||
|
|
||||||
function transcriptsCol(sessionId) {
|
function transcriptsCol(sessionId) {
|
||||||
if (!sessionId) throw new Error("Session ID is required to access transcripts.");
|
if (!sessionId) throw new Error("Session ID is required to access transcripts.");
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, `sessions/${sessionId}/transcripts`).withConverter(transcriptConverter);
|
return collection(db, `sessions/${sessionId}/transcripts`).withConverter(transcriptConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addTranscript({ uid, sessionId, speaker, text }) {
|
async function addTranscript({ uid, sessionId, speaker, text }) {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Timestamp.now();
|
||||||
const newTranscript = {
|
const newTranscript = {
|
||||||
uid, // To identify the author/source of the transcript
|
uid, // To identify the author/source of the transcript
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
const { getFirestore, collection, doc, setDoc, getDoc } = require('firebase/firestore');
|
const { collection, doc, setDoc, getDoc, Timestamp } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../../../../common/services/firebaseClient');
|
||||||
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
|
const { createEncryptedConverter } = require('../../../../common/repositories/firestoreConverter');
|
||||||
|
const encryptionService = require('../../../../common/services/encryptionService');
|
||||||
|
|
||||||
const fieldsToEncrypt = ['tldr', 'text', 'bullet_json', 'action_json'];
|
const fieldsToEncrypt = ['tldr', 'text', 'bullet_json', 'action_json'];
|
||||||
const summaryConverter = createEncryptedConverter(fieldsToEncrypt);
|
const summaryConverter = createEncryptedConverter(fieldsToEncrypt);
|
||||||
|
|
||||||
function summaryDocRef(sessionId) {
|
function summaryDocRef(sessionId) {
|
||||||
if (!sessionId) throw new Error("Session ID is required to access summary.");
|
if (!sessionId) throw new Error("Session ID is required to access summary.");
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
const path = `sessions/${sessionId}/summary`;
|
// Reverting to the original structure with 'data' as the document ID.
|
||||||
return doc(collection(db, path).withConverter(summaryConverter), 'data');
|
const docPath = `sessions/${sessionId}/summary/data`;
|
||||||
|
return doc(db, docPath).withConverter(summaryConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSummary({ uid, sessionId, tldr, text, bullet_json, action_json, model = 'unknown' }) {
|
async function saveSummary({ uid, sessionId, tldr, text, bullet_json, action_json, model = 'unknown' }) {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Timestamp.now();
|
||||||
const summaryData = {
|
const summaryData = {
|
||||||
uid, // To know who generated the summary
|
uid, // To know who generated the summary
|
||||||
|
session_id: sessionId,
|
||||||
generated_at: now,
|
generated_at: now,
|
||||||
model,
|
model,
|
||||||
text,
|
text,
|
||||||
@ -24,6 +28,8 @@ async function saveSummary({ uid, sessionId, tldr, text, bullet_json, action_jso
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The converter attached to summaryDocRef will handle encryption via its `toFirestore` method.
|
||||||
|
// Manual encryption was removed to fix the double-encryption bug.
|
||||||
const docRef = summaryDocRef(sessionId);
|
const docRef = summaryDocRef(sessionId);
|
||||||
await setDoc(docRef, summaryData, { merge: true });
|
await setDoc(docRef, summaryData, { merge: true });
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
const { getFirestore, collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy } = require('firebase/firestore');
|
const { collection, doc, addDoc, getDoc, getDocs, updateDoc, deleteDoc, query, where, orderBy } = require('firebase/firestore');
|
||||||
|
const { getFirestoreInstance } = require('../../../common/services/firebaseClient');
|
||||||
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
const { createEncryptedConverter } = require('../../../common/repositories/firestoreConverter');
|
||||||
|
const encryptionService = require('../../../common/services/encryptionService');
|
||||||
|
|
||||||
const userPresetConverter = createEncryptedConverter(['prompt']);
|
const userPresetConverter = createEncryptedConverter(['prompt', 'title']);
|
||||||
|
|
||||||
const defaultPresetConverter = {
|
const defaultPresetConverter = {
|
||||||
toFirestore: (data) => data,
|
toFirestore: (data) => data,
|
||||||
@ -12,13 +14,13 @@ const defaultPresetConverter = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function userPresetsCol() {
|
function userPresetsCol() {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, 'prompt_presets').withConverter(userPresetConverter);
|
return collection(db, 'prompt_presets').withConverter(userPresetConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultPresetsCol() {
|
function defaultPresetsCol() {
|
||||||
const db = getFirestore();
|
const db = getFirestoreInstance();
|
||||||
return collection(db, 'defaults/prompt_presets').withConverter(defaultPresetConverter);
|
return collection(db, 'defaults/v1/prompt_presets').withConverter(defaultPresetConverter);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPresets(uid) {
|
async function getPresets(uid) {
|
||||||
@ -54,7 +56,7 @@ async function createPreset({ uid, title, prompt }) {
|
|||||||
uid: uid,
|
uid: uid,
|
||||||
title,
|
title,
|
||||||
prompt,
|
prompt,
|
||||||
is_default: false,
|
is_default: 0,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
};
|
};
|
||||||
const docRef = await addDoc(userPresetsCol(), newPreset);
|
const docRef = await addDoc(userPresetsCol(), newPreset);
|
||||||
@ -68,8 +70,17 @@ async function updatePreset(id, { title, prompt }, uid) {
|
|||||||
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
|
if (!docSnap.exists() || docSnap.data().uid !== uid || docSnap.data().is_default) {
|
||||||
throw new Error("Preset not found or permission denied to update.");
|
throw new Error("Preset not found or permission denied to update.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updates = {};
|
||||||
|
if (title !== undefined) {
|
||||||
|
updates.title = encryptionService.encrypt(title);
|
||||||
|
}
|
||||||
|
if (prompt !== undefined) {
|
||||||
|
updates.prompt = encryptionService.encrypt(prompt);
|
||||||
|
}
|
||||||
|
updates.updated_at = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
await updateDoc(docRef, { title, prompt });
|
await updateDoc(docRef, updates);
|
||||||
return { changes: 1 };
|
return { changes: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +98,7 @@ async function deletePreset(id, uid) {
|
|||||||
|
|
||||||
async function getAutoUpdate(uid) {
|
async function getAutoUpdate(uid) {
|
||||||
// Assume users are stored in a "users" collection, and auto_update_enabled is a field
|
// Assume users are stored in a "users" collection, and auto_update_enabled is a field
|
||||||
const userDocRef = doc(getFirestore(), 'users', uid);
|
const userDocRef = doc(getFirestoreInstance(), 'users', uid);
|
||||||
try {
|
try {
|
||||||
const userSnap = await getDoc(userDocRef);
|
const userSnap = await getDoc(userDocRef);
|
||||||
if (userSnap.exists()) {
|
if (userSnap.exists()) {
|
||||||
@ -110,7 +121,7 @@ async function getAutoUpdate(uid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setAutoUpdate(uid, isEnabled) {
|
async function setAutoUpdate(uid, isEnabled) {
|
||||||
const userDocRef = doc(getFirestore(), 'users', uid);
|
const userDocRef = doc(getFirestoreInstance(), 'users', uid);
|
||||||
try {
|
try {
|
||||||
const userSnap = await getDoc(userDocRef);
|
const userSnap = await getDoc(userDocRef);
|
||||||
if (userSnap.exists()) {
|
if (userSnap.exists()) {
|
||||||
|
@ -187,8 +187,8 @@ app.whenReady().then(async () => {
|
|||||||
await databaseInitializer.initialize();
|
await databaseInitializer.initialize();
|
||||||
console.log('>>> [index.js] Database initialized successfully');
|
console.log('>>> [index.js] Database initialized successfully');
|
||||||
|
|
||||||
// Clean up zombie sessions from previous runs first
|
// Clean up zombie sessions from previous runs first - MOVED TO authService
|
||||||
sessionRepository.endAllActiveSessions();
|
// sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
await authService.initialize();
|
await authService.initialize();
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ app.on('window-all-closed', () => {
|
|||||||
app.on('before-quit', async () => {
|
app.on('before-quit', async () => {
|
||||||
console.log('[Shutdown] App is about to quit.');
|
console.log('[Shutdown] App is about to quit.');
|
||||||
listenService.stopMacOSAudioCapture();
|
listenService.stopMacOSAudioCapture();
|
||||||
await sessionRepository.endAllActiveSessions();
|
// await sessionRepository.endAllActiveSessions(); // MOVED TO authService
|
||||||
databaseInitializer.close();
|
databaseInitializer.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user