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 askService = require('../features/ask/askService');
|
||||||
const listenService = require('../features/listen/listenService');
|
const listenService = require('../features/listen/listenService');
|
||||||
const permissionService = require('../features/common/services/permissionService');
|
const permissionService = require('../features/common/services/permissionService');
|
||||||
|
const encryptionService = require('../features/common/services/encryptionService');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Renderer로부터의 요청을 수신하고 서비스로 전달
|
// Renderer로부터의 요청을 수신하고 서비스로 전달
|
||||||
@ -20,7 +21,6 @@ module.exports = {
|
|||||||
ipcMain.handle('settings:get-auto-update', async () => await settingsService.getAutoUpdateSetting());
|
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:set-auto-update', async (event, isEnabled) => await settingsService.setAutoUpdateSetting(isEnabled));
|
||||||
ipcMain.handle('settings:get-model-settings', async () => await settingsService.getModelSettings());
|
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: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));
|
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('check-system-permissions', async () => await permissionService.checkSystemPermissions());
|
||||||
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
|
ipcMain.handle('request-microphone-permission', async () => await permissionService.requestMicrophonePermission());
|
||||||
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
|
ipcMain.handle('open-system-preferences', async (event, section) => await permissionService.openSystemPreferences(section));
|
||||||
ipcMain.handle('mark-permissions-completed', async () => await permissionService.markPermissionsAsCompleted());
|
ipcMain.handle('mark-keychain-completed', async () => await permissionService.markKeychainCompleted());
|
||||||
ipcMain.handle('check-permissions-completed', async () => await permissionService.checkPermissionsCompleted());
|
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
|
// User/Auth
|
||||||
ipcMain.handle('get-current-user', () => authService.getCurrentUser());
|
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:get-available-models', (e, { type }) => modelStateService.getAvailableModels(type));
|
||||||
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
ipcMain.handle('model:are-providers-configured', () => modelStateService.areProvidersConfigured());
|
||||||
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
ipcMain.handle('model:get-provider-config', () => modelStateService.getProviderConfig());
|
||||||
|
ipcMain.handle('model:re-initialize-state', () => modelStateService.initialize());
|
||||||
|
|
||||||
console.log('[FeatureBridge] Initialized with all feature handlers.');
|
console.log('[FeatureBridge] Initialized with all feature handlers.');
|
||||||
},
|
},
|
||||||
|
@ -117,6 +117,12 @@ const LATEST_SCHEMA = {
|
|||||||
{ name: 'accelerator', type: 'TEXT NOT NULL' },
|
{ name: 'accelerator', type: 'TEXT NOT NULL' },
|
||||||
{ name: 'created_at', type: 'INTEGER' }
|
{ 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 = {
|
module.exports = {
|
||||||
markPermissionsAsCompleted: (...args) => getRepository().markPermissionsAsCompleted(...args),
|
markKeychainCompleted: (...args) => getRepository().markKeychainCompleted(...args),
|
||||||
checkPermissionsCompleted: (...args) => getRepository().checkPermissionsCompleted(...args),
|
checkKeychainCompleted: (...args) => getRepository().checkKeychainCompleted(...args),
|
||||||
};
|
};
|
@ -1,14 +1,18 @@
|
|||||||
const sqliteClient = require('../../services/sqliteClient');
|
const sqliteClient = require('../../services/sqliteClient');
|
||||||
|
|
||||||
async function markPermissionsAsCompleted() {
|
function markKeychainCompleted(uid) {
|
||||||
return sqliteClient.markPermissionsAsCompleted();
|
return sqliteClient.query(
|
||||||
|
'INSERT OR REPLACE INTO permissions (uid, keychain_completed) VALUES (?, 1)',
|
||||||
|
[uid]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkPermissionsCompleted() {
|
function checkKeychainCompleted(uid) {
|
||||||
return sqliteClient.checkPermissionsCompleted();
|
const row = sqliteClient.query('SELECT keychain_completed FROM permissions WHERE uid = ?', [uid]);
|
||||||
|
return row.length > 0 && row[0].keychain_completed === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
markPermissionsAsCompleted,
|
markKeychainCompleted,
|
||||||
checkPermissionsCompleted,
|
checkKeychainCompleted
|
||||||
};
|
};
|
@ -56,6 +56,13 @@ const providerSettingsRepositoryAdapter = {
|
|||||||
const repo = getBaseRepository();
|
const repo = getBaseRepository();
|
||||||
const uid = authService.getCurrentUserId();
|
const uid = authService.getCurrentUserId();
|
||||||
return await repo.removeAllByUid(uid);
|
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;
|
const result = stmt.get(uid, provider) || null;
|
||||||
|
|
||||||
if (result && result.api_key) {
|
if (result && result.api_key) {
|
||||||
// Decrypt API key if it exists
|
|
||||||
result.api_key = encryptionService.decrypt(result.api_key);
|
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 stmt = db.prepare('SELECT * FROM provider_settings WHERE uid = ? ORDER BY provider');
|
||||||
const results = stmt.all(uid);
|
const results = stmt.all(uid);
|
||||||
|
|
||||||
// Decrypt API keys for all results
|
|
||||||
return results.map(result => {
|
return results.map(result => {
|
||||||
if (result.api_key) {
|
if (result.api_key) {
|
||||||
result.api_key = encryptionService.decrypt(result.api_key);
|
result.api_key = encryptionService.decrypt(result.api_key);
|
||||||
@ -31,12 +29,6 @@ function getAllByUid(uid) {
|
|||||||
function upsert(uid, provider, settings) {
|
function upsert(uid, provider, settings) {
|
||||||
const db = sqliteClient.getDb();
|
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)
|
// Use SQLite's UPSERT syntax (INSERT ... ON CONFLICT ... DO UPDATE)
|
||||||
const stmt = db.prepare(`
|
const stmt = db.prepare(`
|
||||||
INSERT INTO provider_settings (uid, provider, api_key, selected_llm_model, selected_stt_model, created_at, updated_at)
|
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(
|
const result = stmt.run(
|
||||||
uid,
|
uid,
|
||||||
provider,
|
provider,
|
||||||
encryptedSettings.api_key || null,
|
settings.api_key || null,
|
||||||
encryptedSettings.selected_llm_model || null,
|
settings.selected_llm_model || null,
|
||||||
encryptedSettings.selected_stt_model || null,
|
settings.selected_stt_model || null,
|
||||||
encryptedSettings.created_at || Date.now(),
|
settings.created_at || Date.now(),
|
||||||
encryptedSettings.updated_at
|
settings.updated_at
|
||||||
);
|
);
|
||||||
|
|
||||||
return { changes: result.changes };
|
return { changes: result.changes };
|
||||||
@ -75,10 +67,17 @@ function removeAllByUid(uid) {
|
|||||||
return { changes: result.changes };
|
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 = {
|
module.exports = {
|
||||||
getByProvider,
|
getByProvider,
|
||||||
getAllByUid,
|
getAllByUid,
|
||||||
upsert,
|
upsert,
|
||||||
remove,
|
remove,
|
||||||
removeAllByUid
|
removeAllByUid,
|
||||||
|
getRawApiKeysByUid
|
||||||
};
|
};
|
@ -7,6 +7,7 @@ const migrationService = require('./migrationService');
|
|||||||
const sessionRepository = require('../repositories/session');
|
const sessionRepository = require('../repositories/session');
|
||||||
const providerSettingsRepository = require('../repositories/providerSettings');
|
const providerSettingsRepository = require('../repositories/providerSettings');
|
||||||
const userModelSelectionsRepository = require('../repositories/userModelSelections');
|
const userModelSelectionsRepository = require('../repositories/userModelSelections');
|
||||||
|
const permissionService = require('./permissionService');
|
||||||
|
|
||||||
async function getVirtualKeyByEmail(email, idToken) {
|
async function getVirtualKeyByEmail(email, idToken) {
|
||||||
if (!idToken) {
|
if (!idToken) {
|
||||||
@ -43,7 +44,6 @@ class AuthService {
|
|||||||
this.isInitialized = false;
|
this.isInitialized = false;
|
||||||
|
|
||||||
// This ensures the key is ready before any login/logout state change.
|
// This ensures the key is ready before any login/logout state change.
|
||||||
encryptionService.initializeKey(this.currentUserId);
|
|
||||||
this.initializationPromise = null;
|
this.initializationPromise = null;
|
||||||
|
|
||||||
sessionRepository.setAuthService(this);
|
sessionRepository.setAuthService(this);
|
||||||
@ -69,8 +69,12 @@ class AuthService {
|
|||||||
// Clean up any zombie sessions from a previous run for this user.
|
// Clean up any zombie sessions from a previous run for this user.
|
||||||
await sessionRepository.endAllActiveSessions();
|
await sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
// ** Initialize encryption key for the logged-in user **
|
// ** 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);
|
await encryptionService.initializeKey(user.uid);
|
||||||
|
}
|
||||||
|
|
||||||
// ** Check for and run data migration for the user **
|
// ** Check for and run data migration for the user **
|
||||||
// No 'await' here, so it runs in the background without blocking startup.
|
// No 'await' here, so it runs in the background without blocking startup.
|
||||||
@ -109,9 +113,13 @@ class AuthService {
|
|||||||
// End active sessions for the local/default user as well.
|
// End active sessions for the local/default user as well.
|
||||||
await sessionRepository.endAllActiveSessions();
|
await sessionRepository.endAllActiveSessions();
|
||||||
|
|
||||||
// ** Initialize encryption key for the default/local user **
|
// ** 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);
|
await encryptionService.initializeKey(this.currentUserId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
this.broadcastUserState();
|
this.broadcastUserState();
|
||||||
|
|
||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
@ -175,7 +183,6 @@ class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getCurrentUserId() {
|
getCurrentUserId() {
|
||||||
return this.currentUserId;
|
return this.currentUserId;
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,13 @@ class DatabaseInitializer {
|
|||||||
|
|
||||||
// 최종적으로 사용될 DB 경로 (쓰기 가능한 위치)
|
// 최종적으로 사용될 DB 경로 (쓰기 가능한 위치)
|
||||||
const userDataPath = app.getPath('userData');
|
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.dbPath = path.join(userDataPath, 'pickleglass.db');
|
||||||
this.dataDir = userDataPath;
|
this.dataDir = userDataPath;
|
||||||
|
|
||||||
// 원본 DB 경로 (패키지 내 읽기 전용 위치)
|
// The original DB path (read-only location in the package)
|
||||||
this.sourceDbPath = app.isPackaged
|
this.sourceDbPath = app.isPackaged
|
||||||
? path.join(process.resourcesPath, 'data', 'pickleglass.db')
|
? path.join(process.resourcesPath, 'data', 'pickleglass.db')
|
||||||
: path.join(app.getAppPath(), 'data', 'pickleglass.db');
|
: path.join(app.getAppPath(), 'data', 'pickleglass.db');
|
||||||
@ -52,7 +55,7 @@ class DatabaseInitializer {
|
|||||||
try {
|
try {
|
||||||
this.ensureDatabaseExists();
|
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.
|
// This single call will now synchronize the schema and then init default data.
|
||||||
await sqliteClient.initTables();
|
await sqliteClient.initTables();
|
||||||
|
@ -9,6 +9,8 @@ try {
|
|||||||
keytar = null;
|
keytar = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const permissionService = require('./permissionService');
|
||||||
|
|
||||||
const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
|
const SERVICE_NAME = 'com.pickle.glass'; // A unique identifier for the app in the keychain
|
||||||
let sessionKey = null; // In-memory fallback key
|
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.');
|
throw new Error('A user ID must be provided to initialize the encryption key.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let keyRetrieved = false;
|
||||||
|
|
||||||
if (keytar) {
|
if (keytar) {
|
||||||
try {
|
try {
|
||||||
let key = await keytar.getPassword(SERVICE_NAME, userId);
|
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}.`);
|
console.log(`[EncryptionService] New key securely stored in keychain for ${userId}.`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
|
console.log(`[EncryptionService] Encryption key successfully retrieved from keychain for ${userId}.`);
|
||||||
|
keyRetrieved = true;
|
||||||
}
|
}
|
||||||
sessionKey = key;
|
sessionKey = key;
|
||||||
} catch (error) {
|
} 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) {
|
if (!sessionKey) {
|
||||||
throw new Error('Failed to initialize encryption key.');
|
throw new Error('Failed to initialize encryption key.');
|
||||||
}
|
}
|
||||||
@ -129,6 +144,7 @@ function decrypt(encryptedText) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// It's common for this to fail if the data is not encrypted (e.g., legacy data).
|
// 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.
|
// In that case, we return the original value.
|
||||||
|
console.error('[EncryptionService] Decryption failed:', error);
|
||||||
return encryptedText;
|
return encryptedText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,39 @@ const userModelSelectionsRepository = require('../repositories/userModelSelectio
|
|||||||
|
|
||||||
// Import authService directly (singleton)
|
// Import authService directly (singleton)
|
||||||
const authService = require('./authService');
|
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 {
|
class ModelStateService extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.authService = authService;
|
this.authService = authService;
|
||||||
this.store = new Store({ name: 'pickle-glass-model-state' });
|
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;
|
this.hasMigrated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,8 +220,18 @@ class ModelStateService extends EventEmitter {
|
|||||||
async _loadStateForCurrentUser() {
|
async _loadStateForCurrentUser() {
|
||||||
const userId = this.authService.getCurrentUserId();
|
const userId = this.authService.getCurrentUserId();
|
||||||
|
|
||||||
// Initialize encryption service for current user
|
// 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);
|
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
|
// Try to load from database first
|
||||||
await this._loadStateFromDatabase();
|
await this._loadStateFromDatabase();
|
||||||
@ -263,17 +299,6 @@ class ModelStateService extends EventEmitter {
|
|||||||
apiKeys: { ...this.state.apiKeys }
|
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);
|
this.store.set(`users.${userId}`, stateToSave);
|
||||||
console.log(`[ModelStateService] State saved to electron-store for user: ${userId}`);
|
console.log(`[ModelStateService] State saved to electron-store for user: ${userId}`);
|
||||||
this._logCurrentSelection();
|
this._logCurrentSelection();
|
||||||
|
@ -2,27 +2,28 @@ const { systemPreferences, shell, desktopCapturer } = require('electron');
|
|||||||
const permissionRepository = require('../repositories/permission');
|
const permissionRepository = require('../repositories/permission');
|
||||||
|
|
||||||
class PermissionService {
|
class PermissionService {
|
||||||
|
_getAuthService() {
|
||||||
|
return require('./authService');
|
||||||
|
}
|
||||||
|
|
||||||
async checkSystemPermissions() {
|
async checkSystemPermissions() {
|
||||||
const permissions = {
|
const permissions = {
|
||||||
microphone: 'unknown',
|
microphone: 'unknown',
|
||||||
screen: 'unknown',
|
screen: 'unknown',
|
||||||
|
keychain: 'unknown',
|
||||||
needsSetup: true
|
needsSetup: true
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
const micStatus = systemPreferences.getMediaAccessStatus('microphone');
|
permissions.microphone = systemPreferences.getMediaAccessStatus('microphone');
|
||||||
console.log('[Permissions] Microphone status:', micStatus);
|
permissions.screen = systemPreferences.getMediaAccessStatus('screen');
|
||||||
permissions.microphone = micStatus;
|
permissions.keychain = await this.checkKeychainCompleted(this._getAuthService().getCurrentUserId()) ? 'granted' : 'unknown';
|
||||||
|
permissions.needsSetup = permissions.microphone !== 'granted' || permissions.screen !== 'granted' || permissions.keychain !== 'granted';
|
||||||
const screenStatus = systemPreferences.getMediaAccessStatus('screen');
|
|
||||||
console.log('[Permissions] Screen status:', screenStatus);
|
|
||||||
permissions.screen = screenStatus;
|
|
||||||
|
|
||||||
permissions.needsSetup = micStatus !== 'granted' || screenStatus !== 'granted';
|
|
||||||
} else {
|
} else {
|
||||||
permissions.microphone = 'granted';
|
permissions.microphone = 'granted';
|
||||||
permissions.screen = 'granted';
|
permissions.screen = 'granted';
|
||||||
|
permissions.keychain = 'granted';
|
||||||
permissions.needsSetup = false;
|
permissions.needsSetup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ class PermissionService {
|
|||||||
return {
|
return {
|
||||||
microphone: 'unknown',
|
microphone: 'unknown',
|
||||||
screen: 'unknown',
|
screen: 'unknown',
|
||||||
|
keychain: 'unknown',
|
||||||
needsSetup: true,
|
needsSetup: true,
|
||||||
error: error.message
|
error: error.message
|
||||||
};
|
};
|
||||||
@ -92,24 +94,24 @@ class PermissionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async markPermissionsAsCompleted() {
|
async markKeychainCompleted() {
|
||||||
try {
|
try {
|
||||||
await permissionRepository.markPermissionsAsCompleted();
|
await permissionRepository.markKeychainCompleted(this._getAuthService().getCurrentUserId());
|
||||||
console.log('[Permissions] Marked permissions as completed');
|
console.log('[Permissions] Marked keychain as completed');
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} 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 };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkPermissionsCompleted() {
|
async checkKeychainCompleted(uid) {
|
||||||
try {
|
try {
|
||||||
const completed = await permissionRepository.checkPermissionsCompleted();
|
const completed = permissionRepository.checkKeychainCompleted(uid);
|
||||||
console.log('[Permissions] Permissions completed status:', completed);
|
console.log('[Permissions] Keychain completed status:', completed);
|
||||||
return completed;
|
return completed;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Permissions] Error checking permissions completed status:', error);
|
console.error('[Permissions] Error checking keychain completed status:', error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ class SQLiteClient {
|
|||||||
return `"${identifier}"`;
|
return `"${identifier}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronizeSchema() {
|
async synchronizeSchema() {
|
||||||
console.log('[DB Sync] Starting schema synchronization...');
|
console.log('[DB Sync] Starting schema synchronization...');
|
||||||
const tablesInDb = this.getTablesFromDb();
|
const tablesInDb = this.getTablesFromDb();
|
||||||
|
|
||||||
@ -132,8 +132,8 @@ class SQLiteClient {
|
|||||||
console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
|
console.log(`[DB Cleanup] Successfully deleted ${result.changes} empty sessions.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
initTables() {
|
async initTables() {
|
||||||
this.synchronizeSchema();
|
await this.synchronizeSchema();
|
||||||
this.initDefaultData();
|
this.initDefaultData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,21 +166,6 @@ class SQLiteClient {
|
|||||||
console.log('Default data initialized.');
|
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() {
|
close() {
|
||||||
if (this.db) {
|
if (this.db) {
|
||||||
try {
|
try {
|
||||||
|
@ -44,10 +44,6 @@ async function getModelSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateAndSaveKey(provider, key) {
|
|
||||||
return modelStateService.handleValidateKey(provider, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clearApiKey(provider) {
|
async function clearApiKey(provider) {
|
||||||
const success = await modelStateService.handleRemoveApiKey(provider);
|
const success = await modelStateService.handleRemoveApiKey(provider);
|
||||||
return { success };
|
return { success };
|
||||||
@ -461,7 +457,6 @@ module.exports = {
|
|||||||
setAutoUpdateSetting,
|
setAutoUpdateSetting,
|
||||||
// Model settings facade
|
// Model settings facade
|
||||||
getModelSettings,
|
getModelSettings,
|
||||||
validateAndSaveKey,
|
|
||||||
clearApiKey,
|
clearApiKey,
|
||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
// Ollama facade
|
// Ollama facade
|
||||||
|
@ -69,6 +69,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
headerController: {
|
headerController: {
|
||||||
// State Management
|
// State Management
|
||||||
sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
|
sendHeaderStateChanged: (state) => ipcRenderer.send('header-state-changed', state),
|
||||||
|
reInitializeModelState: () => ipcRenderer.invoke('model:re-initialize-state'),
|
||||||
|
|
||||||
// Window Management
|
// Window Management
|
||||||
resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
|
resizeHeaderWindow: (dimensions) => ipcRenderer.invoke('resize-header-window', dimensions),
|
||||||
@ -83,7 +84,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
|
onAuthFailed: (callback) => ipcRenderer.on('auth-failed', callback),
|
||||||
removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
|
removeOnAuthFailed: (callback) => ipcRenderer.removeListener('auth-failed', callback),
|
||||||
onForceShowApiKeyHeader: (callback) => ipcRenderer.on('force-show-apikey-header', 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
|
// src/ui/app/MainHeader.js
|
||||||
@ -114,7 +117,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
|
checkSystemPermissions: () => ipcRenderer.invoke('check-system-permissions'),
|
||||||
requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
|
requestMicrophonePermission: () => ipcRenderer.invoke('request-microphone-permission'),
|
||||||
openSystemPreferences: (preference) => ipcRenderer.invoke('open-system-preferences', preference),
|
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
|
// src/ui/app/PickleGlassApp.js
|
||||||
|
@ -48,7 +48,13 @@ class HeaderTransitionManager {
|
|||||||
console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
|
console.log('[HeaderController] ensureHeader: Header of type:', type, 'created.');
|
||||||
} else if (type === 'permission') {
|
} else if (type === 'permission') {
|
||||||
this.permissionHeader = document.createElement('permission-setup');
|
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);
|
this.headerContainer.appendChild(this.permissionHeader);
|
||||||
} else {
|
} else {
|
||||||
this.mainHeader = document.createElement('main-header');
|
this.mainHeader = document.createElement('main-header');
|
||||||
@ -121,8 +127,7 @@ class HeaderTransitionManager {
|
|||||||
const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
|
const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
|
||||||
|
|
||||||
if (isConfigured) {
|
if (isConfigured) {
|
||||||
const { isLoggedIn } = userState;
|
// If providers are configured, always check permissions regardless of login state.
|
||||||
if (isLoggedIn) {
|
|
||||||
const permissionResult = await this.checkPermissions();
|
const permissionResult = await this.checkPermissions();
|
||||||
if (permissionResult.success) {
|
if (permissionResult.success) {
|
||||||
this.transitionToMainHeader();
|
this.transitionToMainHeader();
|
||||||
@ -130,10 +135,7 @@ class HeaderTransitionManager {
|
|||||||
this.transitionToPermissionHeader();
|
this.transitionToPermissionHeader();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.transitionToMainHeader();
|
// If no providers are configured, show the welcome header to prompt for setup.
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 프로바이더가 설정되지 않았으면 WelcomeHeader 먼저 표시
|
|
||||||
await this._resizeForWelcome();
|
await this._resizeForWelcome();
|
||||||
this.ensureHeader('welcome');
|
this.ensureHeader('welcome');
|
||||||
}
|
}
|
||||||
|
@ -258,6 +258,7 @@ export class PermissionHeader extends LitElement {
|
|||||||
static properties = {
|
static properties = {
|
||||||
microphoneGranted: { type: String },
|
microphoneGranted: { type: String },
|
||||||
screenGranted: { type: String },
|
screenGranted: { type: String },
|
||||||
|
keychainGranted: { type: String },
|
||||||
isChecking: { type: String },
|
isChecking: { type: String },
|
||||||
continueCallback: { type: Function }
|
continueCallback: { type: Function }
|
||||||
};
|
};
|
||||||
@ -266,6 +267,7 @@ export class PermissionHeader extends LitElement {
|
|||||||
super();
|
super();
|
||||||
this.microphoneGranted = 'unknown';
|
this.microphoneGranted = 'unknown';
|
||||||
this.screenGranted = 'unknown';
|
this.screenGranted = 'unknown';
|
||||||
|
this.keychainGranted = 'unknown';
|
||||||
this.isChecking = false;
|
this.isChecking = false;
|
||||||
this.continueCallback = null;
|
this.continueCallback = null;
|
||||||
}
|
}
|
||||||
@ -298,12 +300,14 @@ export class PermissionHeader extends LitElement {
|
|||||||
|
|
||||||
const prevMic = this.microphoneGranted;
|
const prevMic = this.microphoneGranted;
|
||||||
const prevScreen = this.screenGranted;
|
const prevScreen = this.screenGranted;
|
||||||
|
const prevKeychain = this.keychainGranted;
|
||||||
|
|
||||||
this.microphoneGranted = permissions.microphone;
|
this.microphoneGranted = permissions.microphone;
|
||||||
this.screenGranted = permissions.screen;
|
this.screenGranted = permissions.screen;
|
||||||
|
this.keychainGranted = permissions.keychain;
|
||||||
|
|
||||||
// if permissions changed == UI update
|
// 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');
|
console.log('[PermissionHeader] Permission status changed, updating UI');
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
}
|
}
|
||||||
@ -311,6 +315,7 @@ export class PermissionHeader extends LitElement {
|
|||||||
// if all permissions granted == automatically continue
|
// if all permissions granted == automatically continue
|
||||||
if (this.microphoneGranted === 'granted' &&
|
if (this.microphoneGranted === 'granted' &&
|
||||||
this.screenGranted === 'granted' &&
|
this.screenGranted === 'granted' &&
|
||||||
|
this.keychainGranted === 'granted' &&
|
||||||
this.continueCallback) {
|
this.continueCallback) {
|
||||||
console.log('[PermissionHeader] All permissions granted, proceeding automatically');
|
console.log('[PermissionHeader] All permissions granted, proceeding automatically');
|
||||||
setTimeout(() => this.handleContinue(), 500);
|
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() {
|
async handleContinue() {
|
||||||
if (this.continueCallback &&
|
if (this.continueCallback &&
|
||||||
this.microphoneGranted === 'granted' &&
|
this.microphoneGranted === 'granted' &&
|
||||||
this.screenGranted === 'granted') {
|
this.screenGranted === 'granted' &&
|
||||||
|
this.keychainGranted === 'granted') {
|
||||||
// Mark permissions as completed
|
// Mark permissions as completed
|
||||||
if (window.api) {
|
if (window.api) {
|
||||||
try {
|
try {
|
||||||
await window.api.permissionHeader.markPermissionsCompleted();
|
await window.api.permissionHeader.markKeychainCompleted();
|
||||||
console.log('[PermissionHeader] Marked permissions as completed');
|
console.log('[PermissionHeader] Marked keychain as completed');
|
||||||
} catch (error) {
|
} 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() {
|
render() {
|
||||||
const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted';
|
const allGranted = this.microphoneGranted === 'granted' && this.screenGranted === 'granted' && this.keychainGranted === 'granted';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@ -419,7 +443,7 @@ export class PermissionHeader extends LitElement {
|
|||||||
<h1 class="title">Permission Setup Required</h1>
|
<h1 class="title">Permission Setup Required</h1>
|
||||||
|
|
||||||
<div class="form-content">
|
<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-status">
|
||||||
<div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
|
<div class="permission-item ${this.microphoneGranted === 'granted' ? 'granted' : ''}">
|
||||||
@ -449,6 +473,20 @@ export class PermissionHeader extends LitElement {
|
|||||||
<span>Screen Recording</span>
|
<span>Screen Recording</span>
|
||||||
`}
|
`}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
${this.microphoneGranted !== 'granted' ? html`
|
${this.microphoneGranted !== 'granted' ? html`
|
||||||
@ -469,6 +507,16 @@ export class PermissionHeader extends LitElement {
|
|||||||
</button>
|
</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`
|
${allGranted ? html`
|
||||||
<button
|
<button
|
||||||
class="continue-button"
|
class="continue-button"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user