diff --git a/package.json b/package.json index c29b53d..42c1f83 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "asar": true, "win": { "icon": "build/icon.ico", + "requestedExecutionLevel": "highestAvailable", "target": [ "nsis" ] @@ -101,7 +102,7 @@ "installerIcon": "build/icon.ico", "uninstallerIcon": "build/icon.ico", "installerHeaderIcon": "build/icon.ico", - "perMachine": false, + "perMachine": true, "multiLanguageInstaller": true, "installerLanguages": [ "tr_TR", diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 9243429..7105b50 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -1515,4 +1515,11 @@ export function registerIpcHandlers(): void { return '' } }) + + ipcMain.handle('app:get-permissions-status', () => { + return { + hasError: configService.hasPermissionError, + path: configService.failedPath + } + }) } diff --git a/src/main/services/ConfigService.ts b/src/main/services/ConfigService.ts index 43fe1c4..07a4912 100644 --- a/src/main/services/ConfigService.ts +++ b/src/main/services/ConfigService.ts @@ -21,25 +21,51 @@ export class ConfigService { mariaDbTuning: {} as Record } + public hasPermissionError = false + public failedPath = '' + constructor() { this.templateDir = path.join(app.getAppPath(), 'config') const base = this.getDataPath() this.configDir = path.join(base, 'generated_configs') this.settingsPath = path.join(base, 'settings.json') + console.log(`[ConfigService] Base data path: ${base}`) + console.log(`[ConfigService] Config dir: ${this.configDir}`) + // Ensure directories exist + if (!fs.existsSync(base)) { + try { + fs.mkdirSync(base, { recursive: true }) + } catch (e: any) { + if (e.code === 'EPERM' || e.code === 'EACCES') { + this.hasPermissionError = true + this.failedPath = base + } + console.error(`Failed to create base directory (${base}):`, e) + } + } + if (!fs.existsSync(this.configDir)) { try { fs.mkdirSync(this.configDir, { recursive: true }) - } catch (e) { - console.error('Failed to create config directory:', e) + } catch (e: any) { + if (e.code === 'EPERM' || e.code === 'EACCES') { + this.hasPermissionError = true + this.failedPath = this.configDir + } + console.error(`Failed to create config directory (${this.configDir}):`, e) } } if (!fs.existsSync(this.settingsPath)) { try { fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2)) - } catch (e) { + } catch (e: any) { + if (e.code === 'EPERM' || e.code === 'EACCES') { + this.hasPermissionError = true + this.failedPath = this.settingsPath + } console.error('Failed to create settings file:', e) } } @@ -51,27 +77,33 @@ export class ConfigService { * If not writable (e.g. Program Files), falls back to app.getPath('userData'). */ getDataPath(): string { - if (!app.isPackaged) { + const exeDir = path.dirname(app.getPath('exe')) + const isAsar = app.getAppPath().includes('app.asar') + + // If not packaged and not running from asar, we are in development + if (!app.isPackaged && !isAsar) { return process.cwd() } - const exeDir = path.dirname(app.getPath('exe')) - - // Check if we are in Program Files - const isProgramFiles = exeDir.toLowerCase().includes('program files') - - if (isProgramFiles) { + // We are in production/packaged mode. + // Priority 1: Do not even try to use Program Files if we have alternatives. + const winProgramFiles = (process.env.ProgramFiles || 'C:\\Program Files').toLowerCase() + const winProgramFilesX86 = (process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)').toLowerCase() + const currentExeDir = exeDir.toLowerCase() + + if (currentExeDir.startsWith(winProgramFiles) || currentExeDir.startsWith(winProgramFilesX86)) { return app.getPath('userData') } - // Check for write access + // Priority 2: Check for actual write access (Portable Mode check) try { - const testFile = path.join(exeDir, '.write_test') + const testFile = path.join(exeDir, `.write_test_${Date.now()}`) fs.writeFileSync(testFile, 'test') fs.unlinkSync(testFile) return exeDir // Writable (Portable Mode) } catch (e) { - return app.getPath('userData') // Not writable, use AppData + // Not writable, use AppData + return app.getPath('userData') } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 582a577..eb77c63 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -57,6 +57,7 @@ const api = { electronAPI.ipcRenderer.on('update-progress', listener) return () => electronAPI.ipcRenderer.removeListener('update-progress', listener) }, + getPermissionsStatus: () => electronAPI.ipcRenderer.invoke('app:get-permissions-status'), invoke: (channel: string, ...args: any[]) => electronAPI.ipcRenderer.invoke(channel, ...args) } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index bef14c3..d929c34 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -318,6 +318,23 @@ function App(): JSX.Element { } } + const checkPermissions = async () => { + if (window.api && window.api.getPermissionsStatus) { + const status = await window.api.getPermissionsStatus() + if (status.hasError) { + Swal.fire({ + title: t('common.warning'), + text: t('common.admin_required'), + icon: 'warning', + confirmButtonText: t('common.ok'), + confirmButtonColor: '#3085d6', + background: '#1a1d21', + color: '#fff' + }) + } + } + } + fetchStatus() fetchProjects() fetchPhpVersions() @@ -327,6 +344,7 @@ function App(): JSX.Element { fetchSettings() fetchCliConfig() fetchPmaStatus() + checkPermissions() const removeUpdateStatusListener = window.api.onUpdateStatus ? window.api.onUpdateStatus((st: string, info: any) => { setUpdateStatus(st) diff --git a/src/renderer/src/locales/de.json b/src/renderer/src/locales/de.json index 5fa863a..51c0fb0 100644 --- a/src/renderer/src/locales/de.json +++ b/src/renderer/src/locales/de.json @@ -45,7 +45,9 @@ "browse": "Durchsuchen", "reset_confirm_title": "Sind Sie sicher?", "reset_confirm_text": "Möchten Sie die {{service}}-Daten zurücksetzen? Dieser Vorgang löscht alle Daten und führt eine Neuinstallation durch.", - "reset_btn": "Ja, Zurücksetzen" + "reset_btn": "Ja, Zurücksetzen", + "status": "Status", + "admin_required": "Bitte versuchen Sie, die Anwendung als Administrator auszuführen." }, "dashboard": { "title": "Trunçgil Multi-PHP Server", diff --git a/src/renderer/src/locales/en.json b/src/renderer/src/locales/en.json index a3b0779..2ad342f 100644 --- a/src/renderer/src/locales/en.json +++ b/src/renderer/src/locales/en.json @@ -51,7 +51,9 @@ "reset_confirm_text": "Do you want to reset {{service}} data? This operation deletes all data and performs a clean installation.", "reset_btn": "Yes, Reset", "operating": "Operation in progress...", - "please_wait": "Please wait..." + "please_wait": "Please wait...", + "status": "Status", + "admin_required": "Please try running the application as an administrator." }, "dashboard": { "title": "Dashboard", diff --git a/src/renderer/src/locales/ru.json b/src/renderer/src/locales/ru.json index dee8495..03c69ab 100644 --- a/src/renderer/src/locales/ru.json +++ b/src/renderer/src/locales/ru.json @@ -52,7 +52,8 @@ "reset_btn": "Да, сбросить", "operating": "Операция выполняется...", "please_wait": "Пожалуйста, подождите...", - "status": "Статус" + "status": "Статус", + "admin_required": "Пожалуйста, попробуйте запустить приложение от имени администратора." }, "dashboard": { "title": "Панель управления", diff --git a/src/renderer/src/locales/tr.json b/src/renderer/src/locales/tr.json index 3261982..836b3a3 100644 --- a/src/renderer/src/locales/tr.json +++ b/src/renderer/src/locales/tr.json @@ -53,7 +53,8 @@ "reset_btn": "Evet, Sıfırla", "operating": "İşlem devam ediyor...", "please_wait": "Lütfen bekleyin...", - "status": "Durum" + "status": "Durum", + "admin_required": "Lütfen uygulamayı yönetici olarak çalıştırmayı deneyin." }, "dashboard": { "title": "Özet",