add installation and some other fields
Some checks are pending
PHP Composer / build (push) Waiting to run

This commit is contained in:
Hesabix 2025-08-02 01:50:46 +00:00
parent 89dd4f1677
commit e4803a25d7
8 changed files with 1590 additions and 0 deletions

View file

@ -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 = '<option value="">انتخاب کنید...</option>';
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;
}
}

141
docs/installation-guide.md Normal file
View file

@ -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
```

View file

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20250726075416 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

BIN
public/img/sp/irmr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View file

@ -31,6 +31,7 @@ class PostCrudController extends AbstractCrudController
TextField::new('title', 'عنوان'), TextField::new('title', 'عنوان'),
TextareaField::new('intro', 'خلاصه مطلب')->hideOnIndex(), TextareaField::new('intro', 'خلاصه مطلب')->hideOnIndex(),
TextEditorField::new('body', 'متن')->hideOnIndex(), TextEditorField::new('body', 'متن')->hideOnIndex(),
TextEditorField::new('plain', 'متن HTML')->hideOnIndex(),
TextField::new('keywords', 'کلیدواژه‌ها'), TextField::new('keywords', 'کلیدواژه‌ها'),
ImageField::new('mainPic', 'تصویر شاخص') ImageField::new('mainPic', 'تصویر شاخص')
->setUploadDir('/public/uploaded/') ->setUploadDir('/public/uploaded/')

View file

@ -0,0 +1,165 @@
<?php
namespace App\Controller;
use App\Service\InstallationService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* کنترلر اتوماسیون نصب حسابیکس
*/
class InstallationController extends AbstractController
{
public function __construct(
private InstallationService $installationService
) {}
/**
* صفحه اصلی نصب
*/
#[Route('/installation', name: 'app_installation')]
public function index(): Response
{
return $this->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);
}
}
}

View file

@ -0,0 +1,394 @@
<?php
namespace App\Service;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
/**
* سرویس اتوماسیون نصب حسابیکس
*/
class InstallationService
{
public function __construct(
private HttpClientInterface $httpClient,
private ParameterBagInterface $params
) {}
/**
* تست اتصال به هاست
*/
public function testHostConnection(array $data): array
{
$host = $data['host'] ?? '';
$username = $data['username'] ?? '';
$password = $data['password'] ?? '';
$panelType = $data['panel_type'] ?? 'cpanel';
if (empty($host) || empty($username) || empty($password)) {
throw new \Exception('لطفاً تمام فیلدها را پر کنید');
}
$protocols = ($panelType === 'directadmin') ? ['https', 'http'] : ['https'];
$lastException = null;
foreach ($protocols as $protocol) {
$apiUrl = $this->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 "<?php\n\n" .
"// This file was generated by Hesabix Installation\n\n" .
"return array (\n" .
" 'APP_ENV' => '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;
}
}

View file

@ -0,0 +1,427 @@
{% extends 'base.html.twig' %}
{% block title %}نصب حسابیکس{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
.installation-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.step-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
overflow: hidden;
}
.step-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.step-number {
background: rgba(255,255,255,0.2);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.step-content {
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.progress-bar {
background: #f0f0f0;
border-radius: 10px;
height: 8px;
margin: 20px 0;
overflow: hidden;
}
.progress-fill {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
height: 100%;
width: 0%;
transition: width 0.3s ease;
}
.status-message {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
display: none;
}
.status-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.step-hidden {
display: none;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
{% endblock %}
{% block body %}
<div class="installation-container" data-controller="installation">
<h1 class="text-center mb-4">نصب خودکار حسابیکس</h1>
<div class="install-method-toggle" style="text-align:center; margin-bottom:20px;">
<button class="btn" id="apiInstallBtn" onclick="showApiInstall()" style="margin-left:10px;">نصب خودکار (API)</button>
<button class="btn" id="ftpInstallBtn" onclick="showFtpInstall()">نصب با FTP</button>
</div>
<div id="apiInstallSection">
<div class="progress-bar">
<div class="progress-fill" data-installation-target="progressFill"></div>
</div>
<!-- مرحله 1: اطلاعات هاست -->
<div class="step-card" data-installation-target="step">
<div class="step-header">
<div>
<h3>مرحله 1: اطلاعات هاست</h3>
<p>اطلاعات اتصال به پنل هاست خود را وارد کنید</p>
</div>
<div class="step-number">1</div>
</div>
<div class="step-content">
<div class="form-group">
<label class="form-label">نوع پنل</label>
<select class="form-control" data-installation-target="panelType">
<option value="cpanel">cPanel</option>
<option value="directadmin" selected>DirectAdmin</option>
</select>
</div>
<div class="form-group">
<label class="form-label">آدرس هاست</label>
<input type="text" class="form-control" data-installation-target="host" placeholder="example.com" value="cdn.hesabix.ir">
</div>
<div class="form-group">
<label class="form-label">نام کاربری</label>
<input type="text" class="form-control" data-installation-target="username" placeholder="نام کاربری پنل" value="cdnhes">
</div>
<div class="form-group">
<label class="form-label">رمز عبور</label>
<input type="password" class="form-control" data-installation-target="password" placeholder="رمز عبور پنل" value="RuYFFBkamH7MtN">
</div>
<div class="status-message" data-installation-target="status"></div>
<button class="btn" data-action="click->installation#testConnection">
<span data-installation-target="buttonText">تست اتصال</span>
<span class="loading" data-installation-target="loading" style="display: none;"></span>
</button>
</div>
</div>
<!-- ادامه مراحل نصب API ... -->
</div>
<div id="ftpOption" class="step-hidden">
<div id="ftpStep1">
<div class="step-card" style="margin-top:15px;">
<div class="step-header">
<div>
<h3>نصب با اطلاعات FTP</h3>
<p>در صورت عدم دسترسی به API، اطلاعات FTP هاست خود را وارد کنید</p>
</div>
<div class="step-number">FTP</div>
</div>
<div class="step-content">
<div class="form-group">
<label class="form-label">آدرس FTP</label>
<input type="text" class="form-control" id="ftpHost" placeholder="ftp.example.com">
</div>
<div class="form-group">
<label class="form-label">نام کاربری FTP</label>
<input type="text" class="form-control" id="ftpUsername" placeholder="نام کاربری FTP">
</div>
<div class="form-group">
<label class="form-label">رمز عبور FTP</label>
<input type="password" class="form-control" id="ftpPassword" placeholder="رمز عبور FTP">
</div>
<div class="form-group">
<label class="form-label">پورت FTP</label>
<input type="text" class="form-control" id="ftpPort" placeholder="21" value="21">
</div>
<div class="status-message status-info" style="margin-top:10px;display:block;">
<b>اطمینان امنیتی:</b> اطلاعات ورود شما فقط برای نصب استفاده می‌شود و به هیچ عنوان ذخیره نخواهد شد.
</div>
<button class="btn" id="ftpNextBtn" onclick="showFtpDbStep()">بعدی</button>
</div>
</div>
</div>
<div id="ftpStep2" class="step-hidden">
<div class="step-card" style="margin-top:15px;">
<div class="step-header">
<div>
<h3>اطلاعات دیتابیس</h3>
<p>لطفاً اطلاعات دیتابیس جدید را وارد کنید</p>
</div>
<div class="step-number">DB</div>
</div>
<div class="step-content">
<div class="form-group">
<label class="form-label">نام دیتابیس</label>
<input type="text" class="form-control" id="ftpDatabaseName" placeholder="hesabix_db">
</div>
<div class="form-group">
<label class="form-label">نام کاربری دیتابیس</label>
<input type="text" class="form-control" id="ftpDatabaseUser" placeholder="hesabix_user">
</div>
<div class="form-group">
<label class="form-label">رمز عبور دیتابیس</label>
<input type="password" class="form-control" id="ftpDatabasePassword" placeholder="رمز عبور قوی">
</div>
<div class="status-message status-info" style="margin-top:10px;display:block;">
<b>اطمینان امنیتی:</b> اطلاعات ورود شما فقط برای نصب استفاده می‌شود و به هیچ عنوان ذخیره نخواهد شد.
</div>
<button class="btn" onclick="startFtpInstall()">شروع نصب با FTP</button>
</div>
</div>
</div>
<div id="ftpStep3" class="step-hidden">
<div class="step-card" style="margin-top:15px;">
<div class="step-header">
<div>
<h3>نتیجه نصب</h3>
</div>
<div class="step-number">✓</div>
</div>
<div class="step-content">
<div id="ftpResultMsg" class="status-message status-info" style="margin-top:10px;display:block;"></div>
</div>
</div>
</div>
</div>
<!-- مرحله 2: انتخاب دامنه -->
<div class="step-card step-hidden" data-installation-target="step">
<div class="step-header">
<div>
<h3>مرحله 2: انتخاب دامنه</h3>
<p>دامنه‌ای که می‌خواهید حسابیکس روی آن نصب شود را انتخاب کنید</p>
</div>
<div class="step-number">2</div>
</div>
<div class="step-content">
<div class="form-group">
<label class="form-label">دامنه</label>
<select class="form-control" data-installation-target="domain">
<option value="">انتخاب کنید...</option>
</select>
</div>
<div class="status-message" data-installation-target="status"></div>
<button class="btn" data-action="click->installation#getDomainInfo">
<span data-installation-target="buttonText">دریافت دامنه‌ها</span>
<span class="loading" data-installation-target="loading" style="display: none;"></span>
</button>
</div>
</div>
<!-- مرحله 3: تنظیمات دیتابیس -->
<div class="step-card step-hidden" data-installation-target="step">
<div class="step-header">
<div>
<h3>مرحله 3: تنظیمات دیتابیس</h3>
<p>اطلاعات دیتابیس جدید را وارد کنید</p>
</div>
<div class="step-number">3</div>
</div>
<div class="step-content">
<div class="form-group">
<label class="form-label">نام دیتابیس</label>
<input type="text" class="form-control" data-installation-target="databaseName" placeholder="hesabix_db">
</div>
<div class="form-group">
<label class="form-label">نام کاربری دیتابیس</label>
<input type="text" class="form-control" data-installation-target="databaseUser" placeholder="hesabix_user">
</div>
<div class="form-group">
<label class="form-label">رمز عبور دیتابیس</label>
<input type="password" class="form-control" data-installation-target="databasePassword" placeholder="رمز عبور قوی">
</div>
<div class="status-message" data-installation-target="status"></div>
<button class="btn" data-action="click->installation#createDatabase">
<span data-installation-target="buttonText">ایجاد دیتابیس</span>
<span class="loading" data-installation-target="loading" style="display: none;"></span>
</button>
</div>
</div>
<!-- مرحله 4: آپلود فایل‌ها -->
<div class="step-card step-hidden" data-installation-target="step">
<div class="step-header">
<div>
<h3>مرحله 4: آپلود فایل‌ها</h3>
<p>فایل‌های حسابیکس در حال دانلود و آپلود هستند</p>
</div>
<div class="step-number">4</div>
</div>
<div class="step-content">
<div class="status-message" data-installation-target="status"></div>
<button class="btn" data-action="click->installation#uploadFiles">
<span data-installation-target="buttonText">شروع آپلود</span>
<span class="loading" data-installation-target="loading" style="display: none;"></span>
</button>
</div>
</div>
<!-- مرحله 5: تنظیمات نهایی -->
<div class="step-card step-hidden" data-installation-target="step">
<div class="step-header">
<div>
<h3>مرحله 5: تنظیمات نهایی</h3>
<p>تنظیم فایل‌های پیکربندی و نهایی‌سازی نصب</p>
</div>
<div class="step-number">5</div>
</div>
<div class="step-content">
<div class="status-message" data-installation-target="status"></div>
<button class="btn" data-action="click->installation#finalizeInstallation">
<span data-installation-target="buttonText">نهایی‌سازی نصب</span>
<span class="loading" data-installation-target="loading" style="display: none;"></span>
</button>
</div>
</div>
<!-- مرحله 6: تکمیل -->
<div class="step-card step-hidden" data-installation-target="step">
<div class="step-header">
<div>
<h3>تکمیل نصب</h3>
<p>حسابیکس با موفقیت نصب شد!</p>
</div>
<div class="step-number">✓</div>
</div>
<div class="step-content">
<div class="status-message status-success" data-installation-target="status">
نصب حسابیکس با موفقیت تکمیل شد!
</div>
<div class="text-center">
<a href="#" data-installation-target="adminUrl" class="btn" target="_blank">
ورود به پنل مدیریت
</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script>
// تست ساده برای بررسی عملکرد
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded');
// تست دسترسی به المان‌ها
const panelType = document.querySelector('[data-installation-target="panelType"]');
const host = document.querySelector('[data-installation-target="host"]');
const username = document.querySelector('[data-installation-target="username"]');
const password = document.querySelector('[data-installation-target="password"]');
console.log('Elements found:', {
panelType: !!panelType,
host: !!host,
username: !!username,
password: !!password
});
});
</script>
{% endblock %}