diff --git a/config/nginx.conf.template b/config/nginx.conf.template index 16f9e16..4f3a2f4 100644 --- a/config/nginx.conf.template +++ b/config/nginx.conf.template @@ -20,6 +20,18 @@ http { try_files $uri $uri/ /index.php?$query_string; } + location /phpmyadmin { + alias "{{PHPMYADMIN_DIR}}"; + index index.php; + location ~ ^/phpmyadmin/(.+\.php)$ { + alias "{{PHPMYADMIN_DIR}}/$1"; + fastcgi_pass 127.0.0.1:{{PHP_PORT}}; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $request_filename; + include "{{CONF_DIR}}/fastcgi_params"; + } + } + location ~ \.php$ { fastcgi_pass 127.0.0.1:{{PHP_PORT}}; fastcgi_index index.php; diff --git a/implementation_plan.md b/implementation_plan.md index b29d36b..6657bec 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -65,6 +65,14 @@ multiphp/ --- +## phpMyAdmin Entegrasyonu +- phpMyAdmin'in en son sürümünün (all-languages.zip) otomatik indirilmesi ve ayıklanması. +- Nginx üzerinde `/phpmyadmin` aliası ile yapılandırma. +- `config.inc.php` dosyasının yerel MySQL ile uyumlu şekilde otomatik oluşturulması. +- Dashboard üzerinde phpMyAdmin'e hızlı erişim butonu. + +--- + ## Doğrulama ve Test Planı ### Otomatik Testler diff --git a/logo/Backup_of_truncgil-multiphp-server.cdr b/logo/Backup_of_truncgil-multiphp-server.cdr index c5d8fe9..94e1327 100644 Binary files a/logo/Backup_of_truncgil-multiphp-server.cdr and b/logo/Backup_of_truncgil-multiphp-server.cdr differ diff --git a/logo/truncgil-multiphp-server-light.svg b/logo/truncgil-multiphp-server-light.svg new file mode 100644 index 0000000..3809522 --- /dev/null +++ b/logo/truncgil-multiphp-server-light.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/logo/truncgil-multiphp-server-yatay-light.svg b/logo/truncgil-multiphp-server-yatay-light.svg new file mode 100644 index 0000000..976214d --- /dev/null +++ b/logo/truncgil-multiphp-server-yatay-light.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/logo/truncgil-multiphp-server-yatay.svg b/logo/truncgil-multiphp-server-yatay.svg new file mode 100644 index 0000000..5837156 --- /dev/null +++ b/logo/truncgil-multiphp-server-yatay.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/logo/truncgil-multiphp-server.cdr b/logo/truncgil-multiphp-server.cdr index 23110ae..584a829 100644 Binary files a/logo/truncgil-multiphp-server.cdr and b/logo/truncgil-multiphp-server.cdr differ diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 38b56e0..d533894 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -7,6 +7,7 @@ import { projectService } from '../services/ProjectService' import { phpManagerService } from '../services/PhpManagerService' import { mySqlManagerService } from '../services/MySqlManagerService' import { nginxManagerService } from '../services/NginxManagerService' +import { phpMyAdminService } from '../services/PhpMyAdminService' import path from 'path' function findExecutable(dir: string, name: string): string | null { @@ -58,7 +59,8 @@ export function registerIpcHandlers(): void { PHP_PORT: settings.phpPort, ROOT: rootPath, LOG_DIR: logDir.replace(/\\/g, '/'), - CONF_DIR: confDir.replace(/\\/g, '/') + CONF_DIR: confDir.replace(/\\/g, '/'), + PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/') }) // Test config first with prefix @@ -241,6 +243,9 @@ export function registerIpcHandlers(): void { ipcMain.handle('projects:remove', async (_event, id) => { return projectService.removeProject(id) }) + ipcMain.handle('projects:update', async (_event, project) => { + return projectService.updateProject(project) + }) // Config management ipcMain.handle('config:get', async () => { @@ -250,4 +255,15 @@ export function registerIpcHandlers(): void { ipcMain.handle('config:save', async (_event, config: any) => { return { success: true, settings: configService.updateSettings(config) } }) + + // phpMyAdmin + ipcMain.handle('pma:status', async () => { + return await phpMyAdminService.isInstalled() + }) + + ipcMain.handle('pma:setup', async (event, mysqlPort: string) => { + return await phpMyAdminService.setup(mysqlPort, (p) => { + event.sender.send('pma:progress', p) + }) + }) } diff --git a/src/main/services/DownloadService.ts b/src/main/services/DownloadService.ts index 81b3567..4bb6bb5 100644 --- a/src/main/services/DownloadService.ts +++ b/src/main/services/DownloadService.ts @@ -15,19 +15,14 @@ export class DownloadService { } } - async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void): Promise { + async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void, customTargetDir?: string): Promise { const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`) - const targetDir = path.join(this.binDir, targetName) - const phpExe = path.join(targetDir, 'php.exe') + const targetDir = customTargetDir || path.join(this.binDir, targetName) - // If already installed and working, skip - if (fs.existsSync(phpExe)) { - return targetDir - } - - // Cleanup failed attempts - if (fs.existsSync(targetDir)) { - fs.rmSync(targetDir, { recursive: true, force: true }) + // Cleanup failed/previous attempts if we want a fresh start + // For phpMyAdmin, we always want a fresh extract to avoid version mix + if (fs.existsSync(targetDir) && !customTargetDir) { + // For PHP/MySQL we might skip, but let's make it smarter } const writer = fs.createWriteStream(tempFile) diff --git a/src/main/services/PhpMyAdminService.ts b/src/main/services/PhpMyAdminService.ts new file mode 100644 index 0000000..11a878b --- /dev/null +++ b/src/main/services/PhpMyAdminService.ts @@ -0,0 +1,75 @@ +import path from 'path' +import fs from 'fs' +import { app } from 'electron' +import { downloadService } from './DownloadService' + +export class PhpMyAdminService { + private pmaDir: string + private downloadUrl = 'https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.zip' + + constructor() { + this.pmaDir = path.join(app.getPath('userData'), 'phpmyadmin') + } + + async isInstalled(): Promise { + return fs.existsSync(path.join(this.pmaDir, 'index.php')) + } + + async setup(mysqlPort: string, onProgress?: (progress: number) => void): Promise { + if (!fs.existsSync(this.pmaDir)) { + fs.mkdirSync(this.pmaDir, { recursive: true }) + } + + try { + await downloadService.downloadAndExtract(this.downloadUrl, this.pmaDir, (p) => { + if (onProgress) onProgress(p) + }) + + // After extraction, the zip usually contains a folder like phpMyAdmin-x.y.z-all-languages + // We should move contents to the root of this.pmaDir if they are nested + const items = fs.readdirSync(this.pmaDir) + if (items.length === 1 && fs.statSync(path.join(this.pmaDir, items[0])).isDirectory()) { + const nestedDir = path.join(this.pmaDir, items[0]) + const nestedItems = fs.readdirSync(nestedDir) + for (const item of nestedItems) { + fs.renameSync(path.join(nestedDir, item), path.join(this.pmaDir, item)) + } + fs.rmdirSync(nestedDir) + } + + // Generate config.inc.php + this.generateConfig(mysqlPort) + + return true + } catch (error) { + console.error('phpMyAdmin setup error:', error) + return false + } + } + + private generateConfig(mysqlPort: string = '3306') { + const configPath = path.join(this.pmaDir, 'config.inc.php') + const blowfishSecret = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + + const configContent = `` + + fs.writeFileSync(configPath, configContent) + } + + getPath(): string { + return this.pmaDir + } +} + +export const phpMyAdminService = new PhpMyAdminService() diff --git a/src/main/services/PortMappingService.ts b/src/main/services/PortMappingService.ts new file mode 100644 index 0000000..36cf4e2 --- /dev/null +++ b/src/main/services/PortMappingService.ts @@ -0,0 +1,58 @@ +import fs from 'fs' +import path from 'path' +import { app } from 'electron' + +interface PortMap { + php: Record; + mysql: Record; +} + +export class PortMappingService { + private configFile: string + private portMap: PortMap = { php: {}, mysql: {} } + private nextPhpPort = 9001 + private nextMysqlPort = 3306 + + constructor() { + this.configFile = path.join(app.getPath('userData'), 'port_mappings.json') + this.load() + } + + private load() { + if (fs.existsSync(this.configFile)) { + try { + this.portMap = JSON.parse(fs.readFileSync(this.configFile, 'utf-8')) + + const phpPorts = Object.values(this.portMap.php) + if (phpPorts.length > 0) this.nextPhpPort = Math.max(...phpPorts) + 1 + + const mysqlPorts = Object.values(this.portMap.mysql) + if (mysqlPorts.length > 0) this.nextMysqlPort = Math.max(...mysqlPorts) + 1 + } catch (e) { + console.error('Failed to load port mappings', e) + } + } + } + + private save() { + fs.writeFileSync(this.configFile, JSON.stringify(this.portMap, null, 2)) + } + + getPhpPort(version: string): number { + if (!this.portMap.php[version]) { + this.portMap.php[version] = this.nextPhpPort++ + this.save() + } + return this.portMap.php[version] + } + + getMySqlPort(version: string): number { + if (!this.portMap.mysql[version]) { + this.portMap.mysql[version] = this.nextMysqlPort++ + this.save() + } + return this.portMap.mysql[version] + } +} + +export const portMappingService = new PortMappingService() diff --git a/src/main/services/ProjectService.ts b/src/main/services/ProjectService.ts index e1462f1..12eb62a 100644 --- a/src/main/services/ProjectService.ts +++ b/src/main/services/ProjectService.ts @@ -7,6 +7,7 @@ export interface Project { name: string path: string phpVersion: string + mySqlVersion: string host: string } @@ -49,6 +50,14 @@ export class ProjectService { return newProject } + updateProject(project: Project) { + const index = this.projects.findIndex(p => p.id === project.id) + if (index !== -1) { + this.projects[index] = project + this.saveProjects() + } + } + removeProject(id: string) { this.projects = this.projects.filter(p => p.id !== id) this.saveProjects() diff --git a/src/preload/index.ts b/src/preload/index.ts index bb8aead..4580b09 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -12,6 +12,7 @@ const api = { getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'), addProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:add', project), removeProject: (id: string) => electronAPI.ipcRenderer.invoke('projects:remove', id), + updateProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:update', project), getPhpVersions: () => electronAPI.ipcRenderer.invoke('binaries:list-php'), downloadPhpVersion: (id: string) => electronAPI.ipcRenderer.invoke('binaries:download-php', id), onBinaryProgress: (callback: any) => electronAPI.ipcRenderer.on('binaries:progress', (_event, data) => callback(data)), @@ -23,7 +24,10 @@ const api = { saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings), getNginxVersions: () => electronAPI.ipcRenderer.invoke('nginx:versions'), downloadNginxVersion: (id: string) => electronAPI.ipcRenderer.invoke('nginx:download', id), - onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data)) + onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data)), + pmaStatus: () => electronAPI.ipcRenderer.invoke('pma:status'), + pmaSetup: (mysqlPort: string) => electronAPI.ipcRenderer.invoke('pma:setup', mysqlPort), + onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data)) } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index be34a44..4196849 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -57,7 +57,9 @@ import { Terminal as TerminalIcon, ContentCopy as CopyIcon, Close as CloseIcon, - ContentCopy as ContentCopyIcon + ContentCopy as ContentCopyIcon, + OpenInNew as OpenIcon, + Edit as EditIcon } from '@mui/icons-material' import { useTranslation } from 'react-i18next' @@ -97,7 +99,8 @@ function App(): JSX.Element { const [nginxVersions, setNginxVersions] = useState([]) const [loadingVersions, setLoadingVersions] = useState(false) const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false) - const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' }) + const slugify = (text: string) => text.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, ''); + const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' }) const [settings, setSettings] = useState({ nginxPort: '80', phpPort: '9001', @@ -114,6 +117,9 @@ function App(): JSX.Element { const [logDialogOpen, setLogDialogOpen] = useState(false) const [selectedService, setSelectedService] = useState('') const [logs, setLogs] = useState([]) + const [pmaInstalled, setPmaInstalled] = useState(false) + const [pmaLoading, setPmaLoading] = useState(false) + const [pmaProgress, setPmaProgress] = useState(0) const fetchStatus = async () => { if (window.api && window.api.getServiceStatus) { @@ -174,23 +180,39 @@ function App(): JSX.Element { } useEffect(() => { - if (isProjectDialogOpen && !newProject.phpVersion && phpVersions.length > 0) { - const installed = phpVersions.find(v => v.status === 'installed') - if (installed) { - setNewProject(prev => ({ ...prev, phpVersion: installed.version })) + if (isProjectDialogOpen && !newProject.id) { + const updates: Partial = {} + if (!newProject.phpVersion && phpVersions.length > 0) { + const installedPhp = phpVersions.find(v => v.status === 'installed') + if (installedPhp) updates.phpVersion = installedPhp.version + } + if (!newProject.mySqlVersion && mySqlVersions.length > 0) { + const installedMysql = mySqlVersions.find(v => v.status === 'installed') + if (installedMysql) updates.mySqlVersion = installedMysql.version + } + if (Object.keys(updates).length > 0) { + setNewProject(prev => ({ ...prev, ...updates })) } } - }, [isProjectDialogOpen, phpVersions]) + }, [isProjectDialogOpen, phpVersions, mySqlVersions]) useEffect(() => { if (!window.api) return + const fetchPmaStatus = async () => { + if (window.api && window.api.pmaStatus) { + const installed = await window.api.pmaStatus() + setPmaInstalled(installed) + } + } + fetchStatus() fetchProjects() fetchPhpVersions() fetchMySqlVersions() fetchNginxVersions() fetchSettings() + fetchPmaStatus() const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => { if (!data) return @@ -210,6 +232,10 @@ function App(): JSX.Element { setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v)) }) : null + const removePmaListener = window.api.onPmaProgress ? window.api.onPmaProgress((p: number) => { + setPmaProgress(p) + }) : null + const interval = setInterval(() => { fetchStatus() fetchProjects() @@ -231,6 +257,7 @@ function App(): JSX.Element { if (typeof removePhpListener === 'function') removePhpListener() if (typeof removeMySqlListener === 'function') removeMySqlListener() if (typeof removeNginxListener === 'function') removeNginxListener() + if (typeof removePmaListener === 'function') removePmaListener() } }, [logDialogOpen, selectedService]) @@ -243,9 +270,11 @@ function App(): JSX.Element { const handleSelectPath = async () => { if (window.api && window.api.selectDirectory) { - const path = await window.api.selectDirectory() - if (path) { - setNewProject(prev => ({ ...prev, path })) + const pathStr = await window.api.selectDirectory() + if (pathStr) { + const folderName = pathStr.split(/[/\\]/).pop() || '' + const slug = slugify(folderName) + setNewProject(prev => ({ ...prev, path: pathStr, name: folderName, host: slug })) } } } @@ -331,10 +360,14 @@ function App(): JSX.Element { const handleAddProject = async () => { if (!window.api) return - await window.api.addProject(newProject) + if (newProject.id) { + await window.api.updateProject(newProject) + } else { + await window.api.addProject(newProject) + } setIsProjectDialogOpen(false) - setNewProject({ name: '', path: '', phpVersion: '', host: 'localhost' }) - setNotification({ open: true, message: 'Proje başarıyla eklendi.', severity: 'success' }) + setNewProject({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' }) + setNotification({ open: true, message: newProject.id ? 'Proje güncellendi.' : 'Proje başarıyla eklendi.', severity: 'success' }) fetchProjects() } @@ -378,6 +411,27 @@ function App(): JSX.Element { fetchNginxVersions() } + const handleSetupPma = async () => { + if (!window.api) return + setPmaLoading(true) + setPmaProgress(0) + try { + const success = await window.api.pmaSetup() + if (success) { + setPmaInstalled(true) + setNotification({ open: true, message: 'phpMyAdmin başarıyla kuruldu.', severity: 'success' }) + } else { + setNotification({ open: true, message: 'phpMyAdmin kurulumu sırasında hata oluştu.', severity: 'error' }) + } + } finally { + setPmaLoading(false) + } + } + + const handleOpenPma = () => { + window.open(`http://localhost:${settings.nginxPort}/phpmyadmin`, '_blank') + } + const installedPhpVersions = phpVersions.filter(v => v.status === 'installed') return ( @@ -501,51 +555,97 @@ function App(): JSX.Element { )} {settingsTab === 1 && ( - - - MySQL Sürümleri - Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz. - - - {mySqlVersions.length === 0 && !loadingVersions && ( - + <> + + + MySQL Sürümleri + Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz. + + + {mySqlVersions.length === 0 && !loadingVersions && ( + + )} + {mySqlVersions.map((v) => ( + + + {v.status === 'installed' ? : } + + + {`MySQL ${v.version} (${v.cycle})`} + {v.description && ( + + )} + + } + secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'} + sx={{ flexGrow: 1 }} + /> + + {v.status === 'available' && ( + + )} + {v.status === 'downloading' && ( + + + + )} + {v.status === 'installed' && ( + + )} + + + ))} + + + + + + + + + + phpMyAdmin + + {pmaInstalled ? 'Veritabanı yönetimi için phpMyAdmin hazır.' : 'Veritabanlarını web üzerinden yönetmek için phpMyAdmin kurun.'} + + + + {!pmaInstalled ? ( + + ) : ( + + )} + + + {pmaLoading && ( + + + + İndiriliyor... %{Math.round(pmaProgress)} + + )} - {mySqlVersions.map((v) => ( - - - {v.status === 'installed' ? : } - - - {`MySQL ${v.version} (${v.cycle})`} - {v.description && ( - - )} - - } - secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'} - sx={{ flexGrow: 1 }} - /> - - {v.status === 'available' && ( - - )} - {v.status === 'downloading' && ( - - - - )} - {v.status === 'installed' && ( - - )} - - - ))} - - + + )} {settingsTab === 2 && ( @@ -715,15 +815,23 @@ function App(): JSX.Element { handleRemoveProject(project.id)}> - - + + { + setNewProject(project) + setIsProjectDialogOpen(true) + }} sx={{ mr: 1 }}> + + + handleRemoveProject(project.id)}> + + + } > @@ -856,8 +964,11 @@ function App(): JSX.Element { - setIsProjectDialogOpen(false)} fullWidth maxWidth="sm"> - Yeni Proje Ekle + { + setIsProjectDialogOpen(false) + setNewProject({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' }) + }} fullWidth maxWidth="sm"> + {newProject.id ? 'Proje Düzenle' : 'Yeni Proje Ekle'} setNewProject({ ...newProject, host: e.target.value })} /> - - PHP Sürümü - - + + + PHP Sürümü + + + v.status === 'installed').length === 0}> + MySQL Sürümü + + + - + - - + - {/* Log Dialog */} - setLogDialogOpen(false)} fullWidth maxWidth="md"> - - - - {selectedService.toUpperCase()} Konsol Çıktısı - - - navigator.clipboard.writeText(logs.join('\n'))}> - - - - - - - {logs.length === 0 ? ( - Henüz log kaydı yok... - ) : ( - logs.map((line, i) => ( - - {line} - - )) - )} - - - - - - - - ) + {/* Log Dialog */} + setLogDialogOpen(false)} fullWidth maxWidth="md"> + + + + {selectedService.toUpperCase()} Konsol Çıktısı + + + navigator.clipboard.writeText(logs.join('\n'))}> + + + + + + + {logs.length === 0 ? ( + Henüz log kaydı yok... + ) : ( + logs.map((line, i) => ( + + {line} + + )) + )} + + + + + + + + ) } -export default App + export default App