WIP encryption cleanup + providerSetting refactor
This commit is contained in:
		
							parent
							
								
									94ae002d83
								
							
						
					
					
						commit
						ab23c10006
					
				@ -11,6 +11,7 @@ const presetRepository = require('../features/common/repositories/preset');
 | 
			
		||||
const askService = require('../features/ask/askService');
 | 
			
		||||
const listenService = require('../features/listen/listenService');
 | 
			
		||||
const permissionService = require('../features/common/services/permissionService');
 | 
			
		||||
const encryptionService = require('../features/common/services/encryptionService');
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
  // Renderer로부터의 요청을 수신하고 서비스로 전달
 | 
			
		||||
@ -20,7 +21,6 @@ module.exports = {
 | 
			
		||||
    ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
 | 
			
		||||
    ipcMain.handle('settings:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));  
 | 
			
		||||
    ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
 | 
			
		||||
    ipcMain.handle('settings:validate-and-save-key', async (e, { provider, key }) => await settingsService.validateAndSaveKey(provider, key));
 | 
			
		||||
    ipcMain.handle('settings:clear-api-key', async (e, { provider }) => await settingsService.clearApiKey(provider));
 | 
			
		||||
    ipcMain.handle('settings:set-selected-model', async (e, { type, modelId }) => await settingsService.setSelectedModel(type, modelId));    
 | 
			
		||||
 | 
			
		||||
@ -37,8 +37,13 @@ module.exports = {
 | 
			
		||||
    ipcMain.handle('check-system-permissions', async () => await permissionService.checkSystemPermissions());
 | 
			
		||||
    ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
 | 
			
		||||
    ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
 | 
			
		||||
    ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
 | 
			
		||||
    ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
 | 
			
		||||
    ipcMain.handle('mark-keychain-completed', async () => await permissionService.markKeychainCompleted());
 | 
			
		||||
    ipcMain.handle('check-keychain-completed', async () => await permissionService.checkKeychainCompleted());
 | 
			
		||||
    ipcMain.handle('initialize-encryption-key', async () => {
 | 
			
		||||
        const userId = authService.getCurrentUserId();
 | 
			
		||||
        await encryptionService.initializeKey(userId);
 | 
			
		||||
        return { success: true };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // User/Auth
 | 
			
		||||
    ipcMain.handle('get-current-user', () => authService.getCurrentUser());
 | 
			
		||||
@ -109,6 +114,7 @@ module.exports = {
 | 
			
		||||
    ipcMain.handle('model:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
 | 
			
		||||
    ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
 | 
			
		||||
    ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
 | 
			
		||||
    ipcMain.handle('model:re-initialize-state', () => modelStateService.initialize());
 | 
			
		||||
 | 
			
		||||
    console.log('[FeatureBridge] Initialized with all feature handlers.');
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
@ -117,6 +117,12 @@ const LATEST_SCHEMA = {
 | 
			
		||||
            { name: 'accelerator', type: 'TEXT NOT NULL' },
 | 
			
		||||
            { name: 'created_at', type: 'INTEGER' }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
    permissions: {
 | 
			
		||||
        columns: [
 | 
			
		||||
            { name: 'uid', type: 'TEXT PRIMARY KEY' },
 | 
			
		||||
            { name: 'keychain_completed', type: 'INTEGER DEFAULT 0' }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,6 @@ function getRepository() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    markPermissionsAsCompleted: (...args) => getRepository().markPermissionsAsCompleted(...args),
 | 
			
		||||
    checkPermissionsCompleted: (...args) => getRepository().checkPermissionsCompleted(...args),
 | 
			
		||||
    markKeychainCompleted: (...args) => getRepository().markKeychainCompleted(...args),
 | 
			
		||||
    checkKeychainCompleted: (...args) => getRepository().checkKeychainCompleted(...args),
 | 
			
		||||
}; 
 | 
			
		||||
@ -1,14 +1,18 @@
 | 
			
		||||
const sqliteClient = require('../../services/sqliteClient');
 | 
			
		||||
 | 
			
		||||
async function markPermissionsAsCompleted() {
 | 
			
		||||
    return sqliteClient.markPermissionsAsCompleted();
 | 
			
		||||
function markKeychainCompleted(uid) {
 | 
			
		||||
    return sqliteClient.query(
 | 
			
		||||
        'INSERT OR REPLACE INTO permissions (uid, keychain_completed) VALUES (?, 1)',
 | 
			
		||||
        [uid]
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function checkPermissionsCompleted() {
 | 
			
		||||
    return sqliteClient.checkPermissionsCompleted();
 | 
			
		||||
function checkKeychainCompleted(uid) {
 | 
			
		||||
    const row = sqliteClient.query('SELECT keychain_completed FROM permissions WHERE uid = ?', [uid]);
 | 
			
		||||
    return row.length > 0 && row[0].keychain_completed === 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    markPermissionsAsCompleted,
 | 
			
		||||
    checkPermissionsCompleted,
 | 
			
		||||
    markKeychainCompleted,
 | 
			
		||||
    checkKeychainCompleted
 | 
			
		||||
}; 
 | 
			
		||||
@ -56,6 +56,13 @@ const providerSettingsRepositoryAdapter = {
 | 
			
		||||
        const repo = getBaseRepository();
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await repo.removeAllByUid(uid);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    async getRawApiKeysByUid() {
 | 
			
		||||
        // This function should always target the local sqlite DB,
 | 
			
		||||
        // as it's part of the local-first boot sequence.
 | 
			
		||||
        const uid = authService.getCurrentUserId();
 | 
			
		||||
        return await sqliteRepository.getRawApiKeysByUid(uid);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ function getByProvider(uid, provider) {
 | 
			
		||||
    const result = stmt.get(uid, provider) || null;
 | 
			
		||||
    
 | 
			
		||||
    if (result && result.api_key) {
 | 
			
		||||
        // Decrypt API key if it exists
 | 
			
		||||
        result.api_key = encryptionService.decrypt(result.api_key);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
@ -19,7 +18,6 @@ function getAllByUid(uid) {
 | 
			
		||||
    const stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
 | 
			
		||||
    const results = stmt.all(uid);
 | 
			
		||||
    
 | 
			
		||||
    // Decrypt API keys for all results
 | 
			
		||||
    return results.map(result => {
 | 
			
		||||
        if (result.api_key) {
 | 
			
		||||
            result.api_key = encryptionService.decrypt(result.api_key);
 | 
			
		||||
@ -31,12 +29,6 @@ function getAllByUid(uid) {
 | 
			
		||||
function upsert(uid, provider, settings) {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    
 | 
			
		||||
    // Encrypt API key if it exists
 | 
			
		||||
    const encryptedSettings = { ...settings };
 | 
			
		||||
    if (encryptedSettings.api_key) {
 | 
			
		||||
        encryptedSettings.api_key = encryptionService.encrypt(encryptedSettings.api_key);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
 | 
			
		||||
    const stmt = db.prepare(`
 | 
			
		||||
        INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, created_at, updated_at)
 | 
			
		||||
@ -51,11 +43,11 @@ function upsert(uid, provider, settings) {
 | 
			
		||||
    const result = stmt.run(
 | 
			
		||||
        uid,
 | 
			
		||||
        provider,
 | 
			
		||||
        encryptedSettings.api_key || null,
 | 
			
		||||
        encryptedSettings.selected_llm_model || null,
 | 
			
		||||
        encryptedSettings.selected_stt_model || null,
 | 
			
		||||
        encryptedSettings.created_at || Date.now(),
 | 
			
		||||
        encryptedSettings.updated_at
 | 
			
		||||
        settings.api_key || null,
 | 
			
		||||
        settings.selected_llm_model || null,
 | 
			
		||||
        settings.selected_stt_model || null,
 | 
			
		||||
        settings.created_at || Date.now(),
 | 
			
		||||
        settings.updated_at
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    return { changes: result.changes };
 | 
			
		||||
@ -75,10 +67,17 @@ function removeAllByUid(uid) {
 | 
			
		||||
    return { changes: result.changes };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getRawApiKeysByUid(uid) {
 | 
			
		||||
    const db = sqliteClient.getDb();
 | 
			
		||||
    const stmt = db.prepare('SELECT api_key FROM provider_settings WHERE uid = ?');
 | 
			
		||||
    return stmt.all(uid);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getByProvider,
 | 
			
		||||
    getAllByUid,
 | 
			
		||||
    upsert,
 | 
			
		||||
    remove,
 | 
			
		||||
    removeAllByUid
 | 
			
		||||
    removeAllByUid,
 | 
			
		||||
    getRawApiKeysByUid
 | 
			
		||||
}; 
 | 
			
		||||
@ -7,6 +7,7 @@ const migrationService = require('./migrationService');
 | 
			
		||||
const sessionRepository = require('../repositories/session');
 | 
			
		||||
const providerSettingsRepository = require('../repositories/providerSettings');
 | 
			
		||||
const userModelSelectionsRepository = require('../repositories/userModelSelections');
 | 
			
		||||
const permissionService = require('./permissionService');
 | 
			
		||||
 | 
			
		||||
async function getVirtualKeyByEmail(email, idToken) {
 | 
			
		||||
    if (!idToken) {
 | 
			
		||||
@ -43,7 +44,6 @@ class AuthService {
 | 
			
		||||
        this.isInitialized = false;
 | 
			
		||||
 | 
			
		||||
        // This ensures the key is ready before any login/logout state change.
 | 
			
		||||
        encryptionService.initializeKey(this.currentUserId);
 | 
			
		||||
        this.initializationPromise = null;
 | 
			
		||||
 | 
			
		||||
        sessionRepository.setAuthService(this);
 | 
			
		||||
@ -69,8 +69,12 @@ class AuthService {
 | 
			
		||||
                    // Clean up any zombie sessions from a previous run for this user.
 | 
			
		||||
                    await sessionRepository.endAllActiveSessions();
 | 
			
		||||
 | 
			
		||||
                    // ** Initialize encryption key for the logged-in user **
 | 
			
		||||
                    await encryptionService.initializeKey(user.uid);
 | 
			
		||||
                    // ** Initialize encryption key for the logged-in user if permissions are already granted **
 | 
			
		||||
                    if (process.platform === 'darwin' && !(await permissionService.checkKeychainCompleted(this.currentUserId))) {
 | 
			
		||||
                        console.warn('[AuthService] Keychain permission not yet completed for this user. Deferring key initialization.');
 | 
			
		||||
                    } else if (process.platform === 'darwin') {
 | 
			
		||||
                        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.
 | 
			
		||||
@ -109,8 +113,12 @@ class AuthService {
 | 
			
		||||
                    // End active sessions for the local/default user as well.
 | 
			
		||||
                    await sessionRepository.endAllActiveSessions();
 | 
			
		||||
 | 
			
		||||
                    // ** Initialize encryption key for the default/local user **
 | 
			
		||||
                    await encryptionService.initializeKey(this.currentUserId);
 | 
			
		||||
                    // ** Initialize encryption key for the default/local user if permissions are already granted **
 | 
			
		||||
                    if (process.platform === 'darwin' && !(await permissionService.checkKeychainCompleted(this.currentUserId))) {
 | 
			
		||||
                        console.warn('[AuthService] Keychain permission not yet completed for default user. Deferring key initialization.');
 | 
			
		||||
                    } else if (process.platform === 'darwin') {
 | 
			
		||||
                        await encryptionService.initializeKey(this.currentUserId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                this.broadcastUserState();
 | 
			
		||||
                
 | 
			
		||||
@ -175,7 +183,6 @@ class AuthService {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    getCurrentUserId() {
 | 
			
		||||
        return this.currentUserId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -10,10 +10,13 @@ class DatabaseInitializer {
 | 
			
		||||
        
 | 
			
		||||
        // 최종적으로 사용될 DB 경로 (쓰기 가능한 위치)
 | 
			
		||||
        const userDataPath = app.getPath('userData');
 | 
			
		||||
        // In both development and production mode, the database is stored in the userData directory:
 | 
			
		||||
        //   macOS: ~/Library/Application Support/Glass/pickleglass.db
 | 
			
		||||
        //   Windows: %APPDATA%\Glass\pickleglass.db
 | 
			
		||||
        this.dbPath = path.join(userDataPath, 'pickleglass.db');
 | 
			
		||||
        this.dataDir = userDataPath;
 | 
			
		||||
 | 
			
		||||
        // 원본 DB 경로 (패키지 내 읽기 전용 위치)
 | 
			
		||||
        // The original DB path (read-only location in the package)
 | 
			
		||||
        this.sourceDbPath = app.isPackaged
 | 
			
		||||
            ? path.join(process.resourcesPath, 'data', 'pickleglass.db')
 | 
			
		||||
            : path.join(app.getAppPath(), 'data', 'pickleglass.db');
 | 
			
		||||
@ -52,7 +55,7 @@ class DatabaseInitializer {
 | 
			
		||||
        try {
 | 
			
		||||
            this.ensureDatabaseExists();
 | 
			
		||||
 | 
			
		||||
            await sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
 | 
			
		||||
            sqliteClient.connect(this.dbPath); // DB 경로를 인자로 전달
 | 
			
		||||
            
 | 
			
		||||
            // This single call will now synchronize the schema and then init default data.
 | 
			
		||||
            await sqliteClient.initTables();
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,8 @@ try {
 | 
			
		||||
    keytar = null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const permissionService = require('./permissionService');
 | 
			
		||||
 | 
			
		||||
const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
 | 
			
		||||
let sessionKey = null; // In-memory fallback key
 | 
			
		||||
 | 
			
		||||
@ -31,6 +33,8 @@ async function initializeKey(userId) {
 | 
			
		||||
        throw new Error('A user ID must be provided to initialize the encryption key.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let keyRetrieved = false;
 | 
			
		||||
 | 
			
		||||
    if (keytar) {
 | 
			
		||||
        try {
 | 
			
		||||
            let key = await keytar.getPassword(SERVICE_NAME, userId);
 | 
			
		||||
@ -41,6 +45,7 @@ async function initializeKey(userId) {
 | 
			
		||||
                console.log(`[EncryptionService] New key securely stored in keychain for ${userId}.`);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
 | 
			
		||||
                keyRetrieved = true;
 | 
			
		||||
            }
 | 
			
		||||
            sessionKey = key;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
@ -56,6 +61,16 @@ async function initializeKey(userId) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Mark keychain completed in permissions DB if this is the first successful retrieval or storage
 | 
			
		||||
    try {
 | 
			
		||||
        await permissionService.markKeychainCompleted(userId);
 | 
			
		||||
        if (keyRetrieved) {
 | 
			
		||||
            console.log(`[EncryptionService] Keychain completion marked in DB for ${userId}.`);
 | 
			
		||||
        }
 | 
			
		||||
    } catch (permErr) {
 | 
			
		||||
        console.error('[EncryptionService] Failed to mark keychain completion:', permErr);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!sessionKey) {
 | 
			
		||||
        throw new Error('Failed to initialize encryption key.');
 | 
			
		||||
    }
 | 
			
		||||
@ -129,6 +144,7 @@ function decrypt(encryptedText) {
 | 
			
		||||
    } 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.
 | 
			
		||||
        console.error('[EncryptionService] Decryption failed:', error);
 | 
			
		||||
        return encryptedText;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,13 +9,39 @@ const userModelSelectionsRepository = require('../repositories/userModelSelectio
 | 
			
		||||
 | 
			
		||||
// Import authService directly (singleton)
 | 
			
		||||
const authService = require('./authService');
 | 
			
		||||
const permissionService = require('./permissionService');
 | 
			
		||||
 | 
			
		||||
function looksEncrypted(str) {
 | 
			
		||||
    if (!str || typeof str !== 'string') return false;
 | 
			
		||||
    // Base64 chars + optional '=' padding
 | 
			
		||||
    if (!/^[A-Za-z0-9+/]+={0,2}$/.test(str)) return false;
 | 
			
		||||
    try {
 | 
			
		||||
        const buf = Buffer.from(str, 'base64');
 | 
			
		||||
        // Our AES-GCM cipher text must be at least 32 bytes (IV 16 + TAG 16)
 | 
			
		||||
        return buf.length >= 32;
 | 
			
		||||
    } catch {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ModelStateService extends EventEmitter {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
        this.authService = authService;
 | 
			
		||||
        this.store = new Store({ name: 'pickle-glass-model-state' });
 | 
			
		||||
        this.state = {};
 | 
			
		||||
        
 | 
			
		||||
        const initialApiKeys = Object.keys(PROVIDERS).reduce((acc, provider) => {
 | 
			
		||||
            acc[provider] = null;
 | 
			
		||||
            return acc;
 | 
			
		||||
        }, {});
 | 
			
		||||
 | 
			
		||||
        this.state = {
 | 
			
		||||
            apiKeys: initialApiKeys,
 | 
			
		||||
            selectedModels: {
 | 
			
		||||
                llm: null,
 | 
			
		||||
                stt: null
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        this.hasMigrated = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -194,8 +220,18 @@ class ModelStateService extends EventEmitter {
 | 
			
		||||
    async _loadStateForCurrentUser() {
 | 
			
		||||
        const userId = this.authService.getCurrentUserId();
 | 
			
		||||
 | 
			
		||||
        // Initialize encryption service for current user
 | 
			
		||||
        await encryptionService.initializeKey(userId);
 | 
			
		||||
        // Conditionally initialize encryption if old encrypted keys are detected
 | 
			
		||||
        try {
 | 
			
		||||
            const rows = await providerSettingsRepository.getRawApiKeysByUid();
 | 
			
		||||
            if (rows.some(r => looksEncrypted(r.api_key))) {
 | 
			
		||||
                console.log('[ModelStateService] Encrypted keys detected, initializing encryption...');
 | 
			
		||||
                await encryptionService.initializeKey(userId);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log('[ModelStateService] No encrypted keys detected, skipping encryption initialization.');
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            console.warn('[ModelStateService] Error while checking encrypted keys:', err.message);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Try to load from database first
 | 
			
		||||
        await this._loadStateFromDatabase();
 | 
			
		||||
@ -263,17 +299,6 @@ class ModelStateService extends EventEmitter {
 | 
			
		||||
            apiKeys: { ...this.state.apiKeys }
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        for (const [provider, key] of Object.entries(stateToSave.apiKeys)) {
 | 
			
		||||
            if (key) {
 | 
			
		||||
                try {
 | 
			
		||||
                    stateToSave.apiKeys[provider] = encryptionService.encrypt(key);
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error(`[ModelStateService] Failed to encrypt API key for ${provider}`);
 | 
			
		||||
                    stateToSave.apiKeys[provider] = null;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        this.store.set(`users.${userId}`, stateToSave);
 | 
			
		||||
        console.log(`[ModelStateService] State saved to electron-store for user: ${userId}`);
 | 
			
		||||
        this._logCurrentSelection();
 | 
			
		||||
 | 
			
		||||
@ -2,27 +2,28 @@ const { systemPreferences, shell, desktopCapturer } = require('electron');
 | 
			
		||||
const permissionRepository = require('../repositories/permission');
 | 
			
		||||
 | 
			
		||||
class PermissionService {
 | 
			
		||||
  _getAuthService() {
 | 
			
		||||
    return require('./authService');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkSystemPermissions() {
 | 
			
		||||
    const permissions = {
 | 
			
		||||
      microphone: 'unknown',
 | 
			
		||||
      screen: 'unknown',
 | 
			
		||||
      keychain: 'unknown',
 | 
			
		||||
      needsSetup: true
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (process.platform === 'darwin') {
 | 
			
		||||
        const micStatus = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
        console.log('[Permissions] Microphone status:', micStatus);
 | 
			
		||||
        permissions.microphone = micStatus;
 | 
			
		||||
 | 
			
		||||
        const screenStatus = systemPreferences.getMediaAccessStatus('screen');
 | 
			
		||||
        console.log('[Permissions] Screen status:', screenStatus);
 | 
			
		||||
        permissions.screen = screenStatus;
 | 
			
		||||
 | 
			
		||||
        permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
 | 
			
		||||
        permissions.microphone = systemPreferences.getMediaAccessStatus('microphone');
 | 
			
		||||
        permissions.screen = systemPreferences.getMediaAccessStatus('screen');
 | 
			
		||||
        permissions.keychain = await this.checkKeychainCompleted(this._getAuthService().getCurrentUserId()) ? 'granted' : 'unknown';
 | 
			
		||||
        permissions.needsSetup = permissions.microphone !== 'granted' || permissions.screen !== 'granted' || permissions.keychain !== 'granted';
 | 
			
		||||
      } else {
 | 
			
		||||
        permissions.microphone = 'granted';
 | 
			
		||||
        permissions.screen = 'granted';
 | 
			
		||||
        permissions.keychain = 'granted';
 | 
			
		||||
        permissions.needsSetup = false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -33,6 +34,7 @@ class PermissionService {
 | 
			
		||||
      return {
 | 
			
		||||
        microphone: 'unknown',
 | 
			
		||||
        screen: 'unknown',
 | 
			
		||||
        keychain: 'unknown',
 | 
			
		||||
        needsSetup: true,
 | 
			
		||||
        error: error.message
 | 
			
		||||
      };
 | 
			
		||||
@ -92,24 +94,24 @@ class PermissionService {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async markPermissionsAsCompleted() {
 | 
			
		||||
  async markKeychainCompleted() {
 | 
			
		||||
    try {
 | 
			
		||||
      await permissionRepository.markPermissionsAsCompleted();
 | 
			
		||||
      console.log('[Permissions] Marked permissions as completed');
 | 
			
		||||
      await permissionRepository.markKeychainCompleted(this._getAuthService().getCurrentUserId());
 | 
			
		||||
      console.log('[Permissions] Marked keychain as completed');
 | 
			
		||||
      return { success: true };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[Permissions] Error marking permissions as completed:', error);
 | 
			
		||||
      console.error('[Permissions] Error marking keychain as completed:', error);
 | 
			
		||||
      return { success: false, error: error.message };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async checkPermissionsCompleted() {
 | 
			
		||||
  async checkKeychainCompleted(uid) {
 | 
			
		||||
    try {
 | 
			
		||||
      const completed = await permissionRepository.checkPermissionsCompleted();
 | 
			
		||||
      console.log('[Permissions] Permissions completed status:', completed);
 | 
			
		||||
      const completed = permissionRepository.checkKeychainCompleted(uid);
 | 
			
		||||
      console.log('[Permissions] Keychain completed status:', completed);
 | 
			
		||||
      return completed;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[Permissions] Error checking permissions completed status:', error);
 | 
			
		||||
      console.error('[Permissions] Error checking keychain completed status:', error);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ class SQLiteClient {
 | 
			
		||||
        return `"${identifier}"`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    synchronizeSchema() {
 | 
			
		||||
    async synchronizeSchema() {
 | 
			
		||||
        console.log('[DB Sync] Starting schema synchronization...');
 | 
			
		||||
        const tablesInDb = this.getTablesFromDb();
 | 
			
		||||
 | 
			
		||||
@ -132,8 +132,8 @@ class SQLiteClient {
 | 
			
		||||
        console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    initTables() {
 | 
			
		||||
        this.synchronizeSchema();
 | 
			
		||||
    async initTables() {
 | 
			
		||||
        await this.synchronizeSchema();
 | 
			
		||||
        this.initDefaultData();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -166,21 +166,6 @@ class SQLiteClient {
 | 
			
		||||
        console.log('Default data initialized.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    markPermissionsAsCompleted() {
 | 
			
		||||
        return this.query(
 | 
			
		||||
            'INSERT OR REPLACE INTO system_settings (key, value) VALUES (?, ?)',
 | 
			
		||||
            ['permissions_completed', 'true']
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    checkPermissionsCompleted() {
 | 
			
		||||
        const result = this.query(
 | 
			
		||||
            'SELECT value FROM system_settings WHERE key = ?',
 | 
			
		||||
            ['permissions_completed']
 | 
			
		||||
        );
 | 
			
		||||
        return result.length > 0 && result[0].value === 'true';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    close() {
 | 
			
		||||
        if (this.db) {
 | 
			
		||||
            try {
 | 
			
		||||
 | 
			
		||||
@ -44,10 +44,6 @@ async function getModelSettings() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function validateAndSaveKey(provider, key) {
 | 
			
		||||
    return modelStateService.handleValidateKey(provider, key);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function clearApiKey(provider) {
 | 
			
		||||
    const success = await modelStateService.handleRemoveApiKey(provider);
 | 
			
		||||
    return { success };
 | 
			
		||||
@ -461,7 +457,6 @@ module.exports = {
 | 
			
		||||
    setAutoUpdateSetting,
 | 
			
		||||
    // Model settings facade
 | 
			
		||||
    getModelSettings,
 | 
			
		||||
    validateAndSaveKey,
 | 
			
		||||
    clearApiKey,
 | 
			
		||||
    setSelectedModel,
 | 
			
		||||
    // Ollama facade
 | 
			
		||||
 | 
			
		||||
@ -69,6 +69,7 @@ contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
  headerController: {
 | 
			
		||||
    // State Management
 | 
			
		||||
    sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
 | 
			
		||||
    reInitializeModelState: () => ipcRenderer.invoke('model:re-initialize-state'),
 | 
			
		||||
    
 | 
			
		||||
    // Window Management
 | 
			
		||||
    resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
 | 
			
		||||
@ -83,7 +84,9 @@ contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
    onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
 | 
			
		||||
    removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
 | 
			
		||||
    onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', callback),
 | 
			
		||||
    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback)
 | 
			
		||||
    removeOnForceShowApiKeyHeader: (callback) => ipcRenderer.removeListener('force-show-apikey-header', callback),
 | 
			
		||||
    onForceShowPermissionHeader: (callback) => ipcRenderer.on('force-show-permission-header', callback),
 | 
			
		||||
    removeOnForceShowPermissionHeader: (callback) => ipcRenderer.removeListener('force-show-permission-header', callback)
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/app/MainHeader.js
 | 
			
		||||
@ -114,7 +117,9 @@ contextBridge.exposeInMainWorld('api', {
 | 
			
		||||
    checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
 | 
			
		||||
    requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
 | 
			
		||||
    openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
 | 
			
		||||
    markPermissionsCompleted: () => ipcRenderer.invoke('mark-permissions-completed')
 | 
			
		||||
    markKeychainCompleted: () => ipcRenderer.invoke('mark-keychain-completed'),
 | 
			
		||||
    checkKeychainCompleted: (uid) => ipcRenderer.invoke('check-keychain-completed', uid),
 | 
			
		||||
    initializeEncryptionKey: () => ipcRenderer.invoke('initialize-encryption-key') // New for keychain
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // src/ui/app/PickleGlassApp.js
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,13 @@ class HeaderTransitionManager {
 | 
			
		||||
                console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
 | 
			
		||||
            } else if (type === 'permission') {
 | 
			
		||||
                this.permissionHeader = document.createElement('permission-setup');
 | 
			
		||||
                this.permissionHeader.continueCallback = () => this.transitionToMainHeader();
 | 
			
		||||
                this.permissionHeader.continueCallback = async () => {
 | 
			
		||||
                    if (window.api && window.api.headerController) {
 | 
			
		||||
                        console.log('[HeaderController] Re-initializing model state after permission grant...');
 | 
			
		||||
                        await window.api.headerController.reInitializeModelState();
 | 
			
		||||
                    }
 | 
			
		||||
                    this.transitionToMainHeader();
 | 
			
		||||
                };
 | 
			
		||||
                this.headerContainer.appendChild(this.permissionHeader);
 | 
			
		||||
            } else {
 | 
			
		||||
                this.mainHeader = document.createElement('main-header');
 | 
			
		||||
@ -121,19 +127,15 @@ class HeaderTransitionManager {
 | 
			
		||||
        const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
 | 
			
		||||
 | 
			
		||||
        if (isConfigured) {
 | 
			
		||||
            const { isLoggedIn } = userState;
 | 
			
		||||
            if (isLoggedIn) {
 | 
			
		||||
                const permissionResult = await this.checkPermissions();
 | 
			
		||||
                if (permissionResult.success) {
 | 
			
		||||
                    this.transitionToMainHeader();
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.transitionToPermissionHeader();
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
            // If providers are configured, always check permissions regardless of login state.
 | 
			
		||||
            const permissionResult = await this.checkPermissions();
 | 
			
		||||
            if (permissionResult.success) {
 | 
			
		||||
                this.transitionToMainHeader();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.transitionToPermissionHeader();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // 프로바이더가 설정되지 않았으면 WelcomeHeader 먼저 표시
 | 
			
		||||
            // If no providers are configured, show the welcome header to prompt for setup.
 | 
			
		||||
            await this._resizeForWelcome();
 | 
			
		||||
            this.ensureHeader('welcome');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -258,6 +258,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    static properties = {
 | 
			
		||||
        microphoneGranted: { type: String },
 | 
			
		||||
        screenGranted: { type: String },
 | 
			
		||||
        keychainGranted: { type: String },
 | 
			
		||||
        isChecking: { type: String },
 | 
			
		||||
        continueCallback: { type: Function }
 | 
			
		||||
    };
 | 
			
		||||
@ -266,6 +267,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        super();
 | 
			
		||||
        this.microphoneGranted = 'unknown';
 | 
			
		||||
        this.screenGranted = 'unknown';
 | 
			
		||||
        this.keychainGranted = 'unknown';
 | 
			
		||||
        this.isChecking = false;
 | 
			
		||||
        this.continueCallback = null;
 | 
			
		||||
    }
 | 
			
		||||
@ -298,12 +300,14 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
            
 | 
			
		||||
            const prevMic = this.microphoneGranted;
 | 
			
		||||
            const prevScreen = this.screenGranted;
 | 
			
		||||
            const prevKeychain = this.keychainGranted;
 | 
			
		||||
            
 | 
			
		||||
            this.microphoneGranted = permissions.microphone;
 | 
			
		||||
            this.screenGranted = permissions.screen;
 | 
			
		||||
            this.keychainGranted = permissions.keychain;
 | 
			
		||||
            
 | 
			
		||||
            // if permissions changed == UI update
 | 
			
		||||
            if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted) {
 | 
			
		||||
            if (prevMic !== this.microphoneGranted || prevScreen !== this.screenGranted || prevKeychain !== this.keychainGranted) {
 | 
			
		||||
                console.log('[PermissionHeader] Permission status changed, updating UI');
 | 
			
		||||
                this.requestUpdate();
 | 
			
		||||
            }
 | 
			
		||||
@ -311,6 +315,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
            // if all permissions granted == automatically continue
 | 
			
		||||
            if (this.microphoneGranted === 'granted' && 
 | 
			
		||||
                this.screenGranted === 'granted' && 
 | 
			
		||||
                this.keychainGranted === 'granted' && 
 | 
			
		||||
                this.continueCallback) {
 | 
			
		||||
                console.log('[PermissionHeader] All permissions granted, proceeding automatically');
 | 
			
		||||
                setTimeout(() => this.handleContinue(), 500);
 | 
			
		||||
@ -381,17 +386,36 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleKeychainClick() {
 | 
			
		||||
        if (!window.api || this.keychainGranted === 'granted') return;
 | 
			
		||||
        
 | 
			
		||||
        console.log('[PermissionHeader] Requesting keychain permission...');
 | 
			
		||||
        
 | 
			
		||||
        try {
 | 
			
		||||
            // Trigger initializeKey to prompt for keychain access
 | 
			
		||||
            // Assuming encryptionService is accessible or via API
 | 
			
		||||
            await window.api.permissionHeader.initializeEncryptionKey(); // New IPC handler needed
 | 
			
		||||
            
 | 
			
		||||
            // After success, update status
 | 
			
		||||
            this.keychainGranted = 'granted';
 | 
			
		||||
            this.requestUpdate();
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error('[PermissionHeader] Error requesting keychain permission:', error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async handleContinue() {
 | 
			
		||||
        if (this.continueCallback && 
 | 
			
		||||
            this.microphoneGranted === 'granted' && 
 | 
			
		||||
            this.screenGranted === 'granted') {
 | 
			
		||||
            this.screenGranted === 'granted' && 
 | 
			
		||||
            this.keychainGranted === 'granted') {
 | 
			
		||||
            // Mark permissions as completed
 | 
			
		||||
            if (window.api) {
 | 
			
		||||
                try {
 | 
			
		||||
                    await window.api.permissionHeader.markPermissionsCompleted();
 | 
			
		||||
                    console.log('[PermissionHeader] Marked permissions as completed');
 | 
			
		||||
                    await window.api.permissionHeader.markKeychainCompleted();
 | 
			
		||||
                    console.log('[PermissionHeader] Marked keychain as completed');
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('[PermissionHeader] Error marking permissions as completed:', error);
 | 
			
		||||
                    console.error('[PermissionHeader] Error marking keychain as completed:', error);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
@ -407,7 +431,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    render() {
 | 
			
		||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted';
 | 
			
		||||
        const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && this.keychainGranted === 'granted';
 | 
			
		||||
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="container">
 | 
			
		||||
@ -419,7 +443,7 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
                <h1 class="title">Permission Setup Required</h1>
 | 
			
		||||
 | 
			
		||||
                <div class="form-content">
 | 
			
		||||
                    <div class="subtitle">Grant access to microphone and screen recording to continue</div>
 | 
			
		||||
                    <div class="subtitle">Grant access to microphone, screen recording and keychain to continue</div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div class="permission-status">
 | 
			
		||||
                        <div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
 | 
			
		||||
@ -449,6 +473,20 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
                                <span>Screen Recording</span>
 | 
			
		||||
                            `}
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div class="permission-item ${this.keychainGranted === 'granted' ? 'granted' : ''}">
 | 
			
		||||
                            ${this.keychainGranted === 'granted' ? html`
 | 
			
		||||
                                <svg class="check-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                    <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                <span>Keychain ✓</span>
 | 
			
		||||
                            ` : html`
 | 
			
		||||
                                <svg class="permission-icon" viewBox="0 0 20 20" fill="currentColor">
 | 
			
		||||
                                    <path fill-rule="evenodd" d="M18 8a6 6 0 01-7.744 5.668l-1.649 1.652c-.63.63-1.706.19-1.706-.742V12.18a.75.75 0 00-1.5 0v2.696c0 .932-1.075 1.372-1.706.742l-1.649-1.652A6 6 0 112 8zm-4 0a.75.75 0 00.75-.75A3.75 3.75 0 018.25 4a.75.75 0 000 1.5 2.25 2.25 0 012.25 2.25.75.75 0 00.75.75z" clip-rule="evenodd" />
 | 
			
		||||
                                </svg>
 | 
			
		||||
                                <span>Keychain Access</span>
 | 
			
		||||
                            `}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    ${this.microphoneGranted !== 'granted' ? html`
 | 
			
		||||
@ -469,6 +507,16 @@ export class PermissionHeader extends LitElement {
 | 
			
		||||
                        </button>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
 | 
			
		||||
                    ${this.keychainGranted !== 'granted' ? html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="action-button" 
 | 
			
		||||
                            @click=${this.handleKeychainClick}
 | 
			
		||||
                        >
 | 
			
		||||
                            Grant Keychain Access
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <div class="subtitle">System prompt will appear. Select 'Always Allow' for seamless access.</div>
 | 
			
		||||
                    ` : ''}
 | 
			
		||||
 | 
			
		||||
                    ${allGranted ? html`
 | 
			
		||||
                        <button 
 | 
			
		||||
                            class="continue-button" 
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user