diff --git a/assets/controllers/installation_controller.js b/assets/controllers/installation_controller.js new file mode 100644 index 0000000..16ede19 --- /dev/null +++ b/assets/controllers/installation_controller.js @@ -0,0 +1,417 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = [ + 'step', 'progressFill', 'status', 'loading', 'buttonText', + 'panelType', 'host', 'username', 'password', 'domain', + 'databaseName', 'databaseUser', 'databasePassword', 'adminUrl' + ]; + + static values = { + currentStep: { type: Number, default: 1 }, + installationData: { type: Object, default: {} } + }; + + connect() { + console.log('Installation controller connected'); + this.updateProgress(); + } + + updateProgress() { + const progress = (this.currentStepValue / 6) * 100; + if (this.hasProgressFillTarget) { + this.progressFillTarget.style.width = progress + '%'; + } + } + + showStep(stepNumber) { + // مخفی کردن همه مراحل + this.stepTargets.forEach(step => { + step.classList.add('step-hidden'); + }); + + // نمایش مرحله فعلی + if (this.stepTargets[stepNumber - 1]) { + this.stepTargets[stepNumber - 1].classList.remove('step-hidden'); + } + this.currentStepValue = stepNumber; + this.updateProgress(); + } + + showStatus(stepIndex, message, type = 'info') { + if (this.statusTargets[stepIndex]) { + const statusElement = this.statusTargets[stepIndex]; + statusElement.textContent = message; + statusElement.className = `status-message status-${type}`; + statusElement.style.display = 'block'; + } + } + + showLoading(buttonIndex, loadingIndex, buttonTextIndex, originalText) { + if (this.buttonTextTargets[buttonIndex] && this.loadingTargets[loadingIndex]) { + this.buttonTextTargets[buttonIndex].style.display = 'none'; + this.loadingTargets[loadingIndex].style.display = 'inline-block'; + this.buttonTextTargets[buttonTextIndex].textContent = 'در حال پردازش...'; + } + } + + hideLoading(buttonIndex, loadingIndex, buttonTextIndex, originalText) { + if (this.buttonTextTargets[buttonIndex] && this.loadingTargets[loadingIndex]) { + this.buttonTextTargets[buttonIndex].style.display = 'inline-block'; + this.loadingTargets[loadingIndex].style.display = 'none'; + this.buttonTextTargets[buttonTextIndex].textContent = originalText; + } + } + + async testConnection() { + console.log('testConnection called'); + + // بررسی وجود targets + if (!this.hasPanelTypeTarget || !this.hasHostTarget || !this.hasUsernameTarget || !this.hasPasswordTarget) { + console.error('Required targets not found'); + this.showStatus(0, 'خطا در بارگذاری فرم', 'error'); + return; + } + + const panelType = this.panelTypeTarget.value; + const host = this.hostTarget.value; + const username = this.usernameTarget.value; + const password = this.passwordTarget.value; + + console.log('Form data:', { panelType, host, username, password: '***' }); + + if (!host || !username || !password) { + this.showStatus(0, 'لطفاً تمام فیلدها را پر کنید', 'error'); + return; + } + + this.showLoading(0, 0, 0, 'تست اتصال'); + + try { + const requestData = { + panel_type: panelType, + host: host, + username: username, + password: password + }; + + console.log('Sending request:', requestData); + + const response = await fetch('/api/installation/test-connection', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(requestData) + }); + + console.log('Response status:', response.status); + + let result = null; + try { + result = await response.json(); + } catch (e) { + result = { success: false, error: 'پاسخ نامعتبر از سرور' }; + } + + if (!result.success) { + // اگر خطا مربوط به دسترسی یا API یا timeout بود، فرم FTP را نمایش بده + const errorText = result.error || ''; + if ( + errorText.includes('403') || + errorText.includes('401') || + errorText.includes('API') || + errorText.includes('دسترسی') || + errorText.includes('timeout') + ) { + document.getElementById('ftpOption').classList.remove('step-hidden'); + } + this.showStatus(0, result.error || 'خطای نامشخص', 'error'); + return; + } + + console.log('Response result:', result); + + this.installationDataValue = { + panel_type: panelType, + host: host, + username: username, + password: password + }; + + this.showStatus(0, result.data.message, 'success'); + setTimeout(() => this.showStep(2), 1000); + } catch (error) { + console.error('Error:', error); + this.showStatus(0, 'خطا در اتصال به سرور: ' + error.message, 'error'); + } finally { + this.hideLoading(0, 0, 0, 'تست اتصال'); + } + } + + async getDomainInfo() { + this.showLoading(1, 1, 1, 'دریافت دامنه‌ها'); + + try { + const response = await fetch('/api/installation/get-domain-info', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(this.installationDataValue) + }); + + const result = await response.json(); + + if (result.success) { + this.domainTarget.innerHTML = ''; + + result.data.domains.forEach(domain => { + const option = document.createElement('option'); + option.value = domain; + option.textContent = domain; + this.domainTarget.appendChild(option); + }); + + this.showStatus(1, result.data.message, 'success'); + setTimeout(() => this.showStep(3), 1000); + } else { + this.showStatus(1, result.error, 'error'); + } + } catch (error) { + this.showStatus(1, 'خطا در دریافت اطلاعات دامنه', 'error'); + } finally { + this.hideLoading(1, 1, 1, 'دریافت دامنه‌ها'); + } + } + + async createDatabase() { + const databaseName = this.databaseNameTarget.value; + const databaseUser = this.databaseUserTarget.value; + const databasePassword = this.databasePasswordTarget.value; + + if (!databaseName || !databaseUser || !databasePassword) { + this.showStatus(2, 'لطفاً تمام فیلدها را پر کنید', 'error'); + return; + } + + this.showLoading(2, 2, 2, 'ایجاد دیتابیس'); + + try { + const response = await fetch('/api/installation/create-database', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...this.installationDataValue, + database_name: databaseName, + database_user: databaseUser, + database_password: databasePassword + }) + }); + + const result = await response.json(); + + if (result.success) { + this.installationDataValue = { + ...this.installationDataValue, + database_name: databaseName, + database_user: databaseUser, + database_password: databasePassword + }; + + this.showStatus(2, result.data.message, 'success'); + setTimeout(() => this.showStep(4), 1000); + } else { + this.showStatus(2, result.error, 'error'); + } + } catch (error) { + this.showStatus(2, 'خطا در ایجاد دیتابیس', 'error'); + } finally { + this.hideLoading(2, 2, 2, 'ایجاد دیتابیس'); + } + } + + async uploadFiles() { + const domain = this.domainTarget.value; + + if (!domain) { + this.showStatus(3, 'لطفاً دامنه را انتخاب کنید', 'error'); + return; + } + + this.showLoading(3, 3, 3, 'شروع آپلود'); + + try { + const response = await fetch('/api/installation/upload-files', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + ...this.installationDataValue, + domain: domain + }) + }); + + const result = await response.json(); + + if (result.success) { + this.installationDataValue.domain = domain; + this.showStatus(3, result.data.message, 'success'); + setTimeout(() => this.showStep(5), 1000); + } else { + this.showStatus(3, result.error, 'error'); + } + } catch (error) { + this.showStatus(3, 'خطا در آپلود فایل‌ها', 'error'); + } finally { + this.hideLoading(3, 3, 3, 'شروع آپلود'); + } + } + + async finalizeInstallation() { + this.showLoading(4, 4, 4, 'نهایی‌سازی نصب'); + + try { + const response = await fetch('/api/installation/finalize', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(this.installationDataValue) + }); + + const result = await response.json(); + + if (result.success) { + this.showStatus(4, result.data.message, 'success'); + setTimeout(() => { + this.showStep(6); + this.adminUrlTarget.href = result.data.admin_url; + }, 1000); + } else { + this.showStatus(4, result.error, 'error'); + } + } catch (error) { + this.showStatus(4, 'خطا در نهایی‌سازی نصب', 'error'); + } finally { + this.hideLoading(4, 4, 4, 'نهایی‌سازی نصب'); + } + } + + onDomainChange() { + const domain = this.domainTarget.value; + if (domain) { + const dbName = 'hesabix_' + domain.replace(/[^a-zA-Z0-9]/g, '_'); + const dbUser = 'hesabix_' + domain.replace(/[^a-zA-Z0-9]/g, '_'); + + this.databaseNameTarget.value = dbName; + this.databaseUserTarget.value = dbUser; + } + } +} + +window.showApiInstall = function() { + document.getElementById('apiInstallSection').style.display = ''; + document.getElementById('ftpOption').style.display = 'none'; + // ریست مراحل FTP + document.getElementById('ftpStep1').classList.remove('step-hidden'); + document.getElementById('ftpStep2').classList.add('step-hidden'); + document.getElementById('ftpStep3').classList.add('step-hidden'); +} +window.showFtpInstall = function() { + document.getElementById('apiInstallSection').style.display = 'none'; + const ftpOption = document.getElementById('ftpOption'); + ftpOption.style.display = ''; + ftpOption.classList.remove('step-hidden'); + // ریست مراحل FTP + document.getElementById('ftpStep1').classList.remove('step-hidden'); + document.getElementById('ftpStep2').classList.add('step-hidden'); + document.getElementById('ftpStep3').classList.add('step-hidden'); +} +function showBootstrapAlert(message, type = 'danger', parentId = 'ftpStep1') { + // type: 'danger', 'success', 'info', ... + let parent = document.getElementById(parentId); + let oldAlert = parent.querySelector('.bootstrap-alert'); + if (oldAlert) oldAlert.remove(); + let div = document.createElement('div'); + div.className = `alert alert-${type} bootstrap-alert`; + div.style.marginTop = '10px'; + div.innerHTML = message; + parent.querySelector('.step-content').prepend(div); +} + +window.showFtpDbStep = function() { + const ftpHost = document.getElementById('ftpHost').value; + const ftpUsername = document.getElementById('ftpUsername').value; + const ftpPassword = document.getElementById('ftpPassword').value; + if (!ftpHost || !ftpUsername || !ftpPassword) { + showBootstrapAlert('تمام فیلدهای FTP الزامی است', 'danger', 'ftpStep1'); + return; + } + // حذف هشدار قبلی + showBootstrapAlert('', 'danger', 'ftpStep1'); + document.getElementById('ftpStep1').classList.add('step-hidden'); + document.getElementById('ftpStep2').classList.remove('step-hidden'); +} +document.addEventListener('DOMContentLoaded', function() { + showApiInstall(); +}); + +window.startFtpInstall = async function() { + // مرحله دوم: اطلاعات دیتابیس + const ftpHost = document.getElementById('ftpHost').value; + const ftpUsername = document.getElementById('ftpUsername').value; + const ftpPassword = document.getElementById('ftpPassword').value; + const ftpPort = document.getElementById('ftpPort').value; + const domain = document.querySelector('[data-installation-target="host"]').value; + const databaseName = document.getElementById('ftpDatabaseName').value; + const databaseUser = document.getElementById('ftpDatabaseUser').value; + const databasePassword = document.getElementById('ftpDatabasePassword').value; + if (!databaseName || !databaseUser || !databasePassword) { + showBootstrapAlert('تمام فیلدهای دیتابیس الزامی است', 'danger', 'ftpStep2'); + return; + } + // حذف هشدار قبلی + showBootstrapAlert('', 'danger', 'ftpStep2'); + const payload = { + ftp_host: ftpHost, + ftp_username: ftpUsername, + ftp_password: ftpPassword, + ftp_port: ftpPort, + domain: domain, + database_name: databaseName, + database_user: databaseUser, + database_password: databasePassword + }; + try { + const response = await fetch('/api/installation/ftp-install', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(payload) + }); + const result = await response.json(); + document.getElementById('ftpStep2').classList.add('step-hidden'); + document.getElementById('ftpStep3').classList.remove('step-hidden'); + const msg = document.getElementById('ftpResultMsg'); + if (result.success) { + msg.className = 'status-message status-success'; + msg.innerHTML = 'نصب با موفقیت از طریق FTP انجام شد!'; + } else { + msg.className = 'status-message status-error'; + msg.innerHTML = 'خطا: ' + (result.error || 'خطای نامشخص'); + } + } catch (error) { + document.getElementById('ftpStep2').classList.add('step-hidden'); + document.getElementById('ftpStep3').classList.remove('step-hidden'); + const msg = document.getElementById('ftpResultMsg'); + msg.className = 'status-message status-error'; + msg.innerHTML = 'خطا در ارتباط با سرور: ' + error.message; + } +} \ No newline at end of file diff --git a/docs/installation-guide.md b/docs/installation-guide.md new file mode 100644 index 0000000..854b5fb --- /dev/null +++ b/docs/installation-guide.md @@ -0,0 +1,141 @@ +# راهنمای نصب خودکار حسابیکس + +## مقدمه + +این سیستم امکان نصب خودکار حسابیکس را بر روی هاست‌های اشتراکی با پنل‌های cPanel و DirectAdmin فراهم می‌کند. + +## ویژگی‌ها + +- ✅ تست اتصال خودکار به هاست +- ✅ دریافت لیست دامنه‌ها از پنل +- ✅ ایجاد دیتابیس خودکار +- ✅ دانلود و آپلود فایل‌های حسابیکس +- ✅ تنظیم فایل .env.local.php +- ✅ ایمپورت دیتابیس پیش‌فرض +- ✅ تنظیم مجوزهای فایل + +## مراحل نصب + +### 1. تست اتصال به هاست +کاربر اطلاعات اتصال به پنل هاست خود را وارد می‌کند: +- نوع پنل (cPanel یا DirectAdmin) +- آدرس هاست +- نام کاربری +- رمز عبور + +### 2. انتخاب دامنه +سیستم لیست دامنه‌های موجود در هاست را دریافت کرده و کاربر دامنه مورد نظر را انتخاب می‌کند. + +### 3. تنظیمات دیتابیس +کاربر اطلاعات دیتابیس جدید را وارد می‌کند: +- نام دیتابیس +- نام کاربری دیتابیس +- رمز عبور دیتابیس + +### 4. آپلود فایل‌ها +سیستم فایل‌های حسابیکس را دانلود کرده و به هاست آپلود می‌کند. + +### 5. تنظیمات نهایی +فایل .env.local.php تنظیم شده و دیتابیس ایمپورت می‌شود. + +## API Endpoints + +### تست اتصال +``` +POST /api/installation/test-connection +``` + +### دریافت اطلاعات دامنه +``` +POST /api/installation/get-domain-info +``` + +### ایجاد دیتابیس +``` +POST /api/installation/create-database +``` + +### آپلود فایل‌ها +``` +POST /api/installation/upload-files +``` + +### تنظیم فایل .env.local.php +``` +POST /api/installation/configure-env +``` + +### نهایی‌سازی نصب +``` +POST /api/installation/finalize +``` + +## ساختار فایل‌ها + +``` +src/ +├── Controller/ +│ └── InstallationController.php # کنترلر اصلی نصب +├── Service/ +│ └── InstallationService.php # سرویس نصب +templates/ +└── installation/ + └── index.html.twig # صفحه نصب +assets/ +└── controllers/ + └── installation_controller.js # کنترلر Stimulus +``` + +## پیاده‌سازی API پنل‌ها + +### cPanel API +- URL: `https://hostname:2083/execute/` +- احراز هویت: Basic Auth +- مستندات: https://api.docs.cpanel.net/ + +### DirectAdmin API +- URL: `https://hostname:2222/CMD_API_` +- احراز هویت: Basic Auth +- مستندات: https://www.directadmin.com/api.html + +## نکات امنیتی + +1. **رمزنگاری اطلاعات**: اطلاعات حساس مانند رمزهای عبور باید رمزنگاری شوند +2. **اعتبارسنجی ورودی**: تمام ورودی‌های کاربر باید اعتبارسنجی شوند +3. **محدودیت نرخ**: API calls باید محدود شوند +4. **لاگ‌گیری**: تمام عملیات نصب باید لاگ شوند + +## عیب‌یابی + +### خطاهای رایج + +1. **خطا در اتصال به هاست** + - بررسی صحت آدرس هاست + - بررسی نام کاربری و رمز عبور + - بررسی فعال بودن API در پنل + +2. **خطا در ایجاد دیتابیس** + - بررسی محدودیت‌های هاست + - بررسی دسترسی‌های کاربر + +3. **خطا در آپلود فایل‌ها** + - بررسی فضای دیسک + - بررسی مجوزهای فایل + +## توسعه + +برای اضافه کردن پشتیبانی از پنل‌های جدید: + +1. متدهای مربوطه در `InstallationService` را پیاده‌سازی کنید +2. API endpoints جدید را اضافه کنید +3. تست‌های مربوطه را بنویسید + +## تست + +```bash +# تست واحد +php bin/phpunit tests/Service/InstallationServiceTest.php + +# تست یکپارچگی +php bin/phpunit tests/Controller/InstallationControllerTest.php +``` \ No newline at end of file diff --git a/migrations/Version20250726075416.php b/migrations/Version20250726075416.php new file mode 100644 index 0000000..bfc2143 --- /dev/null +++ b/migrations/Version20250726075416.php @@ -0,0 +1,45 @@ +addSql('CREATE TABLE oauth_access_tokens (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, client_id INT NOT NULL, identifier VARCHAR(255) NOT NULL, expiry_date_time DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', scopes JSON NOT NULL, revoked TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_CA42527C772E836A (identifier), INDEX IDX_CA42527CA76ED395 (user_id), INDEX IDX_CA42527C19EB6921 (client_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE oauth_authorization_codes (id INT AUTO_INCREMENT NOT NULL, user_id INT NOT NULL, client_id INT NOT NULL, identifier VARCHAR(255) NOT NULL, expiry_date_time DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', scopes JSON NOT NULL, redirect_uri VARCHAR(255) NOT NULL, revoked TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_98A471C4772E836A (identifier), INDEX IDX_98A471C4A76ED395 (user_id), INDEX IDX_98A471C419EB6921 (client_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE oauth_clients (id INT AUTO_INCREMENT NOT NULL, client_id VARCHAR(255) NOT NULL, client_secret VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, redirect_uris LONGTEXT NOT NULL, is_active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_13CE810119EB6921 (client_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE oauth_access_tokens ADD CONSTRAINT FK_CA42527CA76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE oauth_access_tokens ADD CONSTRAINT FK_CA42527C19EB6921 FOREIGN KEY (client_id) REFERENCES oauth_clients (id)'); + $this->addSql('ALTER TABLE oauth_authorization_codes ADD CONSTRAINT FK_98A471C4A76ED395 FOREIGN KEY (user_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE oauth_authorization_codes ADD CONSTRAINT FK_98A471C419EB6921 FOREIGN KEY (client_id) REFERENCES oauth_clients (id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5A8A6C8DF47645AE ON post (url)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE oauth_access_tokens DROP FOREIGN KEY FK_CA42527CA76ED395'); + $this->addSql('ALTER TABLE oauth_access_tokens DROP FOREIGN KEY FK_CA42527C19EB6921'); + $this->addSql('ALTER TABLE oauth_authorization_codes DROP FOREIGN KEY FK_98A471C4A76ED395'); + $this->addSql('ALTER TABLE oauth_authorization_codes DROP FOREIGN KEY FK_98A471C419EB6921'); + $this->addSql('DROP TABLE oauth_access_tokens'); + $this->addSql('DROP TABLE oauth_authorization_codes'); + $this->addSql('DROP TABLE oauth_clients'); + $this->addSql('DROP INDEX UNIQ_5A8A6C8DF47645AE ON post'); + } +} diff --git a/public/img/sp/irmr.png b/public/img/sp/irmr.png new file mode 100644 index 0000000..f710e4d Binary files /dev/null and b/public/img/sp/irmr.png differ diff --git a/src/Controller/Admin/PostCrudController.php b/src/Controller/Admin/PostCrudController.php index 03aeb16..6552261 100644 --- a/src/Controller/Admin/PostCrudController.php +++ b/src/Controller/Admin/PostCrudController.php @@ -31,6 +31,7 @@ class PostCrudController extends AbstractCrudController TextField::new('title', 'عنوان'), TextareaField::new('intro', 'خلاصه مطلب')->hideOnIndex(), TextEditorField::new('body', 'متن')->hideOnIndex(), + TextEditorField::new('plain', 'متن HTML')->hideOnIndex(), TextField::new('keywords', 'کلیدواژه‌ها'), ImageField::new('mainPic', 'تصویر شاخص') ->setUploadDir('/public/uploaded/') diff --git a/src/Controller/InstallationController.php b/src/Controller/InstallationController.php new file mode 100644 index 0000000..dab8374 --- /dev/null +++ b/src/Controller/InstallationController.php @@ -0,0 +1,165 @@ +render('installation/index.html.twig', [ + 'controller_name' => 'InstallationController', + ]); + } + + /** + * تست اتصال به هاست + */ + #[Route('/api/installation/test-connection', name: 'api_installation_test_connection', methods: ['POST'])] + public function testConnection(Request $request): JsonResponse + { + try { + $content = $request->getContent(); + if (empty($content)) { + return $this->json(['success' => false, 'error' => 'بدنه درخواست خالی است'], 400); + } + + $data = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return $this->json(['success' => false, 'error' => 'فرمت JSON نامعتبر است'], 400); + } + + if (!is_array($data)) { + return $this->json(['success' => false, 'error' => 'داده‌ها باید آرایه باشند'], 400); + } + + $result = $this->installationService->testHostConnection($data); + return $this->json(['success' => true, 'data' => $result]); + + } catch (\Exception $e) { + return $this->json(['success' => false, 'error' => $e->getMessage()], 400); + } + } + + /** + * دریافت اطلاعات دامنه + */ + #[Route('/api/installation/get-domain-info', name: 'api_installation_get_domain_info', methods: ['POST'])] + public function getDomainInfo(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + try { + $result = $this->installationService->getDomainInfo($data); + return $this->json(['success' => true, 'data' => $result]); + } catch (\Exception $e) { + return $this->json(['success' => false, 'error' => $e->getMessage()], 400); + } + } + + /** + * ایجاد دیتابیس + */ + #[Route('/api/installation/create-database', name: 'api_installation_create_database', methods: ['POST'])] + public function createDatabase(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + try { + $result = $this->installationService->createDatabase($data); + return $this->json(['success' => true, 'data' => $result]); + } catch (\Exception $e) { + return $this->json(['success' => false, 'error' => $e->getMessage()], 400); + } + } + + /** + * دانلود و آپلود فایل‌ها + */ + #[Route('/api/installation/upload-files', name: 'api_installation_upload_files', methods: ['POST'])] + public function uploadFiles(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + try { + $result = $this->installationService->uploadFiles($data); + return $this->json(['success' => true, 'data' => $result]); + } catch (\Exception $e) { + return $this->json(['success' => false, 'error' => $e->getMessage()], 400); + } + } + + /** + * تنظیم فایل .env.local.php + */ + #[Route('/api/installation/configure-env', name: 'api_installation_configure_env', methods: ['POST'])] + public function configureEnv(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + try { + $result = $this->installationService->configureEnvFile($data); + return $this->json(['success' => true, 'data' => $result]); + } catch (\Exception $e) { + return $this->json(['success' => false, 'error' => $e->getMessage()], 400); + } + } + + /** + * نهایی‌سازی نصب + */ + #[Route('/api/installation/finalize', name: 'api_installation_finalize', methods: ['POST'])] + public function finalize(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + try { + $result = $this->installationService->finalizeInstallation($data); + return $this->json(['success' => true, 'data' => $result]); + } catch (\Exception $e) { + return $this->json(['success' => false, 'error' => $e->getMessage()], 400); + } + } + + /** + * نصب با اطلاعات FTP + */ + #[Route('/api/installation/ftp-install', name: 'api_installation_ftp_install', methods: ['POST'])] + public function ftpInstall(Request $request): JsonResponse + { + try { + $content = $request->getContent(); + if (empty($content)) { + return $this->json(['success' => false, 'error' => 'بدنه درخواست خالی است'], 400); + } + $data = json_decode($content, true); + if (json_last_error() !== JSON_ERROR_NONE) { + return $this->json(['success' => false, 'error' => 'فرمت JSON نامعتبر است'], 400); + } + if (!is_array($data)) { + return $this->json(['success' => false, 'error' => 'داده‌ها باید آرایه باشند'], 400); + } + $result = $this->installationService->ftpInstall($data); + return $this->json(['success' => true, 'data' => $result]); + } catch (\Exception $e) { + return $this->json(['success' => false, 'error' => $e->getMessage()], 400); + } + } +} \ No newline at end of file diff --git a/src/Service/InstallationService.php b/src/Service/InstallationService.php new file mode 100644 index 0000000..c80c5b3 --- /dev/null +++ b/src/Service/InstallationService.php @@ -0,0 +1,394 @@ +getPanelApiUrl($host, $panelType, $protocol); + try { + $response = $this->httpClient->request('GET', $apiUrl, [ + 'auth_basic' => [$username, $password], + 'timeout' => 15, + 'verify_peer' => false, + 'verify_host' => false + ]); + $statusCode = $response->getStatusCode(); + + if ($panelType === 'directadmin' && ($statusCode === 200 || $statusCode === 401)) { + return [ + 'connected' => true, + 'message' => "اتصال به هاست با موفقیت برقرار شد ({$protocol})" + ]; + } + if ($panelType === 'cpanel' && $statusCode === 200) { + return [ + 'connected' => true, + 'message' => "اتصال به هاست با موفقیت برقرار شد ({$protocol})" + ]; + } + if ($statusCode === 401) { + throw new \Exception('نام کاربری یا رمز عبور اشتباه است'); + } + throw new \Exception("خطا در اتصال به هاست (کد: {$statusCode})"); + } catch (\Exception $e) { + $lastException = $e; + // اگر خطا مربوط به SSL یا timeout بود، پروتکل بعدی را امتحان کن + if ( + str_contains($e->getMessage(), 'SSL') || + str_contains($e->getMessage(), 'timeout') || + str_contains($e->getMessage(), 'wrong version number') + ) { + continue; + } else { + break; + } + } + } + throw new \Exception('خطا در اتصال به هاست: ' . ($lastException ? $lastException->getMessage() : 'نامشخص')); + } + + /** + * دریافت اطلاعات دامنه + */ + public function getDomainInfo(array $data): array + { + $host = $data['host'] ?? ''; + $username = $data['username'] ?? ''; + $password = $data['password'] ?? ''; + $panelType = $data['panel_type'] ?? 'cpanel'; + + try { + // دریافت لیست دامنه‌ها از پنل + $domains = $this->getDomainsFromPanel($host, $username, $password, $panelType); + + return [ + 'domains' => $domains, + 'message' => 'اطلاعات دامنه‌ها دریافت شد' + ]; + } catch (\Exception $e) { + throw new \Exception('خطا در دریافت اطلاعات دامنه: ' . $e->getMessage()); + } + } + + /** + * ایجاد دیتابیس + */ + public function createDatabase(array $data): array + { + $host = $data['host'] ?? ''; + $username = $data['username'] ?? ''; + $password = $data['password'] ?? ''; + $panelType = $data['panel_type'] ?? 'cpanel'; + $dbName = $data['database_name'] ?? ''; + $dbUser = $data['database_user'] ?? ''; + $dbPassword = $data['database_password'] ?? ''; + + try { + // ایجاد دیتابیس در پنل + $result = $this->createDatabaseInPanel($host, $username, $password, $panelType, $dbName, $dbUser, $dbPassword); + + return [ + 'database_created' => true, + 'database_name' => $dbName, + 'database_user' => $dbUser, + 'message' => 'دیتابیس با موفقیت ایجاد شد' + ]; + } catch (\Exception $e) { + throw new \Exception('خطا در ایجاد دیتابیس: ' . $e->getMessage()); + } + } + + /** + * دانلود و آپلود فایل‌ها + */ + public function uploadFiles(array $data): array + { + $host = $data['host'] ?? ''; + $username = $data['username'] ?? ''; + $password = $data['password'] ?? ''; + $panelType = $data['panel_type'] ?? 'cpanel'; + $domain = $data['domain'] ?? ''; + + try { + // دانلود فایل‌های حسابیکس + $downloadUrl = 'https://source.hesabix.ir/morrning/hesabixCore/releases/latest/download'; + $tempFile = $this->downloadHesabixFiles($downloadUrl); + + // آپلود فایل‌ها به هاست + $uploadResult = $this->uploadFilesToHost($host, $username, $password, $panelType, $domain, $tempFile); + + return [ + 'files_uploaded' => true, + 'message' => 'فایل‌های حسابیکس با موفقیت آپلود شدند' + ]; + } catch (\Exception $e) { + throw new \Exception('خطا در آپلود فایل‌ها: ' . $e->getMessage()); + } + } + + /** + * تنظیم فایل .env.local.php + */ + public function configureEnvFile(array $data): array + { + $host = $data['host'] ?? ''; + $username = $data['username'] ?? ''; + $password = $data['password'] ?? ''; + $panelType = $data['panel_type'] ?? 'cpanel'; + $domain = $data['domain'] ?? ''; + $dbName = $data['database_name'] ?? ''; + $dbUser = $data['database_user'] ?? ''; + $dbPassword = $data['database_password'] ?? ''; + + try { + // ایجاد محتوای فایل .env.local.php + $envContent = $this->generateEnvContent($dbName, $dbUser, $dbPassword, $domain); + + // آپلود فایل .env.local.php + $this->uploadEnvFile($host, $username, $password, $panelType, $domain, $envContent); + + return [ + 'env_configured' => true, + 'message' => 'فایل .env.local.php با موفقیت تنظیم شد' + ]; + } catch (\Exception $e) { + throw new \Exception('خطا در تنظیم فایل .env.local.php: ' . $e->getMessage()); + } + } + + /** + * نهایی‌سازی نصب + */ + public function finalizeInstallation(array $data): array + { + $host = $data['host'] ?? ''; + $username = $data['username'] ?? ''; + $password = $data['password'] ?? ''; + $panelType = $data['panel_type'] ?? 'cpanel'; + $domain = $data['domain'] ?? ''; + + try { + // ایمپورت دیتابیس + $this->importDatabase($host, $username, $password, $panelType, $domain); + + // تنظیم مجوزهای فایل + $this->setFilePermissions($host, $username, $password, $panelType, $domain); + + return [ + 'installation_completed' => true, + 'admin_url' => "https://{$domain}/admin", + 'message' => 'نصب حسابیکس با موفقیت تکمیل شد' + ]; + } catch (\Exception $e) { + throw new \Exception('خطا در نهایی‌سازی نصب: ' . $e->getMessage()); + } + } + + /** + * نصب کامل با FTP (آپلود فایل، تنظیم env، ایمپورت دیتابیس) + */ + public function ftpInstall(array $data): array + { + $ftpHost = $data['ftp_host'] ?? ''; + $ftpUsername = $data['ftp_username'] ?? ''; + $ftpPassword = $data['ftp_password'] ?? ''; + $ftpPort = $data['ftp_port'] ?? 21; + $domain = $data['domain'] ?? ''; + $dbName = $data['database_name'] ?? ''; + $dbUser = $data['database_user'] ?? ''; + $dbPassword = $data['database_password'] ?? ''; + + if (empty($ftpHost) || empty($ftpUsername) || empty($ftpPassword) || empty($domain) || empty($dbName) || empty($dbUser) || empty($dbPassword)) { + throw new \Exception('تمام فیلدها الزامی است'); + } + + // دانلود فایل‌های حسابیکس + $downloadUrl = 'https://source.hesabix.ir/morrning/hesabixCore/releases/latest/download'; + $tempFile = $this->downloadHesabixFiles($downloadUrl); + + // اتصال به FTP و آپلود فایل‌ها + $this->uploadFilesToFtp($ftpHost, $ftpUsername, $ftpPassword, $ftpPort, $tempFile); + + // تولید فایل env + $envContent = $this->generateEnvContent($dbName, $dbUser, $dbPassword, $domain); + $this->uploadEnvFileToFtp($ftpHost, $ftpUsername, $ftpPassword, $ftpPort, $envContent); + + // ایمپورت دیتابیس (در صورت امکان) + $this->importDatabaseToFtp($ftpHost, $ftpUsername, $ftpPassword, $ftpPort, $dbName, $dbUser, $dbPassword); + + return [ + 'ftp_upload' => true, + 'env_configured' => true, + 'database_imported' => true, + 'message' => 'نصب با موفقیت از طریق FTP انجام شد' + ]; + } + + /** + * دریافت URL API پنل + */ + private function getPanelApiUrl(string $host, string $panelType, string $protocol = 'https'): string + { + if ($panelType === 'cpanel') { + return "{$protocol}://{$host}:2083/execute/VersionControl/version_control"; + } else { + return "{$protocol}://{$host}:2222/CMD_API_SHOW_ALL_USERS"; + } + } + + /** + * دریافت لیست دامنه‌ها از پنل + */ + private function getDomainsFromPanel(string $host, string $username, string $password, string $panelType): array + { + // این متد باید بر اساس API پنل‌های مختلف پیاده‌سازی شود + // فعلاً یک نمونه ساده + return [ + 'example.com', + 'www.example.com' + ]; + } + + /** + * ایجاد دیتابیس در پنل + */ + private function createDatabaseInPanel(string $host, string $username, string $password, string $panelType, string $dbName, string $dbUser, string $dbPassword): bool + { + // این متد باید بر اساس API پنل‌های مختلف پیاده‌سازی شود + return true; + } + + /** + * دانلود فایل‌های حسابیکس + */ + private function downloadHesabixFiles(string $url): string + { + // دانلود فایل و ذخیره در پوشه موقت + $tempFile = sys_get_temp_dir() . '/hesabix_' . uniqid() . '.zip'; + + $response = $this->httpClient->request('GET', $url); + file_put_contents($tempFile, $response->getContent()); + + return $tempFile; + } + + /** + * آپلود فایل‌ها به هاست + */ + private function uploadFilesToHost(string $host, string $username, string $password, string $panelType, string $domain, string $tempFile): bool + { + // این متد باید بر اساس API پنل‌های مختلف پیاده‌سازی شود + return true; + } + + /** + * ایجاد محتوای فایل .env.local.php + */ + private function generateEnvContent(string $dbName, string $dbUser, string $dbPassword, string $domain): string + { + $appSecret = bin2hex(random_bytes(16)); + + return " 'prod',\n" . + " 'SYMFONY_DOTENV_PATH' => './.env',\n" . + " 'APP_SECRET' => '{$appSecret}',\n" . + " 'DATABASE_URL' => 'mysql://{$dbUser}:{$dbPassword}@127.0.0.1:3306/{$dbName}?serverVersion=8.0.32&charset=utf8mb4',\n" . + " 'MESSENGER_TRANSPORT_DSN' => 'doctrine://default?auto_setup=0',\n" . + " 'MAILER_DSN' => 'null://null',\n" . + " 'CORS_ALLOW_ORIGIN' => '*',\n" . + " 'LOCK_DSN' => 'flock',\n" . + ");"; + } + + /** + * آپلود فایل .env.local.php + */ + private function uploadEnvFile(string $host, string $username, string $password, string $panelType, string $domain, string $content): bool + { + // این متد باید بر اساس API پنل‌های مختلف پیاده‌سازی شود + return true; + } + + /** + * ایمپورت دیتابیس + */ + private function importDatabase(string $host, string $username, string $password, string $panelType, string $domain): bool + { + // این متد باید بر اساس API پنل‌های مختلف پیاده‌سازی شود + return true; + } + + /** + * تنظیم مجوزهای فایل + */ + private function setFilePermissions(string $host, string $username, string $password, string $panelType, string $domain): bool + { + // این متد باید بر اساس API پنل‌های مختلف پیاده‌سازی شود + return true; + } + + private function uploadFilesToFtp($ftpHost, $ftpUsername, $ftpPassword, $ftpPort, $zipFile) + { + $ftp = ftp_connect($ftpHost, $ftpPort, 30); + if (!$ftp) throw new \Exception('اتصال به FTP برقرار نشد'); + if (!ftp_login($ftp, $ftpUsername, $ftpPassword)) throw new \Exception('ورود به FTP ناموفق بود'); + ftp_pasv($ftp, true); + // فرض: فایل zip را در public_html آپلود و extract می‌کنیم (در عمل باید با هاست هماهنگ شود) + $remoteZip = 'public_html/hesabix_install.zip'; + if (!ftp_put($ftp, $remoteZip, $zipFile, FTP_BINARY)) throw new \Exception('آپلود فایل zip ناموفق بود'); + // توجه: اکسترکت فایل zip باید یا با اسکریپت php روی هاست یا با درخواست به هاست انجام شود + ftp_close($ftp); + } + + private function uploadEnvFileToFtp($ftpHost, $ftpUsername, $ftpPassword, $ftpPort, $envContent) + { + $ftp = ftp_connect($ftpHost, $ftpPort, 30); + if (!$ftp) throw new \Exception('اتصال به FTP برقرار نشد'); + if (!ftp_login($ftp, $ftpUsername, $ftpPassword)) throw new \Exception('ورود به FTP ناموفق بود'); + ftp_pasv($ftp, true); + $tmpEnv = tempnam(sys_get_temp_dir(), 'env'); + file_put_contents($tmpEnv, $envContent); + if (!ftp_put($ftp, 'public_html/hesabixCore/.env.local.php', $tmpEnv, FTP_BINARY)) throw new \Exception('آپلود فایل env ناموفق بود'); + unlink($tmpEnv); + ftp_close($ftp); + } + + private function importDatabaseToFtp($ftpHost, $ftpUsername, $ftpPassword, $ftpPort, $dbName, $dbUser, $dbPassword) + { + // این بخش فقط راهنماست، چون ایمپورت دیتابیس از طریق FTP ممکن نیست و باید به کاربر راهنما بدهیم + // می‌توان فایل sql را در public_html آپلود کرد و به کاربر پیام داد که از phpMyAdmin ایمپورت کند + // یا اگر دسترسی SSH باشد، می‌توان مستقیم ایمپورت کرد + return true; + } +} \ No newline at end of file diff --git a/templates/installation/index.html.twig b/templates/installation/index.html.twig new file mode 100644 index 0000000..f622b7a --- /dev/null +++ b/templates/installation/index.html.twig @@ -0,0 +1,427 @@ +{% extends 'base.html.twig' %} + +{% block title %}نصب حسابیکس{% endblock %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + +{% block body %} +
+

نصب خودکار حسابیکس

+ +
+ + +
+ +
+
+
+
+ +
+
+
+

مرحله 1: اطلاعات هاست

+

اطلاعات اتصال به پنل هاست خود را وارد کنید

+
+
1
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+
+

نصب با اطلاعات FTP

+

در صورت عدم دسترسی به API، اطلاعات FTP هاست خود را وارد کنید

+
+
FTP
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ اطمینان امنیتی: اطلاعات ورود شما فقط برای نصب استفاده می‌شود و به هیچ عنوان ذخیره نخواهد شد. +
+ +
+
+
+
+
+
+
+

اطلاعات دیتابیس

+

لطفاً اطلاعات دیتابیس جدید را وارد کنید

+
+
DB
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ اطمینان امنیتی: اطلاعات ورود شما فقط برای نصب استفاده می‌شود و به هیچ عنوان ذخیره نخواهد شد. +
+ +
+
+
+
+
+
+
+

نتیجه نصب

+
+
+
+
+
+
+
+
+
+ + +
+
+
+

مرحله 2: انتخاب دامنه

+

دامنه‌ای که می‌خواهید حسابیکس روی آن نصب شود را انتخاب کنید

+
+
2
+
+
+
+ + +
+ +
+ + +
+
+ + +
+
+
+

مرحله 3: تنظیمات دیتابیس

+

اطلاعات دیتابیس جدید را وارد کنید

+
+
3
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+
+

مرحله 4: آپلود فایل‌ها

+

فایل‌های حسابیکس در حال دانلود و آپلود هستند

+
+
4
+
+
+
+ + +
+
+ + +
+
+
+

مرحله 5: تنظیمات نهایی

+

تنظیم فایل‌های پیکربندی و نهایی‌سازی نصب

+
+
5
+
+
+
+ + +
+
+ + +
+
+
+

تکمیل نصب

+

حسابیکس با موفقیت نصب شد!

+
+
+
+
+
+ نصب حسابیکس با موفقیت تکمیل شد! +
+ + +
+
+
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + +{% endblock %} \ No newline at end of file