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 **
|
||||
// ** 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,9 +113,13 @@ class AuthService {
|
||||
// End active sessions for the local/default user as well.
|
||||
await sessionRepository.endAllActiveSessions();
|
||||
|
||||
// ** Initialize encryption key for the default/local user **
|
||||
// ** Initialize encryption key for the default/local user 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();
|
||||
|
||||
if (!this.isInitialized) {
|
||||
@ -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
|
||||
// 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,8 +127,7 @@ class HeaderTransitionManager {
|
||||
const isConfigured = await window.api.apiKeyHeader.areProvidersConfigured();
|
||||
|
||||
if (isConfigured) {
|
||||
const { isLoggedIn } = userState;
|
||||
if (isLoggedIn) {
|
||||
// If providers are configured, always check permissions regardless of login state.
|
||||
const permissionResult = await this.checkPermissions();
|
||||
if (permissionResult.success) {
|
||||
this.transitionToMainHeader();
|
||||
@ -130,10 +135,7 @@ class HeaderTransitionManager {
|
||||
this.transitionToPermissionHeader();
|
||||
}
|
||||
} else {
|
||||
this.transitionToMainHeader();
|
||||
}
|
||||
} 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