diff --git a/src/bridge/featureBridge.js b/src/bridge/featureBridge.js index 4dde899..d4571b4 100644 --- a/src/bridge/featureBridge.js +++ b/src/bridge/featureBridge.js @@ -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.'); }, diff --git a/src/features/common/config/schema.js b/src/features/common/config/schema.js index ad5c3b6..628679f 100644 --- a/src/features/common/config/schema.js +++ b/src/features/common/config/schema.js @@ -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' } + ] } }; diff --git a/src/features/common/repositories/permission/index.js b/src/features/common/repositories/permission/index.js index aef30f1..9888bc4 100644 --- a/src/features/common/repositories/permission/index.js +++ b/src/features/common/repositories/permission/index.js @@ -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), }; \ No newline at end of file diff --git a/src/features/common/repositories/permission/sqlite.repository.js b/src/features/common/repositories/permission/sqlite.repository.js index 1643395..09f62ad 100644 --- a/src/features/common/repositories/permission/sqlite.repository.js +++ b/src/features/common/repositories/permission/sqlite.repository.js @@ -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 }; \ No newline at end of file diff --git a/src/features/common/repositories/providerSettings/index.js b/src/features/common/repositories/providerSettings/index.js index d4fb384..93d10af 100644 --- a/src/features/common/repositories/providerSettings/index.js +++ b/src/features/common/repositories/providerSettings/index.js @@ -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); } }; diff --git a/src/features/common/repositories/providerSettings/sqlite.repository.js b/src/features/common/repositories/providerSettings/sqlite.repository.js index 1967890..66dcf19 100644 --- a/src/features/common/repositories/providerSettings/sqlite.repository.js +++ b/src/features/common/repositories/providerSettings/sqlite.repository.js @@ -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 }; \ No newline at end of file diff --git a/src/features/common/services/authService.js b/src/features/common/services/authService.js index face151..9d821a1 100644 --- a/src/features/common/services/authService.js +++ b/src/features/common/services/authService.js @@ -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; } diff --git a/src/features/common/services/databaseInitializer.js b/src/features/common/services/databaseInitializer.js index 70cff75..59285a7 100644 --- a/src/features/common/services/databaseInitializer.js +++ b/src/features/common/services/databaseInitializer.js @@ -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(); diff --git a/src/features/common/services/encryptionService.js b/src/features/common/services/encryptionService.js index 7b3d419..68fc224 100644 --- a/src/features/common/services/encryptionService.js +++ b/src/features/common/services/encryptionService.js @@ -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) { @@ -55,7 +60,17 @@ async function initializeKey(userId) { sessionKey = crypto.randomBytes(32).toString('hex'); } } - + + // 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; } } diff --git a/src/features/common/services/modelStateService.js b/src/features/common/services/modelStateService.js index c331958..98a9b7a 100644 --- a/src/features/common/services/modelStateService.js +++ b/src/features/common/services/modelStateService.js @@ -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; } @@ -193,9 +219,19 @@ 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(); diff --git a/src/features/common/services/permissionService.js b/src/features/common/services/permissionService.js index d94ac04..7d3260b 100644 --- a/src/features/common/services/permissionService.js +++ b/src/features/common/services/permissionService.js @@ -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; } } diff --git a/src/features/common/services/sqliteClient.js b/src/features/common/services/sqliteClient.js index c49d0b2..08c8c9d 100644 --- a/src/features/common/services/sqliteClient.js +++ b/src/features/common/services/sqliteClient.js @@ -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 { diff --git a/src/features/settings/settingsService.js b/src/features/settings/settingsService.js index da68a3a..906a6c4 100644 --- a/src/features/settings/settingsService.js +++ b/src/features/settings/settingsService.js @@ -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 diff --git a/src/preload.js b/src/preload.js index 6121c84..0e3710a 100644 --- a/src/preload.js +++ b/src/preload.js @@ -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 diff --git a/src/ui/app/HeaderController.js b/src/ui/app/HeaderController.js index 336f6a9..47a18d1 100644 --- a/src/ui/app/HeaderController.js +++ b/src/ui/app/HeaderController.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'); } diff --git a/src/ui/app/PermissionHeader.js b/src/ui/app/PermissionHeader.js index d5761b1..b7b7b53 100644 --- a/src/ui/app/PermissionHeader.js +++ b/src/ui/app/PermissionHeader.js @@ -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`