final stage
This commit is contained in:
+12
-29
@@ -1,17 +1,14 @@
|
|||||||
APP_NAME=Laravel
|
APP_NAME="Glastree"
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
APP_LOCALE=en
|
APP_LOCALE=it
|
||||||
APP_FALLBACK_LOCALE=en
|
APP_FALLBACK_LOCALE=en
|
||||||
APP_FAKER_LOCALE=en_US
|
APP_FAKER_LOCALE=it_IT
|
||||||
|
|
||||||
APP_MAINTENANCE_DRIVER=file
|
APP_MAINTENANCE_DRIVER=file
|
||||||
# APP_MAINTENANCE_STORE=database
|
|
||||||
|
|
||||||
# PHP_CLI_SERVER_WORKERS=4
|
|
||||||
|
|
||||||
BCRYPT_ROUNDS=12
|
BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
@@ -20,14 +17,14 @@ LOG_STACK=single
|
|||||||
LOG_DEPRECATIONS_CHANNEL=null
|
LOG_DEPRECATIONS_CHANNEL=null
|
||||||
LOG_LEVEL=debug
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
DB_CONNECTION=sqlite
|
DB_CONNECTION=mysql
|
||||||
# DB_HOST=127.0.0.1
|
DB_HOST=127.0.0.1
|
||||||
# DB_PORT=3306
|
DB_PORT=3306
|
||||||
# DB_DATABASE=laravel
|
DB_DATABASE=glastree
|
||||||
# DB_USERNAME=root
|
DB_USERNAME=glastree
|
||||||
# DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
|
|
||||||
SESSION_DRIVER=database
|
SESSION_DRIVER=file
|
||||||
SESSION_LIFETIME=120
|
SESSION_LIFETIME=120
|
||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
@@ -35,17 +32,9 @@ SESSION_DOMAIN=null
|
|||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
QUEUE_CONNECTION=database
|
QUEUE_CONNECTION=file
|
||||||
|
|
||||||
CACHE_STORE=database
|
CACHE_STORE=file
|
||||||
# CACHE_PREFIX=
|
|
||||||
|
|
||||||
MEMCACHED_HOST=127.0.0.1
|
|
||||||
|
|
||||||
REDIS_CLIENT=phpredis
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
MAIL_MAILER=log
|
MAIL_MAILER=log
|
||||||
MAIL_SCHEME=null
|
MAIL_SCHEME=null
|
||||||
@@ -56,10 +45,4 @@ MAIL_PASSWORD=null
|
|||||||
MAIL_FROM_ADDRESS="hello@example.com"
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
|
||||||
AWS_BUCKET=
|
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
VITE_APP_NAME="${APP_NAME}"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":2,"defects":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":7,"Tests\\Feature\\StorageRepositoryTest::test_can_create_webdav_repository":8,"Tests\\Feature\\StorageRepositoryTest::test_can_create_google_drive_repository":8,"Tests\\Feature\\StorageRepositoryTest::test_requires_nome_and_tipo":8,"Tests\\Feature\\StorageRepositoryTest::test_can_list_repositories_in_settings":8,"Tests\\Feature\\StorageRepositoryTest::test_can_delete_repository_with_no_documents":7,"Tests\\Feature\\StorageRepositoryTest::test_encrypts_sensitive_config_fields":8,"Tests\\Feature\\StorageRepositoryTest::test_tipo_labels":8,"Tests\\Feature\\StorageRepositoryTest::test_tipo_icons":8,"Tests\\Feature\\StorageRepositoryTest::test_can_reorder_repositories":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_webdav_password":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_google_drive_secrets":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_builds_null_filesystem_for_unknown_tipo":8},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.029,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.003,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.001,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":0.014,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":0.005,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_webdav_password":0.016,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_google_drive_secrets":0.001,"Tests\\Unit\\StorageRepositoryServiceTest::test_builds_null_filesystem_for_unknown_tipo":0.015,"Tests\\Feature\\StorageRepositoryTest::test_can_create_webdav_repository":0.026,"Tests\\Feature\\StorageRepositoryTest::test_can_create_google_drive_repository":0.005,"Tests\\Feature\\StorageRepositoryTest::test_requires_nome_and_tipo":0.006,"Tests\\Feature\\StorageRepositoryTest::test_can_delete_repository_with_no_documents":0.009,"Tests\\Feature\\StorageRepositoryTest::test_tipo_labels":0.001,"Tests\\Feature\\StorageRepositoryTest::test_tipo_icons":0.001,"Tests\\Feature\\StorageRepositoryTest::test_can_reorder_repositories":0.007}}
|
{"version":2,"defects":{"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":8,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":8,"Tests\\Feature\\StorageRepositoryTest::test_can_create_webdav_repository":8,"Tests\\Feature\\StorageRepositoryTest::test_can_create_google_drive_repository":8,"Tests\\Feature\\StorageRepositoryTest::test_requires_nome_and_tipo":8,"Tests\\Feature\\StorageRepositoryTest::test_can_list_repositories_in_settings":8,"Tests\\Feature\\StorageRepositoryTest::test_can_delete_repository_with_no_documents":8,"Tests\\Feature\\StorageRepositoryTest::test_encrypts_sensitive_config_fields":8,"Tests\\Feature\\StorageRepositoryTest::test_tipo_labels":8,"Tests\\Feature\\StorageRepositoryTest::test_tipo_icons":8,"Tests\\Feature\\StorageRepositoryTest::test_can_reorder_repositories":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_webdav_password":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_google_drive_secrets":8,"Tests\\Unit\\StorageRepositoryServiceTest::test_builds_null_filesystem_for_unknown_tipo":8,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":8,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":8,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":8,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":8,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":8,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":8},"times":{"Tests\\Unit\\ExampleTest::test_that_true_is_true":0.001,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.029,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.003,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.001,"Tests\\Feature\\CalendarEventsTest::test_find_fifth_sunday_returns_null_when_not_in_month":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_for_all_months":0.014,"Tests\\Feature\\CalendarEventsTest::test_generate_mensile_events_respects_selected_months":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_annuale_events_produces_correct_dates":0.001,"Tests\\Feature\\CalendarEventsTest::test_generate_settimanale_events_produces_weekly_occurrences":0.005,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_webdav_password":0.016,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_google_drive_secrets":0.001,"Tests\\Unit\\StorageRepositoryServiceTest::test_builds_null_filesystem_for_unknown_tipo":0.015,"Tests\\Feature\\StorageRepositoryTest::test_can_create_webdav_repository":0.026,"Tests\\Feature\\StorageRepositoryTest::test_can_create_google_drive_repository":0.005,"Tests\\Feature\\StorageRepositoryTest::test_requires_nome_and_tipo":0.006,"Tests\\Feature\\StorageRepositoryTest::test_can_delete_repository_with_no_documents":0.009,"Tests\\Feature\\StorageRepositoryTest::test_tipo_labels":0.001,"Tests\\Feature\\StorageRepositoryTest::test_tipo_icons":0.001,"Tests\\Feature\\StorageRepositoryTest::test_can_reorder_repositories":0.007}}
|
||||||
@@ -6,12 +6,22 @@ Laravel 13 / PHP 8.4 project for parish management (Glastree). Full codebase exi
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Issues Open
|
## Issues Open
|
||||||
1. **Drive-Segreteria-ADIM (#3)**: Root ha 0 elementi. Verificare root_folder_id nelle impostazioni repository.
|
*Nessuna issue aperta.*
|
||||||
2. **Nextcloud_MB (#4)**: Connessione "Unauthorized" dopo fix authType — credenziali da verificare (username con @).
|
|
||||||
3. **Nextcloud username**: L'utente ha `@` nel nome utente. Sabre DAV Client usa `CURLOPT_USERPWD` → `user:password` in Basic Auth, il `@` non causa problemi in questo contesto (non è in una URL).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### 01 Giugno 2026 - Guida Docker in Help Page
|
||||||
|
- **Aggiunto**: Nuovo tab "Docker" nella pagina `/help` con guida completa per creare un'immagine Docker
|
||||||
|
- **Contenuto**:
|
||||||
|
- Due opzioni affiancate: SQLite (leggero/demo) e MySQL (standard/produzione)
|
||||||
|
- Dockerfile basato su `php:8.4-apache` con pdo_mysql + pdo_sqlite, composer, asset compilati
|
||||||
|
- Entrypoint script `docker-entrypoint.sh` che rileva automaticamente `DB_CONNECTION` e configura .env di conseguenza; attende MySQL via PDO poll se necessario
|
||||||
|
- Seeder `DockerBaseDataSeeder` con tipologie documenti (7), tipologie eventi (8) e ruoli (8)
|
||||||
|
- Docker Compose in due varianti (Opzione A — SQLite, Opzione B — MySQL con healthcheck e volume separato)
|
||||||
|
- Istruzioni build, avvio con `-f` per scegliere il compose, e creazione primo utente amministratore via CLI
|
||||||
|
- **Files modificati**:
|
||||||
|
- `resources/views/help/index.blade.php`: tab-pane `#help-docker` riscritto con doppia opzione SQLite/MySQL
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## System Status
|
## System Status
|
||||||
@@ -223,8 +233,7 @@ php artisan route:list --name=email
|
|||||||
---
|
---
|
||||||
|
|
||||||
## Known Issues / TODO
|
## Known Issues / TODO
|
||||||
1. SMTP authentication con Infomaniak - verificare credenziali corrette
|
*Nessuna issue nota.*
|
||||||
2. Invio email richiede password SMTP corretta nel form impostazioni
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1002,4 +1011,79 @@ php artisan route:list --name=email
|
|||||||
- `routes/web.php`: aggiunta route `storage-repositories.import`
|
- `routes/web.php`: aggiunta route `storage-repositories.import`
|
||||||
- `resources/views/documenti/index.blade.php`: pulsanti Salva in grid+list, funzione `importRepoFile()`, variabile JS `cartelle`
|
- `resources/views/documenti/index.blade.php`: pulsanti Salva in grid+list, funzione `importRepoFile()`, variabile JS `cartelle`
|
||||||
|
|
||||||
(Last updated: 29 Maggio 2026 - Import file remoto come documento locale)
|
(Last updated: 01 Giugno 2026 - Sistema Backup Completo + Help Header + Script Migrazione)
|
||||||
|
|
||||||
|
### 01 Giugno 2026 - Sistema Backup Completo per Migrazione
|
||||||
|
|
||||||
|
- **Header navbar**: Aggiunta icona `fa-question-circle` che linka a `route('help')` — posizionata nella navbar superiore prima della campanella notifiche
|
||||||
|
- **Sidebar**: Aggiunto link "Backup" sotto Admin (protetto da `canManage('settings')`)
|
||||||
|
- **BackupController** (`app/Http/Controllers/Admin/BackupController.php`): 8 metodi:
|
||||||
|
- `index()` — lista backup + form config
|
||||||
|
- `run()` — esegue backup manuale
|
||||||
|
- `download(filename)` — scarica ZIP
|
||||||
|
- `destroy(filename)` — elimina backup
|
||||||
|
- `updateConfig()` — salva impostazioni backup
|
||||||
|
- `toggleAuto()` — abilita/disabilita backup automatico
|
||||||
|
- `saveAutoConfig()` — salva frequenza e ora backup automatico
|
||||||
|
- `downloadMigrationScript()` — (JSON) restituisce script di migrazione
|
||||||
|
- **Backup view** (`resources/views/admin/backup/index.blade.php`):
|
||||||
|
- Tabella backup con file, data, dimensione, contenuto, azioni (scarica/elimina)
|
||||||
|
- Card configurazione: percorso, ritenzione, toggle files/.env
|
||||||
|
- Card backup automatico: toggle on/off, frequenza (daily/weekly/monthly), ora
|
||||||
|
- Card migrazione: link alla guida
|
||||||
|
- Alert informativo sul cron necessario
|
||||||
|
- **BackupRunCommand** (`app/Console/Commands/BackupRunCommand.php`): `php artisan backup:run` con opzioni `--filename=`, `--no-files`, `--no-env`
|
||||||
|
- **Console scheduler** (`routes/console.php`): backup automatico con `dailyAt()` e `withoutOverlapping()`, controlla `backup_auto_enabled`
|
||||||
|
- **Route backup** (7 rotte sotto `admin/`):
|
||||||
|
- `GET /admin/backup` → backup.index
|
||||||
|
- `POST /admin/backup/run` → backup.run
|
||||||
|
- `GET /admin/backup/download/{filename}` → backup.download
|
||||||
|
- `DELETE /admin/backup/{filename}` → backup.destroy
|
||||||
|
- `POST /admin/backup/config` → backup.updateConfig
|
||||||
|
- `POST /admin/backup/toggle-auto` → backup.toggleAuto
|
||||||
|
- `POST /admin/backup/save-auto` → backup.saveAutoConfig
|
||||||
|
- **Guida help**: Nuovo tab "Backup e Migrazione" con:
|
||||||
|
- Spiegazione contenuto backup (tabella)
|
||||||
|
- Backup via UI e via CLI
|
||||||
|
- Backup automatico e requisiti cron
|
||||||
|
- **⚠️ Avviso APP_KEY**: blocco informativo su rigenerazione
|
||||||
|
- **Procedura migrazione completa** in 12 passi (LAMP)
|
||||||
|
- Riferimento a `installa-glastree.sh`
|
||||||
|
- **PHP Web Installer** (`installer/`):
|
||||||
|
- Sostituisce il vecchio script bash, ora rimosso
|
||||||
|
- **index.php**: entry point — se .env esiste, redirect a login; altrimenti avvia wizard
|
||||||
|
- **setup.php**: wizard multi-step con sessione PHP, routing via `?step=N`
|
||||||
|
- **functions.php**: 15 helper functions
|
||||||
|
- **assets/style.css**: stile AdminLTE-like, responsive
|
||||||
|
- **Step 1 — Benvenuto**: scegli Fresh Install o Restore da Backup (option cards)
|
||||||
|
- **Step 2 — Requisiti**: verifica PHP 8.2+, estensioni (pdo_mysql, mbstring, xml, curl, zip, gd, fileinfo), permessi storage e bootstrap/cache
|
||||||
|
- **Step 3 — Database**: due modalita:
|
||||||
|
- A: Crea nuovo (root MySQL → crea DB + utente)
|
||||||
|
- B: Usa esistente (test PDO → salva .env)
|
||||||
|
- **Step 4 — Installazione**:
|
||||||
|
- Fresh: `key:generate`, `migrate`, `db:seed`, `storage:link` via bootstrap Laravel
|
||||||
|
- Backup: upload ZIP o percorso server → extract, import SQL, copy files, merge .env
|
||||||
|
- **Step 5 — Amministratore**:
|
||||||
|
- Fresh: form creazione super-admin (nome, email, password con validazione)
|
||||||
|
- Backup: mostra utenti esistenti dal DB, opzione salta o crea nuovo
|
||||||
|
- **Step 6 — Finalizzazione**: `config:cache`, `route:cache`, `view:cache`, **self-destruct** (elimina installer/)
|
||||||
|
- **Sicurezza**: self-destruct dopo installazione, blocco re-install se .env esiste, CSRF token, HTTPS warning, password mai esposte in HTML dopo invio
|
||||||
|
- **Migration backup**: `2026_06_01_100155_add_backup_settings_to_app_settings` estesa con `backup_auto_enabled`, `backup_auto_frequency`, `backup_auto_hour`
|
||||||
|
- **.env.example**: aggiornato — default MySQL invece di SQLite, locale it, righe minime essenziali
|
||||||
|
- **Guida Installazione**: nuovo tab nella help page con:
|
||||||
|
- Requisiti server (tabella)
|
||||||
|
- Installazione passo-passo (LAMP, clone, composer, Apache, wizard)
|
||||||
|
- Spiegazione delle due modalita (fresh/backup)
|
||||||
|
- Cosa fare dopo (SSL, configurazione, cron)
|
||||||
|
- Risoluzione problemi comuni (500 error, APP_KEY, mod_rewrite)
|
||||||
|
- **Files creati** (installer/):
|
||||||
|
- `installer/index.php`
|
||||||
|
- `installer/setup.php`
|
||||||
|
- `installer/functions.php`
|
||||||
|
- `installer/assets/style.css`
|
||||||
|
- **Files rimossi**: `installa-glastree.sh`
|
||||||
|
- **Files modificati**:
|
||||||
|
- `.env.example`: default MySQL, pulizia righe superflue
|
||||||
|
- `database/migrations/2026_06_01_100155_add_backup_settings_to_app_settings.php`: +3 colonne auto-backup
|
||||||
|
- `resources/views/help/index.blade.php`: nuovo tab Installazione
|
||||||
|
- **Stato**: ✅ PHP syntax OK su tutti i file. ✅ view:cache OK. ✅ 7 route backup registrate. ✅ `php artisan backup:run` registrato. ✅ installer/ struttura completa.
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\BackupService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class BackupRunCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'backup:run
|
||||||
|
{--filename= : Custom filename prefix for the backup file}
|
||||||
|
{--no-files : Exclude uploaded files from backup}
|
||||||
|
{--no-env : Exclude .env from backup}';
|
||||||
|
|
||||||
|
protected $description = 'Esegue un backup completo del database, file e configurazione';
|
||||||
|
|
||||||
|
private BackupService $backupService;
|
||||||
|
|
||||||
|
public function __construct(BackupService $backupService)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->backupService = $backupService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Avvio backup...');
|
||||||
|
|
||||||
|
if ($this->option('no-files')) {
|
||||||
|
$this->backupService->saveConfig(['backup_include_files' => false]);
|
||||||
|
$this->warn('Files esclusi dal backup.');
|
||||||
|
}
|
||||||
|
if ($this->option('no-env')) {
|
||||||
|
$this->backupService->saveConfig(['backup_include_env' => false]);
|
||||||
|
$this->warn('.env escluso dal backup.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->backupService->run();
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$this->info(' Backup completato con successo!');
|
||||||
|
$this->line('File: ' . $result['filename']);
|
||||||
|
$this->line('Dimensione: ' . $result['size_formatted']);
|
||||||
|
$this->line('Percorso: ' . storage_path('app/backups/' . $result['filename']));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->error(' Backup fallito: ' . ($result['message'] ?? 'Errore sconosciuto'));
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\AppSetting;
|
||||||
|
use App\Services\BackupService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class BackupController extends Controller
|
||||||
|
{
|
||||||
|
private BackupService $backupService;
|
||||||
|
|
||||||
|
public function __construct(BackupService $backupService)
|
||||||
|
{
|
||||||
|
$this->backupService = $backupService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$backups = $this->backupService->list();
|
||||||
|
$config = $this->backupService->getConfig();
|
||||||
|
|
||||||
|
return view('admin.backup.index', compact('backups', 'config'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): RedirectResponse
|
||||||
|
{
|
||||||
|
$result = $this->backupService->run();
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
return redirect()->route('admin.backup.index')
|
||||||
|
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.backup.index')
|
||||||
|
->with('error', $result['message'] ?? 'Errore durante il backup.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(string $filename)
|
||||||
|
{
|
||||||
|
$filepath = $this->backupService->download($filename);
|
||||||
|
|
||||||
|
if (!$filepath) {
|
||||||
|
return redirect()->route('admin.backup.index')
|
||||||
|
->with('error', 'File di backup non trovato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->download($filepath)->deleteFileAfterSend(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(string $filename): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($this->backupService->delete($filename)) {
|
||||||
|
return redirect()->route('admin.backup.index')
|
||||||
|
->with('success', 'Backup "' . $filename . '" eliminato.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->route('admin.backup.index')
|
||||||
|
->with('error', 'Impossibile eliminare il backup.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateConfig(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'backup_path' => 'nullable|string|max:255',
|
||||||
|
'backup_retention_days' => 'nullable|integer|min:1|max:365',
|
||||||
|
'backup_include_files' => 'nullable|boolean',
|
||||||
|
'backup_include_env' => 'nullable|boolean',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->backupService->saveConfig($validated);
|
||||||
|
|
||||||
|
return redirect()->route('admin.backup.index')
|
||||||
|
->with('success', 'Configurazione backup salvata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggleAuto(): RedirectResponse
|
||||||
|
{
|
||||||
|
$enabled = !AppSetting::getSetting('backup_auto_enabled', false);
|
||||||
|
$setting = AppSetting::first() ?? new AppSetting();
|
||||||
|
$setting->backup_auto_enabled = $enabled;
|
||||||
|
$setting->save();
|
||||||
|
|
||||||
|
$msg = $enabled ? 'Backup automatico abilitato.' : 'Backup automatico disabilitato.';
|
||||||
|
return redirect()->route('admin.backup.index')->with('success', $msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveAutoConfig(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$validated = $request->validate([
|
||||||
|
'backup_auto_enabled' => 'nullable|boolean',
|
||||||
|
'backup_auto_frequency' => 'nullable|in:daily,weekly,monthly',
|
||||||
|
'backup_auto_hour' => 'nullable|integer|min:0|max:23',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$setting = AppSetting::first() ?? new AppSetting();
|
||||||
|
$setting->backup_auto_enabled = !empty($validated['backup_auto_enabled']);
|
||||||
|
$setting->backup_auto_frequency = $validated['backup_auto_frequency'] ?? 'daily';
|
||||||
|
$setting->backup_auto_hour = $validated['backup_auto_hour'] ?? 3;
|
||||||
|
$setting->save();
|
||||||
|
|
||||||
|
return redirect()->route('admin.backup.index')
|
||||||
|
->with('success', 'Configurazione backup automatico salvata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadMigrationScript(): JsonResponse
|
||||||
|
{
|
||||||
|
$script = view('admin.backup.migration-script')->render();
|
||||||
|
|
||||||
|
return response()->json(['script' => $script]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -242,23 +242,43 @@ class StorageRepositoryController extends Controller
|
|||||||
|
|
||||||
$normalizedPath = $this->repoService->normalizePath($path);
|
$normalizedPath = $this->repoService->normalizePath($path);
|
||||||
|
|
||||||
try {
|
if ($storageRepository->tipo === 'google_drive') {
|
||||||
$stream = $filesystem->readStream($normalizedPath);
|
try {
|
||||||
} catch (\Exception $e) {
|
$fileData = $this->repoService->readGoogleDriveFile($storageRepository, $normalizedPath, $basename);
|
||||||
Log::error("StorageRepository importToLocal: readStream failed for repo #{$storageRepository->id} path '{$path}': " . $e->getMessage());
|
} catch (\Exception $e) {
|
||||||
return response()->json(['success' => false, 'message' => 'File non trovato nel repository remoto o impossibile da leggere.'], 404);
|
Log::error("StorageRepository importToLocal: readGoogleDriveFile failed for repo #{$storageRepository->id} path '{$path}': " . $e->getMessage());
|
||||||
|
return response()->json(['success' => false, 'message' => 'Impossibile leggere il file da Google Drive: ' . $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$fileData = [];
|
||||||
|
$fileData['stream'] = $filesystem->readStream($normalizedPath);
|
||||||
|
$fileData['mimeType'] = null;
|
||||||
|
try { $fileData['mimeType'] = $filesystem->mimeType($normalizedPath); } catch (\Exception) {}
|
||||||
|
$fileData['fileSize'] = null;
|
||||||
|
try { $fileData['fileSize'] = $filesystem->fileSize($normalizedPath); } catch (\Exception) {}
|
||||||
|
$fileData['basename'] = $basename;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error("StorageRepository importToLocal: readStream failed for repo #{$storageRepository->id} path '{$path}': " . $e->getMessage());
|
||||||
|
return response()->json(['success' => false, 'message' => 'File non trovato nel repository remoto o impossibile da leggere.'], 404);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stream = $fileData['stream'];
|
||||||
if (!$stream) {
|
if (!$stream) {
|
||||||
return response()->json(['success' => false, 'message' => 'Impossibile leggere il file remoto.'], 500);
|
return response()->json(['success' => false, 'message' => 'Impossibile leggere il file remoto.'], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$basename = $fileData['basename'];
|
||||||
|
$mimeType = $fileData['mimeType'] ?? null;
|
||||||
|
$fileSize = $fileData['fileSize'] ?? null;
|
||||||
|
|
||||||
$disk = \App\Models\AppSetting::getDocumentiStorageDisk();
|
$disk = \App\Models\AppSetting::getDocumentiStorageDisk();
|
||||||
$storagePath = \App\Models\AppSetting::getDocumentiStoragePath();
|
$storagePath = \App\Models\AppSetting::getDocumentiStoragePath();
|
||||||
|
|
||||||
$extension = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
|
$extension = strtolower(pathinfo($basename, PATHINFO_EXTENSION));
|
||||||
$nomeSenzaEstensione = pathinfo($basename, PATHINFO_FILENAME);
|
$nomeSenzaEstensione = pathinfo($basename, PATHINFO_FILENAME);
|
||||||
$uniqueName = preg_replace('/[^a-zA-Z0-9_\-\x{80}-\x{FF}]/u', '_', $nomeSenzaEstensione) . '_' . time() . ($extension ? '.' . $extension : '');
|
$uniqueName = preg_replace('/[^a-zA-Z0-9_\-\x80-\x{FF}]/u', '_', $nomeSenzaEstensione) . '_' . time() . ($extension ? '.' . $extension : '');
|
||||||
$filePath = $storagePath . '/' . date('Y/m/d') . '/' . $uniqueName;
|
$filePath = $storagePath . '/' . date('Y/m/d') . '/' . $uniqueName;
|
||||||
|
|
||||||
$stored = \Illuminate\Support\Facades\Storage::disk($disk)->writeStream($filePath, $stream);
|
$stored = \Illuminate\Support\Facades\Storage::disk($disk)->writeStream($filePath, $stream);
|
||||||
@@ -268,11 +288,6 @@ class StorageRepositoryController extends Controller
|
|||||||
return response()->json(['success' => false, 'message' => 'Errore durante il salvataggio locale del file.'], 500);
|
return response()->json(['success' => false, 'message' => 'Errore durante il salvataggio locale del file.'], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$mimeType = null;
|
|
||||||
try { $mimeType = $filesystem->mimeType($path); } catch (\Exception) {}
|
|
||||||
$fileSize = null;
|
|
||||||
try { $fileSize = $filesystem->fileSize($path); } catch (\Exception) {}
|
|
||||||
|
|
||||||
$visibilita = $validated['visibilita'] ?? 'pubblico';
|
$visibilita = $validated['visibilita'] ?? 'pubblico';
|
||||||
$targetType = null;
|
$targetType = null;
|
||||||
$targetId = null;
|
$targetId = null;
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\AppSetting;
|
||||||
|
use App\Models\Notifica;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use RuntimeException;
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
|
class BackupService
|
||||||
|
{
|
||||||
|
private string $backupDir;
|
||||||
|
private string $tempDir;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$basePath = AppSetting::getSetting('backup_path', 'backups');
|
||||||
|
$this->backupDir = storage_path('app/' . ltrim($basePath, '/'));
|
||||||
|
$this->tempDir = storage_path('app/backup-temp');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBackupDir(): string
|
||||||
|
{
|
||||||
|
return $this->backupDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list(): array
|
||||||
|
{
|
||||||
|
if (!is_dir($this->backupDir)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = File::files($this->backupDir);
|
||||||
|
$backups = [];
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file->getExtension() !== 'zip') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = $this->readManifest($file->getPathname());
|
||||||
|
$backups[] = [
|
||||||
|
'filename' => $file->getFilename(),
|
||||||
|
'pathname' => $file->getPathname(),
|
||||||
|
'size' => $file->getSize(),
|
||||||
|
'size_formatted' => $this->formatBytes($file->getSize()),
|
||||||
|
'last_modified' => $file->getMTime(),
|
||||||
|
'manifest' => $manifest,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($backups, fn(array $a, array $b) => $b['last_modified'] - $a['last_modified']);
|
||||||
|
|
||||||
|
return $backups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): array
|
||||||
|
{
|
||||||
|
$result = ['success' => true, 'message' => '', 'filename' => ''];
|
||||||
|
|
||||||
|
try {
|
||||||
|
File::ensureDirectoryExists($this->tempDir, 0755, true);
|
||||||
|
File::ensureDirectoryExists($this->backupDir, 0755, true);
|
||||||
|
|
||||||
|
$this->cleanTemp();
|
||||||
|
|
||||||
|
$timestamp = now()->format('Y-m-d_H-i-s');
|
||||||
|
$appName = str_replace(' ', '_', AppSetting::getAppName() ?? 'app');
|
||||||
|
$filename = $appName . '_' . $timestamp . '.zip';
|
||||||
|
$filepath = $this->backupDir . '/' . $filename;
|
||||||
|
|
||||||
|
$steps = [];
|
||||||
|
|
||||||
|
$steps[] = $this->backupDatabase();
|
||||||
|
$steps[] = $this->backupEnv();
|
||||||
|
$steps[] = $this->backupFiles();
|
||||||
|
$this->writeManifest($timestamp);
|
||||||
|
|
||||||
|
$zipResult = $this->createZip($filepath);
|
||||||
|
if (!$zipResult) {
|
||||||
|
throw new RuntimeException('Errore durante la creazione del file ZIP.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->cleanTemp();
|
||||||
|
|
||||||
|
$size = file_exists($filepath) ? filesize($filepath) : 0;
|
||||||
|
|
||||||
|
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ")");
|
||||||
|
|
||||||
|
$this->cleanupOldBackups();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Backup completato con successo.',
|
||||||
|
'filename' => $filename,
|
||||||
|
'size' => $size,
|
||||||
|
'size_formatted' => $this->formatBytes($size),
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->cleanTemp();
|
||||||
|
Log::error('Backup fallito: ' . $e->getMessage());
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Backup fallito: ' . $e->getMessage(),
|
||||||
|
'filename' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function download(string $filename): ?string
|
||||||
|
{
|
||||||
|
$filepath = $this->backupDir . '/' . basename($filename);
|
||||||
|
return file_exists($filepath) ? $filepath : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(string $filename): bool
|
||||||
|
{
|
||||||
|
$filepath = $this->backupDir . '/' . basename($filename);
|
||||||
|
if (file_exists($filepath)) {
|
||||||
|
unlink($filepath);
|
||||||
|
Log::info("Backup eliminato: {$filename}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'backup_path' => AppSetting::getSetting('backup_path', 'backups'),
|
||||||
|
'backup_retention_days' => (int) AppSetting::getSetting('backup_retention_days', 30),
|
||||||
|
'backup_include_files' => (bool) AppSetting::getSetting('backup_include_files', true),
|
||||||
|
'backup_include_env' => (bool) AppSetting::getSetting('backup_include_env', true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveConfig(array $data): void
|
||||||
|
{
|
||||||
|
$settings = AppSetting::first() ?? new AppSetting();
|
||||||
|
$settings->backup_path = trim($data['backup_path'] ?? 'backups', '/');
|
||||||
|
$settings->backup_retention_days = max(1, (int) ($data['backup_retention_days'] ?? 30));
|
||||||
|
$settings->backup_include_files = !empty($data['backup_include_files']);
|
||||||
|
$settings->backup_include_env = !empty($data['backup_include_env']);
|
||||||
|
$settings->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backupDatabase(): string
|
||||||
|
{
|
||||||
|
$dbName = config('database.connections.mysql.database');
|
||||||
|
$dbUser = config('database.connections.mysql.username');
|
||||||
|
$dbPass = config('database.connections.mysql.password');
|
||||||
|
$dbHost = config('database.connections.mysql.host');
|
||||||
|
$dbPort = config('database.connections.mysql.port');
|
||||||
|
|
||||||
|
$sqlFile = $this->tempDir . '/database.sql';
|
||||||
|
|
||||||
|
$command = sprintf(
|
||||||
|
'mysqldump --host=%s --port=%s --user=%s --password=%s --routines --events --single-transaction --quick %s > %s 2>&1',
|
||||||
|
escapeshellarg($dbHost),
|
||||||
|
escapeshellarg($dbPort),
|
||||||
|
escapeshellarg($dbUser),
|
||||||
|
escapeshellarg($dbPass),
|
||||||
|
escapeshellarg($dbName),
|
||||||
|
escapeshellarg($sqlFile)
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = null;
|
||||||
|
$exitCode = null;
|
||||||
|
exec($command, $output, $exitCode);
|
||||||
|
|
||||||
|
if ($exitCode !== 0 || !file_exists($sqlFile) || filesize($sqlFile) === 0) {
|
||||||
|
throw new RuntimeException('mysqldump fallito: ' . implode("\n", (array) $output));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backupEnv(): string
|
||||||
|
{
|
||||||
|
if (!AppSetting::getSetting('backup_include_env', true)) {
|
||||||
|
return '.env (escluso)';
|
||||||
|
}
|
||||||
|
|
||||||
|
$envPath = base_path('.env');
|
||||||
|
$envKeyPath = base_path('.env.key');
|
||||||
|
|
||||||
|
if (file_exists($envPath)) {
|
||||||
|
copy($envPath, $this->tempDir . '/.env');
|
||||||
|
}
|
||||||
|
if (file_exists($envKeyPath)) {
|
||||||
|
copy($envKeyPath, $this->tempDir . '/.env.key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '.env copiato';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backupFiles(): string
|
||||||
|
{
|
||||||
|
if (!AppSetting::getSetting('backup_include_files', true)) {
|
||||||
|
return 'files (esclusi)';
|
||||||
|
}
|
||||||
|
|
||||||
|
$filesDir = AppSetting::getDocumentiStorageAbsolutePath();
|
||||||
|
$targetDir = $this->tempDir . '/files';
|
||||||
|
|
||||||
|
if (!is_dir($filesDir)) {
|
||||||
|
return 'files (cartella non trovata)';
|
||||||
|
}
|
||||||
|
|
||||||
|
File::ensureDirectoryExists($targetDir, 0755, true);
|
||||||
|
|
||||||
|
File::copyDirectory($filesDir, $targetDir);
|
||||||
|
|
||||||
|
return 'files copiati';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function writeManifest(string $timestamp): void
|
||||||
|
{
|
||||||
|
$manifest = [
|
||||||
|
'created_at' => $timestamp,
|
||||||
|
'app_version' => AppSetting::getAppVersion() ?? 'unknown',
|
||||||
|
'app_name' => AppSetting::getAppName() ?? 'Glastree',
|
||||||
|
'db_driver' => config('database.default'),
|
||||||
|
'db_name' => config('database.connections.mysql.database'),
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'laravel_version' => app()->version(),
|
||||||
|
];
|
||||||
|
|
||||||
|
File::put($this->tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readManifest(string $zipPath): ?array
|
||||||
|
{
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipPath) === true) {
|
||||||
|
$manifestContent = $zip->getFromName('manifest.json');
|
||||||
|
$zip->close();
|
||||||
|
if ($manifestContent !== false) {
|
||||||
|
return json_decode($manifestContent, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createZip(string $filepath): bool
|
||||||
|
{
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($filepath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = File::allFiles($this->tempDir);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$relativePath = substr($file->getPathname(), strlen($this->tempDir) + 1);
|
||||||
|
$zip->addFile($file->getPathname(), $relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $zip->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanTemp(): void
|
||||||
|
{
|
||||||
|
if (is_dir($this->tempDir)) {
|
||||||
|
File::deleteDirectory($this->tempDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanupOldBackups(): void
|
||||||
|
{
|
||||||
|
$retentionDays = (int) AppSetting::getSetting('backup_retention_days', 30);
|
||||||
|
$cutoff = now()->subDays($retentionDays)->timestamp;
|
||||||
|
|
||||||
|
foreach (File::files($this->backupDir) as $file) {
|
||||||
|
if ($file->getExtension() === 'zip' && $file->getMTime() < $cutoff) {
|
||||||
|
unlink($file->getPathname());
|
||||||
|
Log::info("Backup eliminato per retention: " . $file->getFilename());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatBytes(int $bytes): string
|
||||||
|
{
|
||||||
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
$i = 0;
|
||||||
|
while ($bytes >= 1024 && $i < count($units) - 1) {
|
||||||
|
$bytes /= 1024;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
return round($bytes, 2) . ' ' . $units[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,6 +45,11 @@ class StorageRepositoryService
|
|||||||
if (empty($config['client_id']) || empty($config['client_secret']) || empty($config['refresh_token'])) {
|
if (empty($config['client_id']) || empty($config['client_secret']) || empty($config['refresh_token'])) {
|
||||||
return ['success' => false, 'message' => 'Configurazione incompleta: client_id, client_secret e refresh_token sono necessari per Google Drive.'];
|
return ['success' => false, 'message' => 'Configurazione incompleta: client_id, client_secret e refresh_token sono necessari per Google Drive.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$apiError = $this->checkGoogleDriveApiStatus($config);
|
||||||
|
if ($apiError) {
|
||||||
|
return ['success' => false, 'message' => $apiError];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$filesystem = $this->buildFilesystem($repo);
|
$filesystem = $this->buildFilesystem($repo);
|
||||||
@@ -61,6 +66,42 @@ class StorageRepositoryService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkGoogleDriveApiStatus(array $config): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$client = new \Google_Client();
|
||||||
|
$client->setClientId($config['client_id']);
|
||||||
|
$client->setClientSecret($config['client_secret']);
|
||||||
|
$client->addScope(\Google_Service_Drive::DRIVE);
|
||||||
|
$token = $client->refreshToken($config['refresh_token']);
|
||||||
|
$client->setAccessToken($token);
|
||||||
|
|
||||||
|
$service = new \Google_Service_Drive($client);
|
||||||
|
|
||||||
|
$rootId = $config['root_folder_id'] ?? 'root';
|
||||||
|
$service->files->get($rootId, ['fields' => 'id']);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (\Google\Service\Exception $e) {
|
||||||
|
$errors = $e->getErrors();
|
||||||
|
$reason = $errors[0]['reason'] ?? '';
|
||||||
|
$message = $errors[0]['message'] ?? $e->getMessage();
|
||||||
|
|
||||||
|
if ($reason === 'accessNotConfigured' || str_contains($message, 'Drive API has not been used')) {
|
||||||
|
$projectNumber = explode('-', $config['client_id'] ?? '')[0] ?? '';
|
||||||
|
return 'Google Drive API non abilitata. Vai su https://console.developers.google.com/apis/api/drive.googleapis.com/overview?project=' . $projectNumber . ' e clicca "Enable".';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($e->getCode() === 404) {
|
||||||
|
return 'Cartella root non trovata (ID: ' . ($config['root_folder_id'] ?? 'root') . '). Verifica che il folder ID sia corretto.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Errore API Google Drive: ' . $message;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return 'Errore di connessione Google Drive: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function listContents(StorageRepository $repo, string $path = '/'): array
|
public function listContents(StorageRepository $repo, string $path = '/'): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -198,4 +239,126 @@ class StorageRepositoryService
|
|||||||
|
|
||||||
return new Filesystem($adapter);
|
return new Filesystem($adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function readGoogleDriveFile(StorageRepository $repo, string $normalizedPath, string $basename): array
|
||||||
|
{
|
||||||
|
$config = $repo->getDecryptedConfig();
|
||||||
|
|
||||||
|
$client = new \Google_Client();
|
||||||
|
$client->setClientId($config['client_id']);
|
||||||
|
$client->setClientSecret($config['client_secret']);
|
||||||
|
$client->addScope(\Google_Service_Drive::DRIVE);
|
||||||
|
$token = $client->refreshToken($config['refresh_token']);
|
||||||
|
$client->setAccessToken($token);
|
||||||
|
|
||||||
|
$service = new \Google_Service_Drive($client);
|
||||||
|
$rootFolderId = $config['root_folder_id'] ?? 'root';
|
||||||
|
|
||||||
|
$fileId = $this->findGoogleDriveFileId($service, $rootFolderId, $normalizedPath);
|
||||||
|
$file = $service->files->get($fileId, ['fields' => 'id,name,mimeType,size']);
|
||||||
|
|
||||||
|
$mimeType = $file->getMimeType();
|
||||||
|
|
||||||
|
if ($mimeType && str_starts_with($mimeType, 'application/vnd.google-apps.')) {
|
||||||
|
return $this->exportGoogleDriveDoc($service, $fileId, $mimeType, $basename);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $service->files->get($fileId, ['alt' => 'media']);
|
||||||
|
$body = $response->getBody();
|
||||||
|
$stream = $body instanceof \Psr\Http\Message\StreamInterface ? $body->detach() : null;
|
||||||
|
|
||||||
|
if (!$stream) {
|
||||||
|
$stream = fopen('php://temp', 'r+');
|
||||||
|
fwrite($stream, (string) $body);
|
||||||
|
rewind($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileSize = $file->getSize();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'stream' => $stream,
|
||||||
|
'mimeType' => $mimeType,
|
||||||
|
'basename' => $basename,
|
||||||
|
'fileSize' => $fileSize !== null ? (int) $fileSize : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function exportGoogleDriveDoc(\Google_Service_Drive $service, string $fileId, string $googleMimeType, string $originalBasename): array
|
||||||
|
{
|
||||||
|
$exportMap = [
|
||||||
|
'application/vnd.google-apps.document' => [
|
||||||
|
'mime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'ext' => 'docx',
|
||||||
|
],
|
||||||
|
'application/vnd.google-apps.spreadsheet' => [
|
||||||
|
'mime' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
'ext' => 'xlsx',
|
||||||
|
],
|
||||||
|
'application/vnd.google-apps.presentation' => [
|
||||||
|
'mime' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'ext' => 'pptx',
|
||||||
|
],
|
||||||
|
'application/vnd.google-apps.drawing' => [
|
||||||
|
'mime' => 'image/png',
|
||||||
|
'ext' => 'png',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$format = $exportMap[$googleMimeType] ?? ['mime' => 'application/pdf', 'ext' => 'pdf'];
|
||||||
|
|
||||||
|
$response = $service->files->export($fileId, $format['mime'], ['alt' => 'media']);
|
||||||
|
$body = $response->getBody();
|
||||||
|
$stream = $body instanceof \Psr\Http\Message\StreamInterface ? $body->detach() : null;
|
||||||
|
|
||||||
|
if (!$stream) {
|
||||||
|
$stream = fopen('php://temp', 'r+');
|
||||||
|
fwrite($stream, (string) $body);
|
||||||
|
rewind($stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
$nameWithoutExt = pathinfo($originalBasename, PATHINFO_FILENAME);
|
||||||
|
$newBasename = $nameWithoutExt . '.' . $format['ext'];
|
||||||
|
|
||||||
|
Log::info("StorageRepository: exported Google Drive file {$fileId} from {$googleMimeType} to {$format['mime']} as {$newBasename}");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'stream' => $stream,
|
||||||
|
'mimeType' => $format['mime'],
|
||||||
|
'basename' => $newBasename,
|
||||||
|
'fileSize' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findGoogleDriveFileId(\Google_Service_Drive $service, string $rootFolderId, string $path): string
|
||||||
|
{
|
||||||
|
$path = trim($path, '/');
|
||||||
|
if ($path === '') {
|
||||||
|
return $rootFolderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode('/', $path);
|
||||||
|
$currentParentId = $rootFolderId;
|
||||||
|
|
||||||
|
foreach ($parts as $name) {
|
||||||
|
$optParams = [
|
||||||
|
'q' => sprintf("trashed = false and '%s' in parents and name = '%s'",
|
||||||
|
str_replace("'", "\\'", $currentParentId),
|
||||||
|
str_replace("'", "\\'", $name)
|
||||||
|
),
|
||||||
|
'fields' => 'files(id, name, mimeType)',
|
||||||
|
'pageSize' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
$results = $service->files->listFiles($optParams);
|
||||||
|
$files = $results->getFiles();
|
||||||
|
|
||||||
|
if (empty($files)) {
|
||||||
|
throw new \RuntimeException("Elemento non trovato su Google Drive: {$name} (path: {$path})");
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentParentId = $files[0]->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $currentParentId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) {
|
||||||
|
$table->string('backup_path')->default('backups')->after('show_version');
|
||||||
|
$table->integer('backup_retention_days')->default(30)->after('backup_path');
|
||||||
|
$table->boolean('backup_include_files')->default(true)->after('backup_retention_days');
|
||||||
|
$table->boolean('backup_include_env')->default(true)->after('backup_include_files');
|
||||||
|
$table->boolean('backup_auto_enabled')->default(false)->after('backup_include_env');
|
||||||
|
$table->string('backup_auto_frequency', 20)->default('daily')->after('backup_auto_enabled');
|
||||||
|
$table->integer('backup_auto_hour')->default(3)->after('backup_auto_frequency');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('app_settings', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['backup_path', 'backup_retention_days', 'backup_include_files', 'backup_include_env', 'backup_auto_enabled', 'backup_auto_frequency', 'backup_auto_hour']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
/* =============================================================================
|
||||||
|
Glastree Installer - Styles
|
||||||
|
============================================================================= */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary: #007bff;
|
||||||
|
--primary-dark: #0056b3;
|
||||||
|
--success: #28a745;
|
||||||
|
--danger: #dc3545;
|
||||||
|
--warning: #ffc107;
|
||||||
|
--info: #17a2b8;
|
||||||
|
--dark: #343a40;
|
||||||
|
--light: #f8f9fa;
|
||||||
|
--border: #dee2e6;
|
||||||
|
--text: #212529;
|
||||||
|
--text-muted: #6c757d;
|
||||||
|
--radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text);
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.installer-header {
|
||||||
|
background: linear-gradient(135deg, #343a40 0%, #212529 100%);
|
||||||
|
color: #fff;
|
||||||
|
padding: 30px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installer-header h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.installer-header .subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.75;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step indicator */
|
||||||
|
.step-indicator {
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body h2 {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body h4 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body h5 {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0,123,255,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 22px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
padding: 14px 32px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Option cards */
|
||||||
|
.option-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,123,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card.selected {
|
||||||
|
border-color: var(--primary);
|
||||||
|
background: rgba(0,123,255,0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card .option-radio {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-icon {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card h3 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card p {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card ul {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-left: 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card ul li {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Requirements */
|
||||||
|
.requirements-list {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement-item.passed {
|
||||||
|
background: #f0fff4;
|
||||||
|
border-color: #b8e6c8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement-item.failed {
|
||||||
|
background: #fff5f5;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.req-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement-item.passed .req-icon { color: var(--success); }
|
||||||
|
.requirement-item.failed .req-icon { color: var(--danger); }
|
||||||
|
|
||||||
|
.req-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.req-status {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement-item.passed .req-status {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.requirement-item.failed .req-status {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.req-msg {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.alert {
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffeeba;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-info {
|
||||||
|
background: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert ul {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log list */
|
||||||
|
.log-list {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
background: var(--light);
|
||||||
|
border-left: 3px solid var(--success);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results list */
|
||||||
|
.results-list {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item.success {
|
||||||
|
background: #f0fff4;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item.error {
|
||||||
|
background: #fff5f5;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Final box */
|
||||||
|
.final-box {
|
||||||
|
background: var(--light);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-box h3 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-box ol,
|
||||||
|
.final-box ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.d-block {
|
||||||
|
display: block;
|
||||||
|
background: #f4f4f4;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-light {
|
||||||
|
background: var(--light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-2 {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-3 {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-2 { margin-top: 8px; }
|
||||||
|
.mt-3 { margin-top: 16px; }
|
||||||
|
.mb-0 { margin-bottom: 0; }
|
||||||
|
.mb-2 { margin-bottom: 8px; }
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-admin {
|
||||||
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Icons via unicode/emoji fallback since no icon font */
|
||||||
|
.icon-ok::before { content: "\2714 "; }
|
||||||
|
.icon-error::before { content: "\2718 "; }
|
||||||
|
.icon-warning::before { content: "\26A0 "; }
|
||||||
|
.icon-rocket::before { content: "\1F680 "; }
|
||||||
|
.icon-upload::before { content: "\2191 "; }
|
||||||
|
.icon-tree::before { content: "\2328 "; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.option-group {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.form-actions .btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.installer-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Glastree Installer - Helper Functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
function getBaseUrl(): string
|
||||||
|
{
|
||||||
|
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
$scriptDir = dirname($_SERVER['SCRIPT_NAME']);
|
||||||
|
$scriptDir = str_replace('/installer', '', $scriptDir);
|
||||||
|
return $scheme . '://' . $host . $scriptDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
function siteUrl(string $path = ''): string
|
||||||
|
{
|
||||||
|
return rtrim(getBaseUrl(), '/') . '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirect(string $url): never
|
||||||
|
{
|
||||||
|
header('Location: ' . $url);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEnvFileExists(): bool
|
||||||
|
{
|
||||||
|
return file_exists(dirname(__DIR__) . '/.env');
|
||||||
|
}
|
||||||
|
|
||||||
|
function e(mixed $value): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function old(string $key, mixed $default = ''): string
|
||||||
|
{
|
||||||
|
return e($_POST[$key] ?? $_SESSION['form_data'][$key] ?? $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkRequirements(): array
|
||||||
|
{
|
||||||
|
$checks = [];
|
||||||
|
|
||||||
|
// PHP version
|
||||||
|
$checks[] = [
|
||||||
|
'name' => 'PHP 8.2+',
|
||||||
|
'passed' => PHP_VERSION_ID >= 80200,
|
||||||
|
'message' => PHP_VERSION_ID >= 80200 ? 'PHP ' . PHP_VERSION : 'Versione attuale: PHP ' . PHP_VERSION . '. Necessaria 8.2+',
|
||||||
|
'critical' => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
$extensions = [
|
||||||
|
'pdo_mysql' => 'PDO MySQL',
|
||||||
|
'mbstring' => 'MBString',
|
||||||
|
'xml' => 'XML',
|
||||||
|
'curl' => 'cURL',
|
||||||
|
'zip' => 'Zip',
|
||||||
|
'gd' => 'GD (immagini)',
|
||||||
|
'fileinfo' => 'Fileinfo',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($extensions as $ext => $label) {
|
||||||
|
$checks[] = [
|
||||||
|
'name' => $label,
|
||||||
|
'passed' => extension_loaded($ext),
|
||||||
|
'message' => extension_loaded($ext) ? 'Caricata' : 'Estensione "' . $ext . '" non trovata',
|
||||||
|
'critical' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory permissions
|
||||||
|
$dirs = [
|
||||||
|
'storage' => dirname(__DIR__) . '/storage',
|
||||||
|
'bootstrap/cache' => dirname(__DIR__) . '/bootstrap/cache',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dirs as $label => $dir) {
|
||||||
|
$writable = is_dir($dir) && is_writable($dir);
|
||||||
|
$checks[] = [
|
||||||
|
'name' => $label . '/ scrivibile',
|
||||||
|
'passed' => $writable,
|
||||||
|
'message' => $writable ? 'Permessi OK' : 'Cartella non scrivibile. Esegui: chmod -R 775 ' . $dir,
|
||||||
|
'critical' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// mod_rewrite (suggested)
|
||||||
|
$checks[] = [
|
||||||
|
'name' => 'mod_rewrite (Apache)',
|
||||||
|
'passed' => true,
|
||||||
|
'message' => 'Raccomandato per URL puliti. Verifica configurazione Apache.',
|
||||||
|
'critical' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
// HTTPS
|
||||||
|
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on';
|
||||||
|
$checks[] = [
|
||||||
|
'name' => 'HTTPS attivo',
|
||||||
|
'passed' => $isHttps,
|
||||||
|
'message' => $isHttps ? 'Connessione sicura' : 'Connessione non crittata. Configura SSL in produzione.',
|
||||||
|
'critical' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allRequirementsMet(array $checks): bool
|
||||||
|
{
|
||||||
|
foreach ($checks as $check) {
|
||||||
|
if (!empty($check['critical']) && !$check['passed']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDatabaseConnection(string $host, int $port, string $database, string $user, string $pass): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dsn = "mysql:host={$host};port={$port};dbname={$database};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_TIMEOUT => 5,
|
||||||
|
]);
|
||||||
|
return ['success' => true, 'message' => 'Connessione riuscita.'];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRootConnection(string $host, int $port, string $user, string $pass): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$dsn = "mysql:host={$host};port={$port};charset=utf8mb4";
|
||||||
|
$pdo = new PDO($dsn, $user, $pass, [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_TIMEOUT => 5,
|
||||||
|
]);
|
||||||
|
return ['success' => true, 'message' => 'Connessione root riuscita.', 'pdo' => $pdo];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDatabaseAndUser(string $rootUser, string $rootPass, string $host, int $port, string $dbName, string $dbUser, string $dbPass): array
|
||||||
|
{
|
||||||
|
$result = testRootConnection($host, $port, $rootUser, $rootPass);
|
||||||
|
if (!$result['success']) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = $result['pdo'];
|
||||||
|
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$dbName}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||||
|
$pdo->exec("CREATE USER IF NOT EXISTS '{$dbUser}'@'localhost' IDENTIFIED BY '{$dbPass}'");
|
||||||
|
$pdo->exec("GRANT ALL ON `{$dbName}`.* TO '{$dbUser}'@'localhost'");
|
||||||
|
$pdo->exec("FLUSH PRIVILEGES");
|
||||||
|
return ['success' => true, 'message' => 'Database e utente creati con successo.'];
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
return ['success' => false, 'message' => 'Errore: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeEnvFile(array $params): bool
|
||||||
|
{
|
||||||
|
$rootDir = dirname(__DIR__);
|
||||||
|
$examplePath = $rootDir . '/.env.example';
|
||||||
|
$envPath = $rootDir . '/.env';
|
||||||
|
|
||||||
|
if (!file_exists($examplePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($examplePath);
|
||||||
|
|
||||||
|
$replacements = [
|
||||||
|
'APP_NAME=' => 'APP_NAME=' . ($params['app_name'] ?? 'Glastree'),
|
||||||
|
'APP_ENV=' => 'APP_ENV=production',
|
||||||
|
'APP_KEY=' => 'APP_KEY=',
|
||||||
|
'APP_DEBUG=' => 'APP_DEBUG=false',
|
||||||
|
'APP_URL=' => 'APP_URL=' . ($params['app_url'] ?? 'http://localhost'),
|
||||||
|
'DB_CONNECTION=' => 'DB_CONNECTION=mysql',
|
||||||
|
'# DB_HOST=' => 'DB_HOST=',
|
||||||
|
'# DB_PORT=' => 'DB_PORT=',
|
||||||
|
'# DB_DATABASE=' => 'DB_DATABASE=',
|
||||||
|
'# DB_USERNAME=' => 'DB_USERNAME=',
|
||||||
|
'# DB_PASSWORD=' => 'DB_PASSWORD=',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($replacements as $search => $replace) {
|
||||||
|
if (str_starts_with($content, $search) || preg_match('/^' . preg_quote($search, '/') . '/m', $content)) {
|
||||||
|
$content = preg_replace(
|
||||||
|
'/^' . preg_quote($search, '/') . '.*$/m',
|
||||||
|
$replace . ($params['db_host'] ?? '127.0.0.1'),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific DB replacements (handle commented lines)
|
||||||
|
$content = preg_replace(
|
||||||
|
'/^# DB_HOST=.*$/m',
|
||||||
|
'DB_HOST=' . ($params['db_host'] ?? '127.0.0.1'),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/^# DB_PORT=.*$/m',
|
||||||
|
'DB_PORT=' . ($params['db_port'] ?? '3306'),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/^# DB_DATABASE=.*$/m',
|
||||||
|
'DB_DATABASE=' . ($params['db_database'] ?? 'glastree'),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/^# DB_USERNAME=.*$/m',
|
||||||
|
'DB_USERNAME=' . ($params['db_username'] ?? 'glastree'),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
$content = preg_replace(
|
||||||
|
'/^# DB_PASSWORD=.*$/m',
|
||||||
|
'DB_PASSWORD=' . ($params['db_password'] ?? ''),
|
||||||
|
$content
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure APP_KEY= line exists
|
||||||
|
if (!preg_match('/^APP_KEY=/m', $content)) {
|
||||||
|
$content .= "\nAPP_KEY=\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_put_contents($envPath, $content) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootstrapLaravel(): object
|
||||||
|
{
|
||||||
|
$rootDir = dirname(__DIR__);
|
||||||
|
require_once $rootDir . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
$app = require $rootDir . '/bootstrap/app.php';
|
||||||
|
$kernel = $app->make(\Illuminate\Contracts\Console\Kernel::class);
|
||||||
|
|
||||||
|
return $kernel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runArtisanCommands(object $kernel, array $commands): array
|
||||||
|
{
|
||||||
|
$results = [];
|
||||||
|
foreach ($commands as $command => $options) {
|
||||||
|
try {
|
||||||
|
$exitCode = $kernel->call($command, $options ?? []);
|
||||||
|
$results[] = [
|
||||||
|
'command' => $command,
|
||||||
|
'success' => $exitCode === 0,
|
||||||
|
'output' => $kernel->output(),
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$results[] = [
|
||||||
|
'command' => $command,
|
||||||
|
'success' => false,
|
||||||
|
'output' => $e->getMessage(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFromBackup(string $zipPath, string $dbUser, string $dbPass, string $dbName): array
|
||||||
|
{
|
||||||
|
$rootDir = dirname(__DIR__);
|
||||||
|
$restoreDir = sys_get_temp_dir() . '/glastree-restore-' . uniqid();
|
||||||
|
|
||||||
|
if (!file_exists($zipPath)) {
|
||||||
|
return ['success' => false, 'message' => 'File ZIP non trovato: ' . $zipPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
if ($zip->open($zipPath) !== true) {
|
||||||
|
return ['success' => false, 'message' => 'Impossibile aprire il file ZIP.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->extractTo($restoreDir);
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$log = [];
|
||||||
|
|
||||||
|
// 1. Import database.sql
|
||||||
|
$sqlFile = $restoreDir . '/database.sql';
|
||||||
|
if (file_exists($sqlFile)) {
|
||||||
|
$cmd = sprintf(
|
||||||
|
'mysql --host=%s --port=%s --user=%s --password=%s %s < %s 2>&1',
|
||||||
|
escapeshellarg($_SESSION['db_host'] ?? '127.0.0.1'),
|
||||||
|
escapeshellarg((string)($_SESSION['db_port'] ?? '3306')),
|
||||||
|
escapeshellarg($dbUser),
|
||||||
|
escapeshellarg($dbPass),
|
||||||
|
escapeshellarg($dbName),
|
||||||
|
escapeshellarg($sqlFile)
|
||||||
|
);
|
||||||
|
exec($cmd, $output, $exitCode);
|
||||||
|
if ($exitCode === 0) {
|
||||||
|
$log[] = 'Database importato con successo.';
|
||||||
|
} else {
|
||||||
|
$log[] = 'Errore import database: ' . implode("\n", $output);
|
||||||
|
return ['success' => false, 'message' => 'Import database fallito.', 'log' => $log];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$log[] = 'database.sql non trovato nel backup.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Copy files to storage
|
||||||
|
$filesDir = $restoreDir . '/files';
|
||||||
|
$targetDir = $rootDir . '/storage/app/documenti';
|
||||||
|
if (is_dir($filesDir)) {
|
||||||
|
if (!is_dir($targetDir)) {
|
||||||
|
mkdir($targetDir, 0755, true);
|
||||||
|
}
|
||||||
|
$files = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($filesDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::SELF_FIRST
|
||||||
|
);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$relative = substr($file->getPathname(), strlen($filesDir) + 1);
|
||||||
|
$target = $targetDir . '/' . $relative;
|
||||||
|
if ($file->isDir()) {
|
||||||
|
if (!is_dir($target)) {
|
||||||
|
mkdir($target, 0755, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
copy($file->getPathname(), $target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$log[] = 'File documenti ripristinati.';
|
||||||
|
} else {
|
||||||
|
$log[] = 'Cartella files non trovata nel backup.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Merge .env if present (preserving current DB credentials)
|
||||||
|
$envBackup = $restoreDir . '/.env';
|
||||||
|
if (file_exists($envBackup)) {
|
||||||
|
$currentEnv = file_get_contents($envBackup);
|
||||||
|
$currentEnv = preg_replace('/^DB_HOST=.*$/m', 'DB_HOST=' . ($_SESSION['db_host'] ?? '127.0.0.1'), $currentEnv);
|
||||||
|
$currentEnv = preg_replace('/^DB_PORT=.*$/m', 'DB_PORT=' . ($_SESSION['db_port'] ?? '3306'), $currentEnv);
|
||||||
|
$currentEnv = preg_replace('/^DB_DATABASE=.*$/m', 'DB_DATABASE=' . $dbName, $currentEnv);
|
||||||
|
$currentEnv = preg_replace('/^DB_USERNAME=.*$/m', 'DB_USERNAME=' . $dbUser, $currentEnv);
|
||||||
|
$currentEnv = preg_replace('/^DB_PASSWORD=.*$/m', 'DB_PASSWORD=' . $dbPass, $currentEnv);
|
||||||
|
|
||||||
|
// Only merge if APP_URL differs from localhost
|
||||||
|
if (str_contains($currentEnv, 'APP_URL=http://localhost') || str_contains($currentEnv, 'APP_URL=')) {
|
||||||
|
$currentEnv = preg_replace(
|
||||||
|
'/^APP_URL=.*$/m',
|
||||||
|
'APP_URL=' . getBaseUrl(),
|
||||||
|
$currentEnv
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($rootDir . '/.env', $currentEnv);
|
||||||
|
$log[] = '.env ripristinato dal backup (credenziali DB preservate).';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Copy manifest if exists
|
||||||
|
if (file_exists($restoreDir . '/manifest.json')) {
|
||||||
|
$log[] = 'Manifest: ' . file_get_contents($restoreDir . '/manifest.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup temp
|
||||||
|
$it = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($restoreDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
foreach ($it as $file) {
|
||||||
|
$file->isDir() ? rmdir($file->getPathname()) : unlink($file->getPathname());
|
||||||
|
}
|
||||||
|
rmdir($restoreDir);
|
||||||
|
|
||||||
|
return ['success' => true, 'message' => 'Backup ripristinato con successo.', 'log' => $log];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSuperAdmin(string $name, string $email, string $password): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$kernel = bootstrapLaravel();
|
||||||
|
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\DiocesiSeeder']);
|
||||||
|
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\ComuniSeeder']);
|
||||||
|
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\RoleSeeder']);
|
||||||
|
$kernel->call('db:seed', ['--force' => true, '--class' => 'Database\\Seeders\\RolePresetSeeder']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Seeder might fail if already seeded, that's OK
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$user = new \App\Models\User();
|
||||||
|
$user->name = $name;
|
||||||
|
$user->email = $email;
|
||||||
|
$user->password = \Illuminate\Support\Facades\Hash::make($password);
|
||||||
|
$user->is_admin = true;
|
||||||
|
$user->status = 'active';
|
||||||
|
$user->role_preset_id = 1;
|
||||||
|
$user->permissions = [
|
||||||
|
'individui' => 2,
|
||||||
|
'gruppi' => 2,
|
||||||
|
'eventi' => 2,
|
||||||
|
'documenti' => 2,
|
||||||
|
'mailing' => 2,
|
||||||
|
'viste' => 2,
|
||||||
|
'report' => 2,
|
||||||
|
'settings' => 2,
|
||||||
|
];
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return ['success' => true, 'message' => 'Amministratore creato con successo.'];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ['success' => false, 'message' => 'Errore creazione admin: ' . $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExistingUsers(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$kernel = bootstrapLaravel();
|
||||||
|
$users = \App\Models\User::all(['id', 'name', 'email', 'is_admin'])->toArray();
|
||||||
|
return $users;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfDestruct(): bool
|
||||||
|
{
|
||||||
|
$installerDir = __DIR__;
|
||||||
|
|
||||||
|
if (!is_dir($installerDir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$it = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($installerDir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||||
|
RecursiveIteratorIterator::CHILD_FIRST
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($it as $file) {
|
||||||
|
if ($file->isDir()) {
|
||||||
|
rmdir($file->getPathname());
|
||||||
|
} else {
|
||||||
|
unlink($file->getPathname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rmdir($installerDir);
|
||||||
|
|
||||||
|
return !is_dir($installerDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeader(string $title, string $step = ''): void
|
||||||
|
{
|
||||||
|
$stepLabel = $step ? " — Step {$step}" : '';
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="it">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title><?= e($title) ?> — Glastree Installer</title>
|
||||||
|
<link rel="stylesheet" href="assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="installer-wrapper">
|
||||||
|
<div class="installer-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1><i class="icon-tree"></i> Glastree</h1>
|
||||||
|
<p class="subtitle">Installazione Wizard</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<?php if ($step): ?>
|
||||||
|
<div class="step-indicator">Passo <?= e($step) ?> di 6</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFooter(): void
|
||||||
|
{
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="installer-footer">
|
||||||
|
<div class="container">
|
||||||
|
<p>Glastree © <?= date('Y') ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderErrors(array $errors): void
|
||||||
|
{
|
||||||
|
if (empty($errors)) return;
|
||||||
|
?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong><i class="icon-error"></i> Errori:</strong>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<?php foreach ($errors as $error): ?>
|
||||||
|
<li><?= e($error) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSuccess(string $message, array $log = []): void
|
||||||
|
{
|
||||||
|
?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<strong><i class="icon-ok"></i> <?= e($message) ?></strong>
|
||||||
|
<?php if (!empty($log)): ?>
|
||||||
|
<ul class="mb-0 mt-2">
|
||||||
|
<?php foreach ($log as $item): ?>
|
||||||
|
<li><?= e($item) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Glastree - Installer Entry Point
|
||||||
|
// =============================================================================
|
||||||
|
// Se .env esiste già, l'app è installata → redirect a login.
|
||||||
|
// Altrimenti avvia il wizard di installazione.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
$rootDir = dirname(__DIR__);
|
||||||
|
|
||||||
|
if (file_exists($rootDir . '/.env')) {
|
||||||
|
// App gia' installata
|
||||||
|
$baseUrl = rtrim((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . dirname($_SERVER['SCRIPT_NAME']), '/');
|
||||||
|
$baseUrl = str_replace('/installer', '', $baseUrl);
|
||||||
|
header('Location: ' . $baseUrl . '/login');
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avvia il wizard
|
||||||
|
session_start();
|
||||||
|
$_SESSION['install_mode'] = $_SESSION['install_mode'] ?? '';
|
||||||
|
$_SESSION['install_step'] = 1;
|
||||||
|
|
||||||
|
header('Location: setup.php?step=1');
|
||||||
|
exit;
|
||||||
@@ -0,0 +1,711 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Glastree Installer - Wizard Setup
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
require_once __DIR__ . '/functions.php';
|
||||||
|
|
||||||
|
// If already installed (and not in the middle of install), block
|
||||||
|
if (isEnvFileExists() && empty($_SESSION['install_in_progress'])) {
|
||||||
|
redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$step = max(1, min(6, (int) ($_GET['step'] ?? 1)));
|
||||||
|
$errors = [];
|
||||||
|
$success = '';
|
||||||
|
|
||||||
|
// ─── Step 1: Welcome + Mode Selection ──────────────────────────────────────
|
||||||
|
|
||||||
|
if ($step === 1) {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$mode = $_POST['mode'] ?? '';
|
||||||
|
if (!in_array($mode, ['fresh', 'backup'], true)) {
|
||||||
|
$errors[] = 'Seleziona una modalita di installazione.';
|
||||||
|
} else {
|
||||||
|
$_SESSION['install_mode'] = $mode;
|
||||||
|
$_SESSION['install_in_progress'] = true;
|
||||||
|
redirect('setup.php?step=2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader('Benvenuto', '1');
|
||||||
|
?>
|
||||||
|
<h2>Benvenuto nell'installazione di Glastree</h2>
|
||||||
|
<p>Questo wizard ti guidera nella configurazione dell'applicazione su questo server.</p>
|
||||||
|
|
||||||
|
<?php renderErrors($errors); ?>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<div class="option-group">
|
||||||
|
<label class="option-card <?= ($_SESSION['install_mode'] ?? '') === 'fresh' ? 'selected' : '' ?>">
|
||||||
|
<input type="radio" name="mode" value="fresh" class="option-radio"
|
||||||
|
<?= ($_SESSION['install_mode'] ?? '') === 'fresh' ? 'checked' : '' ?>>
|
||||||
|
<div class="option-content">
|
||||||
|
<div class="option-icon"><i class="icon-rocket"></i></div>
|
||||||
|
<h3>Fresh Install</h3>
|
||||||
|
<p>Crea un database nuovo, esegue migration e seed, e configura l'applicazione da zero.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Database nuovo</li>
|
||||||
|
<li>Migration + dati di base</li>
|
||||||
|
<li>Crea amministratore</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="option-card <?= ($_SESSION['install_mode'] ?? '') === 'backup' ? 'selected' : '' ?>">
|
||||||
|
<input type="radio" name="mode" value="backup" class="option-radio"
|
||||||
|
<?= ($_SESSION['install_mode'] ?? '') === 'backup' ? 'checked' : '' ?>>
|
||||||
|
<div class="option-content">
|
||||||
|
<div class="option-icon"><i class="icon-upload"></i></div>
|
||||||
|
<h3>Restore da Backup</h3>
|
||||||
|
<p>Carica un file ZIP di backup per ripristinare un'installazione esistente su questo server.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Upload file ZIP</li>
|
||||||
|
<li>Importa database</li>
|
||||||
|
<li>Ripristina documenti</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Avanti →</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
renderFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Step 2: Requirements Check ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ($step === 2) {
|
||||||
|
$checks = checkRequirements();
|
||||||
|
$allMet = allRequirementsMet($checks);
|
||||||
|
$nonCriticalFailed = false;
|
||||||
|
|
||||||
|
foreach ($checks as $check) {
|
||||||
|
if (!$check['passed'] && empty($check['critical'])) {
|
||||||
|
$nonCriticalFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $allMet) {
|
||||||
|
redirect('setup.php?step=3');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader('Verifica Requisiti', '2');
|
||||||
|
?>
|
||||||
|
<h2>Verifica Requisiti di Sistema</h2>
|
||||||
|
<p>Il sistema controlla che il server soddisfi i requisiti minimi per eseguire Glastree.</p>
|
||||||
|
|
||||||
|
<?php renderErrors($errors); ?>
|
||||||
|
|
||||||
|
<div class="requirements-list">
|
||||||
|
<?php foreach ($checks as $check): ?>
|
||||||
|
<div class="requirement-item <?= $check['passed'] ? 'passed' : 'failed' ?>">
|
||||||
|
<span class="req-icon">
|
||||||
|
<?php if ($check['passed']): ?>
|
||||||
|
<i class="icon-ok"></i>
|
||||||
|
<?php else: ?>
|
||||||
|
<i class="icon-error"></i>
|
||||||
|
<?php endif; ?>
|
||||||
|
</span>
|
||||||
|
<span class="req-name"><?= e($check['name']) ?></span>
|
||||||
|
<span class="req-status"><?= $check['passed'] ? 'OK' : 'ERRORE' ?></span>
|
||||||
|
<?php if (!$check['passed']): ?>
|
||||||
|
<span class="req-msg"><?= e($check['message']) ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if (!$allMet): ?>
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="icon-error"></i>
|
||||||
|
Requisiti critici non soddisfatti. Risolvi gli errori sopra e ricarica la pagina.
|
||||||
|
</div>
|
||||||
|
<?php elseif ($nonCriticalFailed): ?>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="icon-warning"></i>
|
||||||
|
Alcuni requisiti non critici non sono soddisfatti. Puoi proseguire, ma alcune funzionalita potrebbero essere limitate.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="setup.php?step=1" class="btn btn-secondary">← Indietro</a>
|
||||||
|
<?php if ($allMet): ?>
|
||||||
|
<button type="submit" class="btn btn-primary">Avanti →</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
renderFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Step 3: Database Configuration ─────────────────────────────────────────
|
||||||
|
|
||||||
|
if ($step === 3) {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$action = $_POST['action'] ?? '';
|
||||||
|
$dbHost = $_POST['db_host'] ?? '127.0.0.1';
|
||||||
|
$dbPort = (int) ($_POST['db_port'] ?? 3306);
|
||||||
|
|
||||||
|
if ($action === 'test_root') {
|
||||||
|
// Test root credentials
|
||||||
|
$rootUser = $_POST['root_user'] ?? 'root';
|
||||||
|
$rootPass = $_POST['root_pass'] ?? '';
|
||||||
|
$result = testRootConnection($dbHost, $dbPort, $rootUser, $rootPass);
|
||||||
|
if ($result['success']) {
|
||||||
|
$_SESSION['root_user'] = $rootUser;
|
||||||
|
$_SESSION['root_pass'] = $rootPass;
|
||||||
|
$success = 'Connessione root riuscita. Ora puoi creare il database.';
|
||||||
|
} else {
|
||||||
|
$errors[] = $result['message'];
|
||||||
|
}
|
||||||
|
} elseif ($action === 'create_db') {
|
||||||
|
// Create database + user
|
||||||
|
$rootUser = $_SESSION['root_user'] ?? 'root';
|
||||||
|
$rootPass = $_SESSION['root_pass'] ?? '';
|
||||||
|
$dbName = $_POST['db_name'] ?? 'glastree';
|
||||||
|
$dbUser = $_POST['db_user'] ?? 'glastree';
|
||||||
|
$dbPass = $_POST['db_pass'] ?? '';
|
||||||
|
$appUrl = $_POST['app_url'] ?? getBaseUrl();
|
||||||
|
|
||||||
|
if (strlen($dbPass) < 6) {
|
||||||
|
$errors[] = 'La password del database deve essere almeno 6 caratteri.';
|
||||||
|
} else {
|
||||||
|
$result = createDatabaseAndUser($rootUser, $rootPass, $dbHost, $dbPort, $dbName, $dbUser, $dbPass);
|
||||||
|
if ($result['success']) {
|
||||||
|
$_SESSION['db_host'] = $dbHost;
|
||||||
|
$_SESSION['db_port'] = $dbPort;
|
||||||
|
$_SESSION['db_name'] = $dbName;
|
||||||
|
$_SESSION['db_user'] = $dbUser;
|
||||||
|
$_SESSION['db_pass'] = $dbPass;
|
||||||
|
$_SESSION['app_url'] = $appUrl;
|
||||||
|
|
||||||
|
// Write .env
|
||||||
|
$envWritten = writeEnvFile([
|
||||||
|
'app_url' => $appUrl,
|
||||||
|
'db_host' => $dbHost,
|
||||||
|
'db_port' => (string) $dbPort,
|
||||||
|
'db_database' => $dbName,
|
||||||
|
'db_username' => $dbUser,
|
||||||
|
'db_password' => $dbPass,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($envWritten) {
|
||||||
|
redirect('setup.php?step=4');
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Impossibile scrivere il file .env.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = $result['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($action === 'test_existing') {
|
||||||
|
// Test existing database connection
|
||||||
|
$dbName = $_POST['db_name'] ?? '';
|
||||||
|
$dbUser = $_POST['db_user'] ?? '';
|
||||||
|
$dbPass = $_POST['db_pass'] ?? '';
|
||||||
|
$appUrl = $_POST['app_url'] ?? getBaseUrl();
|
||||||
|
|
||||||
|
if (empty($dbName) || empty($dbUser)) {
|
||||||
|
$errors[] = 'Compila tutti i campi obbligatori.';
|
||||||
|
} else {
|
||||||
|
$result = testDatabaseConnection($dbHost, $dbPort, $dbName, $dbUser, $dbPass);
|
||||||
|
if ($result['success']) {
|
||||||
|
$_SESSION['db_host'] = $dbHost;
|
||||||
|
$_SESSION['db_port'] = $dbPort;
|
||||||
|
$_SESSION['db_name'] = $dbName;
|
||||||
|
$_SESSION['db_user'] = $dbUser;
|
||||||
|
$_SESSION['db_pass'] = $dbPass;
|
||||||
|
$_SESSION['app_url'] = $appUrl;
|
||||||
|
|
||||||
|
$envWritten = writeEnvFile([
|
||||||
|
'app_url' => $appUrl,
|
||||||
|
'db_host' => $dbHost,
|
||||||
|
'db_port' => (string) $dbPort,
|
||||||
|
'db_database' => $dbName,
|
||||||
|
'db_username' => $dbUser,
|
||||||
|
'db_password' => $dbPass,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($envWritten) {
|
||||||
|
redirect('setup.php?step=4');
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Impossibile scrivere il file .env.';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = $result['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Azione non valida.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader('Configurazione Database', '3');
|
||||||
|
?>
|
||||||
|
<h2>Configurazione Database</h2>
|
||||||
|
<p>Configura la connessione al database MySQL.</p>
|
||||||
|
|
||||||
|
<?php renderErrors($errors); ?>
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><i class="icon-ok"></i> <?= e($success) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST" id="db-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_host">Host MySQL</label>
|
||||||
|
<input type="text" name="db_host" id="db_host" class="form-control" value="<?= old('db_host', '127.0.0.1') ?>" placeholder="127.0.0.1">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_port">Porta</label>
|
||||||
|
<input type="number" name="db_port" id="db_port" class="form-control" value="<?= old('db_port', '3306') ?>" placeholder="3306">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="app_url">URL Applicazione</label>
|
||||||
|
<input type="text" name="app_url" id="app_url" class="form-control" value="<?= old('app_url', getBaseUrl()) ?>" placeholder="https://glastree.esempio.it">
|
||||||
|
<small class="form-text text-muted">L'indirizzo pubblico con cui si accedera a Glastree</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h4>Modalita A: Crea Nuovo Database</h4>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="root_user">Utente Root MySQL</label>
|
||||||
|
<input type="text" name="root_user" id="root_user" class="form-control" value="<?= old('root_user', 'root') ?>">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="root_pass">Password Root</label>
|
||||||
|
<input type="password" name="root_pass" id="root_pass" class="form-control" value="">
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="action" value="test_root" class="btn btn-secondary">
|
||||||
|
<i class="icon-ok"></i> Test Connessione Root
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="mt-3 p-3 bg-light border rounded">
|
||||||
|
<h5>Nuovo Database da Creare</h5>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_name">Nome Database</label>
|
||||||
|
<input type="text" name="db_name" class="form-control" value="<?= old('db_name', 'glastree') ?>" placeholder="glastree">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_user">Utente Database</label>
|
||||||
|
<input type="text" name="db_user" class="form-control" value="<?= old('db_user', 'glastree') ?>" placeholder="glastree">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_pass">Password Utente</label>
|
||||||
|
<input type="password" name="db_pass" class="form-control" value="">
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="action" value="create_db" class="btn btn-primary">
|
||||||
|
<i class="icon-rocket"></i> Crea Database e Continua
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<h4>Modalita B: Usa Database Esistente</h4>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_name_ex">Nome Database</label>
|
||||||
|
<input type="text" name="db_name" class="form-control" value="<?= old('db_name', 'glastree') ?>" placeholder="glastree">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_user_ex">Utente</label>
|
||||||
|
<input type="text" name="db_user" class="form-control" value="<?= old('db_user', 'glastree') ?>" placeholder="glastree">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="db_pass_ex">Password</label>
|
||||||
|
<input type="password" name="db_pass" class="form-control" value="">
|
||||||
|
</div>
|
||||||
|
<button type="submit" name="action" value="test_existing" class="btn btn-primary">
|
||||||
|
<i class="icon-ok"></i> Test e Continua
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="form-actions mt-3">
|
||||||
|
<a href="setup.php?step=2" class="btn btn-secondary">← Indietro</a>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
renderFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Step 4: Install (Fresh: migrate+seed / Backup: upload+restore) ────────
|
||||||
|
|
||||||
|
if ($step === 4) {
|
||||||
|
$mode = $_SESSION['install_mode'] ?? 'fresh';
|
||||||
|
$installDone = $_SESSION['install_done'] ?? false;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if ($mode === 'fresh') {
|
||||||
|
// Run Artisan commands
|
||||||
|
try {
|
||||||
|
$kernel = bootstrapLaravel();
|
||||||
|
|
||||||
|
$commands = [
|
||||||
|
'key:generate' => ['--force' => true],
|
||||||
|
'migrate' => ['--force' => true],
|
||||||
|
'db:seed' => ['--force' => true],
|
||||||
|
'storage:link' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$results = runArtisanCommands($kernel, $commands);
|
||||||
|
$allOk = true;
|
||||||
|
$log = [];
|
||||||
|
foreach ($results as $r) {
|
||||||
|
$log[] = ($r['success'] ? 'OK' : 'ERRORE') . ': ' . $r['command'];
|
||||||
|
if (!$r['success']) $allOk = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($allOk) {
|
||||||
|
$_SESSION['install_done'] = true;
|
||||||
|
$success = 'Installazione completata con successo!';
|
||||||
|
$log[] = 'Ora puoi creare l\'amministratore.';
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Alcuni comandi sono falliti. Verifica i log.';
|
||||||
|
foreach ($log as $l) $errors[] = $l;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
} elseif ($mode === 'backup') {
|
||||||
|
// Handle backup upload
|
||||||
|
if (!empty($_FILES['backup_file']) && $_FILES['backup_file']['error'] === UPLOAD_ERR_OK) {
|
||||||
|
$uploadedFile = $_FILES['backup_file']['tmp_name'];
|
||||||
|
$originalName = $_FILES['backup_file']['name'];
|
||||||
|
|
||||||
|
$result = restoreFromBackup(
|
||||||
|
$uploadedFile,
|
||||||
|
$_SESSION['db_user'] ?? '',
|
||||||
|
$_SESSION['db_pass'] ?? '',
|
||||||
|
$_SESSION['db_name'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$_SESSION['install_done'] = true;
|
||||||
|
$success = 'Backup ripristinato con successo!';
|
||||||
|
$log = $result['log'] ?? [];
|
||||||
|
} else {
|
||||||
|
$errors[] = $result['message'];
|
||||||
|
if (!empty($result['log'])) {
|
||||||
|
foreach ($result['log'] as $l) $errors[] = $l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (!empty($_POST['backup_path'])) {
|
||||||
|
// Backup file already on server
|
||||||
|
$serverPath = $_POST['backup_path'];
|
||||||
|
|
||||||
|
if (!file_exists($serverPath)) {
|
||||||
|
$errors[] = 'File non trovato: ' . $serverPath;
|
||||||
|
} else {
|
||||||
|
$result = restoreFromBackup(
|
||||||
|
$serverPath,
|
||||||
|
$_SESSION['db_user'] ?? '',
|
||||||
|
$_SESSION['db_pass'] ?? '',
|
||||||
|
$_SESSION['db_name'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
$_SESSION['install_done'] = true;
|
||||||
|
$success = 'Backup ripristinato con successo!';
|
||||||
|
} else {
|
||||||
|
$errors[] = $result['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Seleziona un file di backup o fornisci il percorso.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader($mode === 'fresh' ? 'Installazione' : 'Ripristino Backup', '4');
|
||||||
|
?>
|
||||||
|
<h2><?= $mode === 'fresh' ? 'Installazione Database' : 'Ripristino Backup' ?></h2>
|
||||||
|
|
||||||
|
<?php renderErrors($errors); ?>
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><i class="icon-ok"></i> <?= e($success) ?></div>
|
||||||
|
<?php if (!empty($log)): ?>
|
||||||
|
<div class="log-list">
|
||||||
|
<?php foreach ($log as $l): ?>
|
||||||
|
<div class="log-item"><?= e($l) ?></div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($installDone): ?>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="icon-ok"></i> Installazione completata. Procedi al passo successivo.
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="setup.php?step=5" class="btn btn-primary">Avanti →</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<?php if ($mode === 'fresh'): ?>
|
||||||
|
<form method="POST">
|
||||||
|
<p>Clicca "Installa" per eseguire migration, seed e generare la chiave dell'applicazione.</p>
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="setup.php?step=3" class="btn btn-secondary">← Indietro</a>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="icon-rocket"></i> Installa</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<form method="POST" enctype="multipart/form-data">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_file">Carica file ZIP di backup</label>
|
||||||
|
<input type="file" name="backup_file" id="backup_file" class="form-control" accept=".zip">
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<p class="text-muted">oppure specifica il percorso sul server:</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_path">Percorso server del file ZIP</label>
|
||||||
|
<input type="text" name="backup_path" id="backup_path" class="form-control" placeholder="/tmp/backup.zip">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="setup.php?step=3" class="btn btn-secondary">← Indietro</a>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="icon-upload"></i> Ripristina Backup</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
renderFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Step 5: Admin User Creation ────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ($step === 5) {
|
||||||
|
$mode = $_SESSION['install_mode'] ?? 'fresh';
|
||||||
|
$adminCreated = false;
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if ($mode === 'fresh') {
|
||||||
|
$name = trim($_POST['name'] ?? '');
|
||||||
|
$email = trim($_POST['email'] ?? '');
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
$passwordConfirm = $_POST['password_confirm'] ?? '';
|
||||||
|
|
||||||
|
if (empty($name)) $errors[] = 'Il nome e obbligatorio.';
|
||||||
|
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Email non valida.';
|
||||||
|
if (strlen($password) < 8) $errors[] = 'La password deve essere almeno 8 caratteri.';
|
||||||
|
if ($password !== $passwordConfirm) $errors[] = 'Le password non coincidono.';
|
||||||
|
|
||||||
|
if (empty($errors)) {
|
||||||
|
$result = createSuperAdmin($name, $email, $password);
|
||||||
|
if ($result['success']) {
|
||||||
|
$adminCreated = true;
|
||||||
|
$_SESSION['admin_created'] = true;
|
||||||
|
} else {
|
||||||
|
$errors[] = $result['message'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Backup mode: skip or create additional admin
|
||||||
|
if (!empty($_POST['name']) && !empty($_POST['email']) && !empty($_POST['password'])) {
|
||||||
|
$result = createSuperAdmin($_POST['name'], $_POST['email'], $_POST['password']);
|
||||||
|
if ($result['success']) {
|
||||||
|
$adminCreated = true;
|
||||||
|
} else {
|
||||||
|
$errors[] = $result['message'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$_SESSION['admin_created'] = true;
|
||||||
|
$adminCreated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existing users (backup mode)
|
||||||
|
$existingUsers = [];
|
||||||
|
if ($mode === 'backup' && file_exists(dirname(__DIR__) . '/.env')) {
|
||||||
|
try {
|
||||||
|
$existingUsers = getExistingUsers();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeader('Amministratore', '5');
|
||||||
|
?>
|
||||||
|
<h2><?= $mode === 'fresh' ? 'Crea Amministratore' : 'Verifica Utenti' ?></h2>
|
||||||
|
|
||||||
|
<?php renderErrors($errors); ?>
|
||||||
|
<?php if ($success): ?>
|
||||||
|
<div class="alert alert-success"><?= e($success) ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($adminCreated || !empty($_SESSION['admin_created'])): ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="icon-ok"></i>
|
||||||
|
<?php if ($mode === 'fresh'): ?>
|
||||||
|
Amministratore creato con successo!
|
||||||
|
<?php else: ?>
|
||||||
|
Utenti configurati.
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="setup.php?step=6" class="btn btn-primary">Avanti →</a>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<?php if (!empty($existingUsers)): ?>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Utenti trovati nel database ripristinato:</strong>
|
||||||
|
<ul>
|
||||||
|
<?php foreach ($existingUsers as $u): ?>
|
||||||
|
<li><?= e($u['name']) ?> <<?= e($u['email']) ?>>
|
||||||
|
<?= $u['is_admin'] ? '<span class="badge badge-admin">Superadmin</span>' : '' ?>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<p>Puoi proseguire senza creare nuovi utenti.</p>
|
||||||
|
</div>
|
||||||
|
<form method="POST" class="mt-3">
|
||||||
|
<button type="submit" class="btn btn-secondary">Salta (usa utenti esistenti)</button>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
<p class="text-muted">oppure crea un nuovo amministratore:</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Nome Amministratore</label>
|
||||||
|
<input type="text" name="name" id="name" class="form-control" value="<?= old('name', 'Amministratore') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email</label>
|
||||||
|
<input type="email" name="email" id="email" class="form-control" value="<?= old('email', 'admin@glastree.local') ?>" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password (min. 8 caratteri)</label>
|
||||||
|
<input type="password" name="password" id="password" class="form-control" required minlength="8">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password_confirm">Conferma Password</label>
|
||||||
|
<input type="password" name="password_confirm" id="password_confirm" class="form-control" required minlength="8">
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="setup.php?step=4" class="btn btn-secondary">← Indietro</a>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="icon-ok"></i> Crea Amministratore</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
renderFooter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Step 6: Finalize ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ($step === 6) {
|
||||||
|
$finalized = false;
|
||||||
|
$results = [];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
try {
|
||||||
|
if (!file_exists(dirname(__DIR__) . '/.env')) {
|
||||||
|
$errors[] = 'File .env non trovato. Qualcosa e andato storto.';
|
||||||
|
} else {
|
||||||
|
$kernel = bootstrapLaravel();
|
||||||
|
$commands = [
|
||||||
|
'config:cache' => [],
|
||||||
|
'route:cache' => [],
|
||||||
|
'view:cache' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$results = runArtisanCommands($kernel, $commands);
|
||||||
|
$allOk = true;
|
||||||
|
foreach ($results as $r) {
|
||||||
|
if (!$r['success']) $allOk = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($allOk) {
|
||||||
|
// Self-destruct
|
||||||
|
selfDestruct();
|
||||||
|
$finalized = true;
|
||||||
|
} else {
|
||||||
|
$errors[] = 'Alcune operazioni sono fallite.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = 'Errore: ' . $e->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$finalized && empty($errors)) {
|
||||||
|
// Initial display
|
||||||
|
renderHeader('Finalizzazione', '6');
|
||||||
|
?>
|
||||||
|
<h2>Finalizzazione</h2>
|
||||||
|
<p>Completa l'installazione generando la cache e rimuovendo i file di setup.</p>
|
||||||
|
|
||||||
|
<?php renderErrors($errors); ?>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="setup.php?step=5" class="btn btn-secondary">← Indietro</a>
|
||||||
|
<button type="submit" class="btn btn-primary"><i class="icon-ok"></i> Completa Installazione</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<?php
|
||||||
|
renderFooter();
|
||||||
|
} else {
|
||||||
|
// Show results
|
||||||
|
renderHeader('Installazione Completata', '6');
|
||||||
|
?>
|
||||||
|
<h2>Installazione Completata!</h2>
|
||||||
|
|
||||||
|
<?php renderErrors($errors); ?>
|
||||||
|
|
||||||
|
<?php if (!empty($results)): ?>
|
||||||
|
<div class="results-list">
|
||||||
|
<?php foreach ($results as $r): ?>
|
||||||
|
<div class="result-item <?= $r['success'] ? 'success' : 'error' ?>">
|
||||||
|
<span class="result-icon"><?= $r['success'] ? '<i class="icon-ok"></i>' : '<i class="icon-error"></i>' ?></span>
|
||||||
|
<span class="result-name"><?= e($r['command']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($finalized): ?>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="icon-ok"></i>
|
||||||
|
<strong>Glastree e stato installato con successo!</strong>
|
||||||
|
<p class="mb-0 mt-2">La cartella <code>installer/</code> e stata eliminata per sicurezza.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="final-box">
|
||||||
|
<h3>Prossimi passi:</h3>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Configura SSL</strong> con Let's Encrypt per connessioni sicure</li>
|
||||||
|
<li>Accedi con le credenziali amministratore</li>
|
||||||
|
<li>Vai su <strong>Impostazioni</strong> per configurare:
|
||||||
|
<ul>
|
||||||
|
<li>Nome applicazione e logo</li>
|
||||||
|
<li>Email (SMTP e IMAP)</li>
|
||||||
|
<li>Repository remoti (Google Drive, Nextcloud)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Vai su <strong>Admin</strong> per creare altri utenti</li>
|
||||||
|
<li>Configura il cron per backup automatico:
|
||||||
|
<code class="d-block mt-2 p-2 bg-light">* * * * * cd <?= e(dirname(__DIR__)) ?> && php artisan schedule:run >> /dev/null 2>&1</code>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="<?= e(getBaseUrl()) ?>/login" class="btn btn-primary btn-lg">
|
||||||
|
<i class="icon-ok"></i> Vai al Login →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
renderFooter();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
@extends('layouts.adminlte')
|
||||||
|
|
||||||
|
@section('title', 'Backup - ' . $appName)
|
||||||
|
@section('page_title', 'Backup e Migrazione')
|
||||||
|
@section('breadcrumbs')
|
||||||
|
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/utenti">Admin</a></li>
|
||||||
|
<li class="breadcrumb-item active">Backup</li>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
{{-- Lista backup --}}
|
||||||
|
<div class="card card-outline card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-hdd mr-2"></i>Backup Disponibili</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<form method="POST" action="{{ route('admin.backup.run') }}" style="display:inline">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-play mr-1"></i> Esegui Backup Ora
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
@if(count($backups) > 0)
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Dimensione</th>
|
||||||
|
<th>Contenuto</th>
|
||||||
|
<th class="text-center">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($backups as $backup)
|
||||||
|
<tr>
|
||||||
|
<td><code>{{ $backup['filename'] }}</code></td>
|
||||||
|
<td>{{ date('d/m/Y H:i', $backup['last_modified']) }}</td>
|
||||||
|
<td>{{ $backup['size_formatted'] }}</td>
|
||||||
|
<td>
|
||||||
|
@if($backup['manifest'])
|
||||||
|
<span class="badge badge-info" title="PHP {{ $backup['manifest']['php_version'] ?? '?' }}, Laravel {{ $backup['manifest']['laravel_version'] ?? '?' }}">
|
||||||
|
<i class="fas fa-database mr-1"></i>DB
|
||||||
|
</span>
|
||||||
|
@if(isset($backup['manifest']['db_name']))
|
||||||
|
<small class="text-muted">{{ $backup['manifest']['db_name'] }}</small>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">N/A</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{{ route('admin.backup.download', $backup['filename']) }}" class="btn btn-success btn-sm" title="Scarica">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="{{ route('admin.backup.destroy', $backup['filename']) }}" style="display:inline" onsubmit="return confirm('Eliminare definitivamente questo backup?')">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm" title="Elimina">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-hdd fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted mb-0">Nessun backup disponibile.</p>
|
||||||
|
<p class="text-muted">Clicca "Esegui Backup Ora" per creare il primo backup.</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Info sul backup --}}
|
||||||
|
<div class="card card-outline card-info">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Cosa Include il Backup</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-bordered mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Componente</th>
|
||||||
|
<th>Incluso</th>
|
||||||
|
<th>Dettagli</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-database text-primary mr-2"></i> Database (MySQL)</td>
|
||||||
|
<td><span class="badge badge-success">Sempre</span></td>
|
||||||
|
<td>Struttura, dati, stored procedure, eventi, trigger</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-file text-warning mr-2"></i> File caricati</td>
|
||||||
|
<td>
|
||||||
|
@if($config['backup_include_files'])
|
||||||
|
<span class="badge badge-success">Sì</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">No</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>Documenti, avatar, gallerie — dalla cartella storage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-cog text-secondary mr-2"></i> Configurazione (.env)</td>
|
||||||
|
<td>
|
||||||
|
@if($config['backup_include_env'])
|
||||||
|
<span class="badge badge-success">Sì</span>
|
||||||
|
@else
|
||||||
|
<span class="badge badge-secondary">No</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>APP_KEY, DB credentials, SMTP, OAuth token</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-tag text-info mr-2"></i> Metadati</td>
|
||||||
|
<td><span class="badge badge-success">Sempre</span></td>
|
||||||
|
<td>Manifest JSON con versione app, PHP, Laravel</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{-- Configurazione --}}
|
||||||
|
<div class="card card-outline card-secondary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-sliders-h mr-2"></i>Configurazione Backup</h3>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('admin.backup.config') }}">
|
||||||
|
@csrf
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_path">Percorso backup</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><code>storage/app/</code></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="backup_path" id="backup_path" class="form-control" value="{{ $config['backup_path'] }}">
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Cartella dove salvare i backup (relativa a storage/app/)</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_retention_days">Ritenzione (giorni)</label>
|
||||||
|
<input type="number" name="backup_retention_days" id="backup_retention_days" class="form-control" value="{{ $config['backup_retention_days'] }}" min="1" max="365">
|
||||||
|
<small class="text-muted">I backup più vecchi vengono eliminati automaticamente</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="backup_include_files" name="backup_include_files" value="1" {{ $config['backup_include_files'] ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label" for="backup_include_files">Includi file caricati</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-switch mt-2">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="backup_include_env" name="backup_include_env" value="1" {{ $config['backup_include_env'] ? 'checked' : '' }}>
|
||||||
|
<label class="custom-control-label" for="backup_include_env">Includi .env (configurazione)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button type="submit" class="btn btn-secondary btn-block">
|
||||||
|
<i class="fas fa-save mr-1"></i> Salva Configurazione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Backup automatico --}}
|
||||||
|
<div class="card card-outline card-warning">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-clock mr-2"></i>Backup Automatico</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
@php $autoEnabled = \App\Models\AppSetting::getSetting('backup_auto_enabled', false) @endphp
|
||||||
|
<form method="POST" action="{{ route('admin.backup.toggle-auto') }}" style="display:inline">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="btn btn-sm {{ $autoEnabled ? 'btn-success' : 'btn-secondary' }}">
|
||||||
|
<i class="fas {{ $autoEnabled ? 'fa-check-circle' : 'fa-times-circle' }} mr-1"></i>
|
||||||
|
{{ $autoEnabled ? 'Attivo' : 'Disattivo' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="{{ route('admin.backup.save-auto') }}">
|
||||||
|
@csrf
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_auto_frequency">Frequenza</label>
|
||||||
|
<select name="backup_auto_frequency" id="backup_auto_frequency" class="form-control">
|
||||||
|
<option value="daily" {{ \App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'daily' ? 'selected' : '' }}>Giornaliero</option>
|
||||||
|
<option value="weekly" {{ \App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'weekly' ? 'selected' : '' }}>Settimanale</option>
|
||||||
|
<option value="monthly" {{ \App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'monthly' ? 'selected' : '' }}>Mensile</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<label for="backup_auto_hour">Ora di esecuzione</label>
|
||||||
|
<select name="backup_auto_hour" id="backup_auto_hour" class="form-control">
|
||||||
|
@for($h = 0; $h <= 23; $h++)
|
||||||
|
<option value="{{ $h }}" {{ \App\Models\AppSetting::getSetting('backup_auto_hour', 3) == $h ? 'selected' : '' }}>{{ sprintf('%02d:00', $h) }}</option>
|
||||||
|
@endfor
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Ora del giorno (formato 24h). Consigliato: 03:00 (notte)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button type="submit" class="btn btn-warning btn-block">
|
||||||
|
<i class="fas fa-save mr-1"></i> Salva Automatico
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Migrazione --}}
|
||||||
|
<div class="card card-outline card-danger">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-truck mr-2"></i>Migrazione Server</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted">Scarica il backup, poi esegui lo script di setup sul nuovo server per una migrazione completa.</p>
|
||||||
|
<a href="{{ route('help') }}#help-backup" class="btn btn-danger btn-block" target="_blank">
|
||||||
|
<i class="fas fa-book mr-1"></i> Guida alla Migrazione
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Alert notifica cron --}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
<strong>Importante:</strong> Per il backup automatico, assicurati che il cron di Laravel sia configurato sul server:
|
||||||
|
<code class="ml-2">* * * * * cd {{ base_path() }} && php artisan schedule:run >> /dev/null 2>&1</code>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -0,0 +1,940 @@
|
|||||||
|
@extends('layouts.adminlte')
|
||||||
|
|
||||||
|
@section('title', 'Guida - ' . $appName)
|
||||||
|
@section('page_title', 'Guida alla Configurazione')
|
||||||
|
@section('breadcrumbs')
|
||||||
|
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active">Guida</li>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card card-outline card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Argomenti</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<ul class="nav nav-pills flex-column" id="helpTabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="#help-webdav" data-toggle="tab">
|
||||||
|
<i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-googledrive" data-toggle="tab">
|
||||||
|
<i class="fab fa-google-drive mr-2"></i> Google Drive
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
|
||||||
|
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-email" data-toggle="tab">
|
||||||
|
<i class="fas fa-envelope mr-2"></i> Email / SMTP
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-installazione" data-toggle="tab">
|
||||||
|
<i class="fas fa-terminal mr-2"></i> Installazione
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-backup" data-toggle="tab">
|
||||||
|
<i class="fas fa-hdd mr-2"></i> Backup e Migrazione
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-docker" data-toggle="tab">
|
||||||
|
<i class="fab fa-docker mr-2"></i> Docker
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="tab-content">
|
||||||
|
{{-- WebDAV / Nextcloud --}}
|
||||||
|
<div class="tab-pane active" id="help-webdav">
|
||||||
|
<div class="card card-outline card-info">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Cos'è</h5>
|
||||||
|
<p>WebDAV è un protocollo che permette di accedere a file su server remoti. Nextcloud lo utilizza per la sincronizzazione dei file.</p>
|
||||||
|
|
||||||
|
<h5>Configurazione</h5>
|
||||||
|
<p>Vai in <strong>Impostazioni → Repository</strong> e crea un nuovo repository di tipo WebDAV.</p>
|
||||||
|
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Campi obbligatori</h6>
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-3">Nome</dt>
|
||||||
|
<dd class="col-sm-9">Un nome descrittivo (es. "Nextcloud Lavoro")</dd>
|
||||||
|
<dt class="col-sm-3">Base URI</dt>
|
||||||
|
<dd class="col-sm-9">L'URL del tuo server WebDAV.
|
||||||
|
<br><em>Nextcloud:</em> <code>https://tuodominio.it/remote.php/dav/files/utente/</code>
|
||||||
|
<br><em>Altri:</em> <code>https://server.webdav.it/</code></dd>
|
||||||
|
<dt class="col-sm-3">Root</dt>
|
||||||
|
<dd class="col-sm-9">Percorso base all'interno del server (es. <code>/Documenti/</code>). Lascia <code>/</code> per usare la root.</dd>
|
||||||
|
<dt class="col-sm-3">Username</dt>
|
||||||
|
<dd class="col-sm-9">Il tuo nome utente</dd>
|
||||||
|
<dt class="col-sm-3">Password</dt>
|
||||||
|
<dd class="col-sm-9">La password dell'account</dd>
|
||||||
|
<dt class="col-sm-3">Auth Type</dt>
|
||||||
|
<dd class="col-sm-9"><code>Basic</code> per la maggior parte dei server (Nextcloud, ownCloud).</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">⚠️ Nome utente con @ (chiocciola)</h5>
|
||||||
|
<p>Se il tuo nome utente contiene il carattere <code>@</code> (es. <code>mario.rossi@azienda.com</code>), segui queste regole:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Nel campo "Base URI":</strong> l'indirizzo email deve essere <strong>URL-encoded</strong>:
|
||||||
|
<br><code>https://server.it/remote.php/dav/files/mario.rossi%40azienda.com/</code>
|
||||||
|
<br><small>(il carattere <code>@</code> diventa <code>%40</code>)</small></li>
|
||||||
|
<li><strong>Nel campo "Username":</strong> inserisci l'indirizzo email <strong>normale</strong> con la <code>@</code> vera.</li>
|
||||||
|
</ul>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Il sistema gestisce automaticamente la differenza tra l'URI (che richiede <code>%40</code>) e l'autenticazione (che usa la <code>@</code> reale).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Test connessione</h5>
|
||||||
|
<p>Dopo aver salvato, usa il pulsante <span class="badge badge-success"><i class="fas fa-plug"></i> Test</span> nella tabella dei repository per verificare che la connessione funzioni.</p>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Importare file</h5>
|
||||||
|
<p>Apri la sezione <strong>Documenti</strong>, clicca sul pulsante del repository nella toolbar per sfogliare i file remoti, poi clicca <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span> su un file per importarlo come documento locale.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Google Drive --}}
|
||||||
|
<div class="tab-pane" id="help-googledrive">
|
||||||
|
<div class="card card-outline card-danger">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fab fa-google-drive mr-2"></i> Google Drive</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Cos'è</h5>
|
||||||
|
<p>Integrazione con Google Drive per sfogliare e importare file direttamente dal tuo spazio Google Drive.</p>
|
||||||
|
|
||||||
|
<h5>Requisiti</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Un account Google / Google Workspace</li>
|
||||||
|
<li>Un progetto sulla <strong>Google Cloud Console</strong> con l'API Google Drive abilitata</li>
|
||||||
|
<li>Credenziali OAuth 2.0 (Client ID e Client Secret)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Passo 1: Creare il progetto Google Cloud</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Vai su <a href="https://console.developers.google.com/" target="_blank">Google Cloud Console</a></li>
|
||||||
|
<li>Crea un nuovo progetto o selezionane uno esistente</li>
|
||||||
|
<li>Vai a <strong>API e servizi → Libreria</strong></li>
|
||||||
|
<li>Cerca "<strong>Google Drive API</strong>" e <strong>abilitala</strong></li>
|
||||||
|
<li>Vai a <strong>API e servizi → Credenziali</strong></li>
|
||||||
|
<li>Clicca "<strong>Crea credenziali → ID client OAuth</strong>"</li>
|
||||||
|
<li>Tipo applicazione: "<strong>Applicazione Web</strong>"</li>
|
||||||
|
<li><strong>URI di reindirizzamento autorizzato:</strong> aggiungi <code>{{ url('/auth/google-drive/callback') }}</code></li>
|
||||||
|
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Passo 2: Configurazione nell'app</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Vai in <strong>Impostazioni → Repository</strong></li>
|
||||||
|
<li>Clicca "<strong>Nuovo Repository</strong>" e seleziona tipo <strong>Google Drive</strong></li>
|
||||||
|
<li>Inserisci un nome descrittivo (es. "Drive Lavoro")</li>
|
||||||
|
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong></li>
|
||||||
|
<li><strong>Folder ID (opzionale):</strong> se vuoi limitare l'accesso a una cartella specifica, inserisci il suo ID (lo trovi nell'URL quando apri la cartella in Google Drive: <code>https://drive.google.com/drive/folders/<strong>ID_QUI</strong></code>)</li>
|
||||||
|
<li>Clicca "<strong>Autorizza Google Drive</strong>" — verrai reindirizzato a Google per concedere i permessi</li>
|
||||||
|
<li>Dopo l'autorizzazione, il refresh token verrà salvato automaticamente</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>⚠️ Google Drive API non abilitata</h5>
|
||||||
|
<p>Se vedi l'errore <em>"Google Drive API non abilitata"</em> nel test connessione, significa che l'API Drive non è stata attivata nel tuo progetto Google Cloud.</p>
|
||||||
|
<div class="callout callout-danger">
|
||||||
|
<h6>Soluzione</h6>
|
||||||
|
<p>Vai su <a href="https://console.developers.google.com/apis/api/drive.googleapis.com/overview" target="_blank">
|
||||||
|
https://console.developers.google.com/apis/api/drive.googleapis.com/overview
|
||||||
|
</a></p>
|
||||||
|
<p>Seleziona il progetto corretto dall'header in alto, clicca <strong>"Enable"</strong> e attendi qualche minuto prima di riprovare.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Importare file</h5>
|
||||||
|
<p>Stessa procedura di WebDAV: apri <strong>Documenti</strong>, clicca sul repository Drive, sfoglia i file e usa <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span>.</p>
|
||||||
|
|
||||||
|
<h5 class="mt-4">⚠️ File Google (Documenti, Fogli, Presentazioni)</h5>
|
||||||
|
<p>I file creati con Google Workspace (Documenti Google, Fogli Google, Presentazioni Google, Disegni Google) non possono essere scaricati direttamente perché non hanno un formato binario nativo.</p>
|
||||||
|
<p>Il sistema li converte automaticamente in formato Office Open XML durante l'import:</p>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>File Google</th><th>Formato convertito</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Documento Google</td><td><code>.docx</code> (Word)</td></tr>
|
||||||
|
<tr><td>Foglio Google</td><td><code>.xlsx</code> (Excel)</td></tr>
|
||||||
|
<tr><td>Presentazione Google</td><td><code>.pptx</code> (PowerPoint)</td></tr>
|
||||||
|
<tr><td>Disegno Google</td><td><code>.png</code> (Immagine)</td></tr>
|
||||||
|
<tr><td>Altri (Moduli, Script)</td><td><code>.pdf</code> (PDF)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Il nome del file viene automaticamente esteso con l'estensione corretta (es. "Relazione" → "Relazione.docx").
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Tipologie e Dati --}}
|
||||||
|
<div class="tab-pane" id="help-tipologie">
|
||||||
|
<div class="card card-outline card-success">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-tags mr-2"></i> Tipologie e Gestione Dati</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Tipologie Documenti</h5>
|
||||||
|
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
|
||||||
|
<p>Si gestiscono in <strong>Impostazioni → Tipologie Documenti</strong>. Puoi aggiungere, rinominare, riordinare ed eliminare tipologie (tranne quelle con documenti associati).</p>
|
||||||
|
|
||||||
|
<h5>Tipologie Individui</h5>
|
||||||
|
<p>Classificano gli individui (es. socio, volontario, dipendente). Si gestiscono in <strong>Impostazioni → Tipologie</strong>.</p>
|
||||||
|
|
||||||
|
<h5>Tipologie Eventi</h5>
|
||||||
|
<p>Classificano gli eventi (es. riunione, assemblea, formazione). Si gestiscono in <strong>Impostazioni → Tipologie Eventi</strong>.</p>
|
||||||
|
|
||||||
|
<h5>Ruoli Individui</h5>
|
||||||
|
<p>Definiscono il ruolo o la carica di un individuo (es. presidente, segretario, tesoriere). Si gestiscono in <strong>Impostazioni → Ruoli</strong>.</p>
|
||||||
|
|
||||||
|
<h5>Visibilità Documenti</h5>
|
||||||
|
<p>Ogni documento può avere diversi livelli di visibilità:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Pubblico</strong> — visibile a tutti gli utenti</li>
|
||||||
|
<li><strong>Individuo</strong> — visibile solo a uno specifico individuo</li>
|
||||||
|
<li><strong>Gruppo</strong> — visibile solo ai membri di un gruppo</li>
|
||||||
|
<li><strong>Evento</strong> — visibile solo ai partecipanti di un evento</li>
|
||||||
|
<li><strong>Mailing</strong> — visibile solo ai membri di una mailing list</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Email / SMTP --}}
|
||||||
|
<div class="tab-pane" id="help-email">
|
||||||
|
<div class="card card-outline card-warning">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-envelope mr-2"></i> Email / SMTP</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Configurazione Email</h5>
|
||||||
|
<p>Vai in <strong>Impostazioni → Email</strong> per configurare la posta elettronica.</p>
|
||||||
|
|
||||||
|
<h5>Server IMAP (Ricezione)</h5>
|
||||||
|
<p>Usato per ricevere e leggere le email direttamente nell'app.</p>
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Campi consigliati per provider comuni</h6>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Gmail</td><td><code>imap.gmail.com</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Outlook/Office 365</td><td><code>outlook.office365.com</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Libero</td><td><code>imapmail.libero.it</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Aruba</td><td><code>imap.aruba.it</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Server SMTP (Invio)</h5>
|
||||||
|
<p>Usato per inviare email dall'app. Supporta account multipli (mittenti).</p>
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Campi consigliati per provider comuni</h6>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Gmail</td><td><code>smtp.gmail.com</code></td><td><code>587</code></td><td>TLS</td></tr>
|
||||||
|
<tr><td>Outlook/Office 365</td><td><code>smtp.office365.com</code></td><td><code>587</code></td><td>TLS</td></tr>
|
||||||
|
<tr><td>Libero</td><td><code>smtp.libero.it</code></td><td><code>465</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Aruba</td><td><code>smtp.aruba.it</code></td><td><code>465</code></td><td>SSL</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Gmail / Google Workspace</h5>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
Gmail richiede una <strong>Password per l'app</strong> (non la password normale) se usi la verifica in due passaggi. Puoi generarla su
|
||||||
|
<a href="https://myaccount.google.com/apppasswords" target="_blank">https://myaccount.google.com/apppasswords</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Mailing</h5>
|
||||||
|
<p>Le <strong>Mailing List</strong> permettono di inviare email a gruppi predefiniti di destinatari.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Crea una mailing list in <strong>Mailing List → Nuova</strong></li>
|
||||||
|
<li>Puoi popolare la lista da individui, gruppi o manualmente</li>
|
||||||
|
<li>Invia email alla lista da <strong>Email → Nuova Email</strong>, selezionando la mailing list come destinatario</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h5>Test connessione</h5>
|
||||||
|
<p>Dopo aver configurato i server, usa il pulsante <span class="badge badge-success"><i class="fas fa-vial"></i> Test Connessione</span> per verificare che IMAP e SMTP funzionino correttamente.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Backup e Migrazione --}}
|
||||||
|
<div class="tab-pane" id="help-backup">
|
||||||
|
<div class="card card-outline card-danger">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-hdd mr-2"></i> Backup e Migrazione Server</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Cos'è</h5>
|
||||||
|
<p>Il sistema di backup crea un archivio ZIP contenente tutto il necessario per ripristinare l'applicazione su un nuovo server. Include database, file caricati e configurazione.</p>
|
||||||
|
|
||||||
|
<h5>Cosa include il backup</h5>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Componente</th><th>Incluso</th><th>Dettagli</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Database (MySQL)</td><td><span class="badge badge-success">Sempre</span></td><td>Struttura, dati, stored procedure, eventi, trigger</td></tr>
|
||||||
|
<tr><td>File caricati</td><td><span class="badge badge-success">Opzionale</span></td><td>Documenti, avatar, gallerie</td></tr>
|
||||||
|
<tr><td>Configurazione .env</td><td><span class="badge badge-success">Opzionale</span></td><td>APP_KEY, credenziali DB, SMTP, token OAuth</td></tr>
|
||||||
|
<tr><td>Metadati</td><td><span class="badge badge-success">Sempre</span></td><td>Versione app, PHP, Laravel</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Come eseguire un backup</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Via Interfaccia Web</h6>
|
||||||
|
<ol class="mb-0">
|
||||||
|
<li>Vai su <strong>Admin → Backup</strong></li>
|
||||||
|
<li>Verifica le impostazioni (includi files/.env)</li>
|
||||||
|
<li>Clicca <span class="badge badge-primary"><i class="fas fa-play"></i> Esegui Backup Ora</span></li>
|
||||||
|
<li>Scarica il file ZIP generato</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<h6>Via Comando</h6>
|
||||||
|
<pre class="mb-0"><code>php artisan backup:run</code></pre>
|
||||||
|
<p class="mb-0 mt-2 small">Il file viene creato in <code>storage/app/backups/</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Backup automatico</h5>
|
||||||
|
<p>Puoi abilitare il backup automatico dalla pagina <strong>Admin → Backup</strong>:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Frequenza:</strong> giornaliero, settimanale o mensile</li>
|
||||||
|
<li><strong>Ora:</strong> consigliata 03:00 (notte)</li>
|
||||||
|
<li><strong>Requisito:</strong> il cron di Laravel deve essere configurato sul server</li>
|
||||||
|
</ul>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-clock mr-2"></i>
|
||||||
|
Per il backup automatico, aggiungi questo cron:
|
||||||
|
<pre class="mt-2 mb-0"><code>* * * * * cd {{ base_path() }} && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h4><i class="fas fa-truck mr-2"></i> Procedura di Migrazione Completa</h4>
|
||||||
|
<p>Segui questa procedura per migrare Glastree su un nuovo server LAMP (Linux, Apache, MySQL, PHP).</p>
|
||||||
|
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||||
|
<strong>ATTENZIONE: APP_KEY</strong>
|
||||||
|
<p class="mb-0 mt-2">L'APP_KEY cripta i dati sensibili (password email SMTP, token OAuth Google Drive, ecc.).</p>
|
||||||
|
<ul class="mt-2">
|
||||||
|
<li><strong>Se hai il .env originale dal backup:</strong> NON rigenerare APP_KEY — mantieni i dati criptati accessibili.</li>
|
||||||
|
<li><strong>Se hai perso il .env:</strong> DEVI rigenerare APP_KEY, ma perderai l'accesso ai dati criptati esistenti. Dovrai riconfigurare SMTP e OAuth.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 1: Preparazione del server</h5>
|
||||||
|
<pre><code>sudo apt update && sudo apt upgrade -y</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 2: Installa LAMP + PHP 8.4</h5>
|
||||||
|
<pre><code>sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
|
||||||
|
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
|
||||||
|
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
|
||||||
|
composer git unzip
|
||||||
|
|
||||||
|
sudo a2enmod rewrite</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 3: Clona l'applicazione</h5>
|
||||||
|
<pre><code>cd /var/www
|
||||||
|
git clone <URL_REPOSITORY> glastree
|
||||||
|
cd glastree
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
npm ci && npm run build</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 4: Carica il backup</h5>
|
||||||
|
<p>Copia il file <code>backup.zip</code> sul nuovo server e estrailo:</p>
|
||||||
|
<pre><code>mkdir -p /tmp/restore
|
||||||
|
unzip backup.zip -d /tmp/restore/
|
||||||
|
ls -la /tmp/restore/
|
||||||
|
# Dovresti vedere: database.sql .env files/ manifest.json</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 5: Crea database e importa</h5>
|
||||||
|
<pre><code>sudo mysql -u root <<EOF
|
||||||
|
CREATE DATABASE glastree CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER 'glastree'@'localhost' IDENTIFIED BY 'ScegliPasswordFortep4ss!';
|
||||||
|
GRANT ALL ON glastree.* TO 'glastree'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mysql -u glastree -p glastree < /tmp/restore/database.sql</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 6: Configura .env</h5>
|
||||||
|
<pre><code>cp /tmp/restore/.env .env
|
||||||
|
# Modifica se necessario:
|
||||||
|
# APP_URL=https://nuovo-dominio.it
|
||||||
|
# DB_HOST=localhost
|
||||||
|
# DB_DATABASE=glastree
|
||||||
|
# DB_USERNAME=glastree
|
||||||
|
# DB_PASSWORD=ScegliPasswordFortep4ss!
|
||||||
|
|
||||||
|
# DECIDI SULL'APP_KEY (leggi avviso sopra!)
|
||||||
|
# php artisan key:generate ← SOLO se non hai il .env originale</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 7: Ripristina file e permessi</h5>
|
||||||
|
<pre><code>cp -r /tmp/restore/files/* storage/app/documenti/
|
||||||
|
|
||||||
|
sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 755 storage bootstrap/cache
|
||||||
|
|
||||||
|
php artisan storage:link</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 8: Cache</h5>
|
||||||
|
<pre><code>php artisan optimize:clear
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 9: Configura Apache</h5>
|
||||||
|
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName glastree.esempio.it
|
||||||
|
DocumentRoot /var/www/glastree/public
|
||||||
|
|
||||||
|
<Directory /var/www/glastree/public>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
|
||||||
|
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
APACHE
|
||||||
|
|
||||||
|
sudo a2dissite 000-default.conf
|
||||||
|
sudo a2ensite glastree.conf
|
||||||
|
sudo systemctl reload apache2</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 10: SSL Let's Encrypt</h5>
|
||||||
|
<pre><code>sudo apt install -y certbot python3-certbot-apache
|
||||||
|
sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 11: Cron per backup automatico</h5>
|
||||||
|
<pre><code>sudo crontab -u www-data -e
|
||||||
|
# Inserisci:
|
||||||
|
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 12: Verifica finale</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Apri https://glastree.esempio.it nel browser</li>
|
||||||
|
<li>Fai login con le credenziali esistenti</li>
|
||||||
|
<li>Verifica: Dashboard, Documenti, Impostazioni, Email</li>
|
||||||
|
<li>Se hai rigenerato APP_KEY: riconfigura SMTP e OAuth Google Drive</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h5>PHP Web Installer</h5>
|
||||||
|
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||||
|
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Docker --}}
|
||||||
|
<div class="tab-pane" id="help-docker">
|
||||||
|
<div class="card card-outline card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fab fa-docker mr-2"></i> Docker</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Creare un'immagine Docker con dati di base</h5>
|
||||||
|
<p>Puoi creare un'immagine Docker pronta all'uso che include l'applicazione e i dati di base (tipologie documenti, tipologie eventi, ruoli). Scegli la configurazione in base alle tue esigenze:</p>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6><i class="fas fa-database"></i> SQLite (Leggero)</h6>
|
||||||
|
<p class="mb-0 small">Database embedded, nessun servizio aggiuntivo. Ideale per demo, test, sviluppo locale o ambienti con poca concorrenza.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<h6><i class="fas fa-server"></i> MySQL (Standard)</h6>
|
||||||
|
<p class="mb-0 small">Database separato in un container MySQL. Configurazione standard, ideale per produzione, staging e ambienti con piu utenti.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">1. Dockerfile (comune a entrambe le modalita)</h5>
|
||||||
|
<p>Crea un file <code>Dockerfile</code> nella root del progetto:</p>
|
||||||
|
<pre><code>FROM php:8.4-apache
|
||||||
|
|
||||||
|
# Abilita mod_rewrite
|
||||||
|
RUN docker-php-ext-enable opcache \
|
||||||
|
&& a2enmod rewrite
|
||||||
|
|
||||||
|
# Installa estensioni PHP (sia SQLite che MySQL)
|
||||||
|
RUN docker-php-ext-install pdo_mysql pdo_sqlite bcmath gd zip
|
||||||
|
|
||||||
|
# Installa Composer
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# Configura DocumentRoot per Laravel
|
||||||
|
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
|
||||||
|
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
|
||||||
|
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
|
||||||
|
|
||||||
|
# Copia l'applicazione
|
||||||
|
COPY . /var/www/html/
|
||||||
|
|
||||||
|
# Installa dipendenze e compila asset
|
||||||
|
RUN composer install --no-dev --optimize-autoloader \
|
||||||
|
&& mkdir -p storage/app/public \
|
||||||
|
&& mkdir -p storage/framework/cache/data \
|
||||||
|
&& mkdir -p storage/framework/sessions \
|
||||||
|
&& mkdir -p storage/framework/views \
|
||||||
|
&& mkdir -p storage/logs \
|
||||||
|
&& mkdir -p bootstrap/cache \
|
||||||
|
&& chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
|
||||||
|
# Entrypoint per inizializzazione
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
|
CMD ["apache2-foreground"]</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">2. Entrypoint Script (comune)</h5>
|
||||||
|
<p>Crea <code>docker-entrypoint.sh</code>. Lo script rileva automaticamente la configurazione in base alle variabili d'ambiente:</p>
|
||||||
|
<pre><code>#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Se .env non esiste, lo genera dalle variabili d'ambiente
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
if [ "$DB_CONNECTION" = "mysql" ]; then
|
||||||
|
# Configura MySQL
|
||||||
|
sed -i "s/DB_CONNECTION=mysql/DB_CONNECTION=mysql/" .env
|
||||||
|
sed -i "s/DB_HOST=.*/DB_HOST=${DB_HOST:-mysql}/" .env
|
||||||
|
sed -i "s/DB_PORT=.*/DB_PORT=${DB_PORT:-3306}/" .env
|
||||||
|
sed -i "s/DB_DATABASE=.*/DB_DATABASE=${DB_DATABASE:-glastree}/" .env
|
||||||
|
sed -i "s/DB_USERNAME=.*/DB_USERNAME=${DB_USERNAME:-glastree}/" .env
|
||||||
|
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${DB_PASSWORD:-secret}/" .env
|
||||||
|
|
||||||
|
# Attende che MySQL sia pronto
|
||||||
|
echo "Attendo MySQL..."
|
||||||
|
until php -r "new PDO('mysql:host=${DB_HOST:-mysql};dbname=${DB_DATABASE:-glastree}', '${DB_USERNAME:-glastree}', '${DB_PASSWORD:-secret}');" 2>/dev/null; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "MySQL pronto."
|
||||||
|
else
|
||||||
|
# Configura SQLite
|
||||||
|
sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env
|
||||||
|
sed -i 's/DB_HOST=.*//' .env
|
||||||
|
sed -i 's/DB_PORT=.*//' .env
|
||||||
|
sed -i 's/DB_DATABASE=.*/DB_DATABASE=\/var\/www\/html\/database\/database.sqlite/' .env
|
||||||
|
sed -i 's/DB_USERNAME=.*//' .env
|
||||||
|
sed -i 's/DB_PASSWORD=.*//' .env
|
||||||
|
|
||||||
|
touch database/database.sqlite
|
||||||
|
chmod 664 database/database.sqlite
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Genera APP_KEY
|
||||||
|
php artisan key:generate --force
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Esegui migration e seed se il database e vuoto
|
||||||
|
if [ ! -f .db_initialized ]; then
|
||||||
|
php artisan migrate --force
|
||||||
|
php artisan db:seed --force
|
||||||
|
touch .db_initialized
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configura storage link
|
||||||
|
php artisan storage:link --force 2>/dev/null || true
|
||||||
|
|
||||||
|
exec "$@"</code></pre>
|
||||||
|
<pre><code>chmod +x docker-entrypoint.sh</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">3. Seed Base Dati (comune)</h5>
|
||||||
|
<p>Per includere tipologie documenti, tipologie eventi e ruoli, crea un seeder dedicato:</p>
|
||||||
|
<pre><code>php artisan make:seeder DockerBaseDataSeeder</code></pre>
|
||||||
|
<p>Contenuto di <code>database/seeders/DockerBaseDataSeeder.php</code>:</p>
|
||||||
|
<pre><code><?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class DockerBaseDataSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Tipologie Documenti
|
||||||
|
$tipologieDoc = ['documento', 'avatar', 'galleria', 'statuto', 'programma', 'locandina', 'altro'];
|
||||||
|
foreach ($tipologieDoc as $ord => $nome) {
|
||||||
|
DB::table('tipologie_documenti')->updateOrInsert(
|
||||||
|
['nome' => $nome],
|
||||||
|
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tipologie Eventi
|
||||||
|
$tipologieEventi = [
|
||||||
|
['nome' => 'catechesi', 'descrizione' => 'Catechesi'],
|
||||||
|
['nome' => 'liturgia', 'descrizione' => 'Liturgia'],
|
||||||
|
['nome' => 'animazione', 'descrizione' => 'Animazione'],
|
||||||
|
['nome' => 'formazione', 'descrizione' => 'Formazione'],
|
||||||
|
['nome' => 'incontro', 'descrizione' => 'Incontro'],
|
||||||
|
['nome' => 'riunione', 'descrizione' => 'Riunione'],
|
||||||
|
['nome' => 'festa', 'descrizione' => 'Festa'],
|
||||||
|
['nome' => 'altro', 'descrizione' => 'Altro'],
|
||||||
|
];
|
||||||
|
foreach ($tipologieEventi as $ord => $t) {
|
||||||
|
DB::table('tipologie_eventi')->updateOrInsert(
|
||||||
|
['nome' => $t['nome']],
|
||||||
|
['descrizione' => $t['descrizione'], 'ordine' => $ord + 1, 'attiva' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ruoli
|
||||||
|
$ruoli = ['presidente', 'vicepresidente', 'segretario', 'tesoriere', 'consigliere', 'volontario', 'socio', 'altro'];
|
||||||
|
foreach ($ruoli as $ord => $nome) {
|
||||||
|
DB::table('ruoli')->updateOrInsert(
|
||||||
|
['nome' => $nome],
|
||||||
|
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
<p>Registralo in <code>database/seeders/DatabaseSeeder.php</code>:</p>
|
||||||
|
<pre><code>$this->call([
|
||||||
|
// ... altri seeder ...
|
||||||
|
DockerBaseDataSeeder::class,
|
||||||
|
]);</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">4. Docker Compose</h5>
|
||||||
|
|
||||||
|
<h6 class="text-info"><i class="fas fa-database mr-1"></i> Opzione A — SQLite (leggero)</h6>
|
||||||
|
<pre><code>services:
|
||||||
|
glastree:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- glastree_storage:/var/www/html/storage
|
||||||
|
- glastree_db:/var/www/html/database
|
||||||
|
environment:
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=false
|
||||||
|
- APP_URL=http://localhost:8080
|
||||||
|
- DB_CONNECTION=sqlite
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
glastree_storage:
|
||||||
|
glastree_db:</code></pre>
|
||||||
|
|
||||||
|
<h6 class="mt-3 text-success"><i class="fas fa-server mr-1"></i> Opzione B — MySQL (standard)</h6>
|
||||||
|
<pre><code>services:
|
||||||
|
glastree:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- glastree_storage:/var/www/html/storage
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=false
|
||||||
|
- APP_URL=http://localhost:8080
|
||||||
|
- DB_CONNECTION=mysql
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_DATABASE=glastree
|
||||||
|
- DB_USERNAME=glastree
|
||||||
|
- DB_PASSWORD=secret
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
- MYSQL_DATABASE=glastree
|
||||||
|
- MYSQL_USER=glastree
|
||||||
|
- MYSQL_PASSWORD=secret
|
||||||
|
- MYSQL_ROOT_PASSWORD=root_secret
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
glastree_storage:
|
||||||
|
mysql_data:</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">5. Build e avvio</h5>
|
||||||
|
<pre><code># Costruisci l'immagine (comune)
|
||||||
|
docker build -t glastree:latest .
|
||||||
|
|
||||||
|
# Avvia con SQLite
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
|
||||||
|
# Oppure avvia con MySQL
|
||||||
|
# docker compose -f docker-compose.mysql.yml up -d
|
||||||
|
|
||||||
|
# Apri nel browser
|
||||||
|
open http://localhost:8080</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">6. Primo accesso</h5>
|
||||||
|
<p>Al primo avvio, l'entrypoint esegue automaticamente migration e seed. Troverai gia popolati:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Tipologie Documenti:</strong> documento, avatar, galleria, statuto, programma, locandina, altro</li>
|
||||||
|
<li><strong>Tipologie Eventi:</strong> catechesi, liturgia, animazione, formazione, incontro, riunione, festa, altro</li>
|
||||||
|
<li><strong>Ruoli:</strong> presidente, vicepresidente, segretario, tesoriere, consigliere, volontario, socio, altro</li>
|
||||||
|
</ul>
|
||||||
|
<p>Dovrai creare il primo utente amministratore via CLI:</p>
|
||||||
|
<pre><code>docker exec -it glastree php artisan tinker --execute="
|
||||||
|
\$u = new \App\Models\User;
|
||||||
|
\$u->name = 'Amministratore';
|
||||||
|
\$u->email = 'admin@esempio.it';
|
||||||
|
\$u->password = bcrypt('password-forte');
|
||||||
|
\$u->is_admin = true;
|
||||||
|
\$u->permissions = ['individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2];
|
||||||
|
\$u->save();
|
||||||
|
echo 'Utente creato con successo!';
|
||||||
|
"</code></pre>
|
||||||
|
|
||||||
|
<div class="callout callout-warning mt-4">
|
||||||
|
<h6><i class="fas fa-exclamation-triangle"></i> Note importanti</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li><strong>Persistenza:</strong> i volumi Docker preservano storage e database tra i riavvii</li>
|
||||||
|
<li><strong>APP_KEY:</strong> viene generata al primo avvio — se elimini i volumi, la perdi e i dati criptati diventano inaccessibili</li>
|
||||||
|
<li><strong>MySQL in produzione:</strong> imposta password sicure per <code>MYSQL_PASSWORD</code> e <code>MYSQL_ROOT_PASSWORD</code></li>
|
||||||
|
<li><strong>File compose separati:</strong> puoi mantenere entrambi i file (<code>docker-compose.yml</code> per SQLite, <code>docker-compose.mysql.yml</code> per MySQL) e scegliere con <code>-f</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Installazione --}}
|
||||||
|
<div class="tab-pane" id="help-installazione">
|
||||||
|
<div class="card card-outline card-dark">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-terminal mr-2"></i> Guida all'Installazione</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Panoramica</h5>
|
||||||
|
<p>Glastree include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6><i class="fas fa-rocket"></i> Fresh Install</h6>
|
||||||
|
<p class="mb-0">Installa l'applicazione da zero su un nuovo server: crea il database, esegue le migration, popola i dati di base e crea l'amministratore.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
|
||||||
|
<p class="mb-0">Ripristina un'installazione esistente da un file ZIP di backup: importa il database, ripristina i documenti e la configurazione.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Requisiti del Server</h5>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Componente</th><th>Versione Minima</th><th>Note</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Web Server</td><td>Apache 2.4+</td><td>Con mod_rewrite abilitato</td></tr>
|
||||||
|
<tr><td>PHP</td><td>8.2+ (consigliato 8.4)</td><td>Estensioni: pdo_mysql, mbstring, xml, curl, zip, gd, fileinfo</td></tr>
|
||||||
|
<tr><td>MySQL / MariaDB</td><td>MySQL 8.0+ / MariaDB 10.5+</td><td>Supporto utf8mb4</td></tr>
|
||||||
|
<tr><td>Composer</td><td>2.x</td><td>Per gestione dipendenze PHP</td></tr>
|
||||||
|
<tr><td>Node.js / npm</td><td>Node 20+ / npm 10+</td><td>Per compilazione asset frontend (opzionale se precompilati)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Installazione Passo-Passo</h5>
|
||||||
|
|
||||||
|
<h6>Passo 1: Preparare il server LAMP</h6>
|
||||||
|
<pre><code>sudo apt update && sudo apt upgrade -y
|
||||||
|
|
||||||
|
sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
|
||||||
|
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
|
||||||
|
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
|
||||||
|
composer git unzip
|
||||||
|
|
||||||
|
sudo a2enmod rewrite
|
||||||
|
sudo systemctl restart apache2</code></pre>
|
||||||
|
|
||||||
|
<h6>Passo 2: Ottenere l'applicazione</h6>
|
||||||
|
<pre><code>cd /var/www
|
||||||
|
git clone <URL_REPOSITORY> glastree
|
||||||
|
cd glastree
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
npm ci && npm run build</code></pre>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6>Passo 3: Permessi cartelle</h6>
|
||||||
|
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||||
|
|
||||||
|
<h6>Passo 4: Configurare Apache</h6>
|
||||||
|
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName glastree.esempio.it
|
||||||
|
DocumentRoot /var/www/glastree/public
|
||||||
|
|
||||||
|
<Directory /var/www/glastree/public>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
|
||||||
|
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
APACHE
|
||||||
|
|
||||||
|
sudo a2dissite 000-default.conf
|
||||||
|
sudo a2ensite glastree.conf
|
||||||
|
sudo systemctl reload apache2</code></pre>
|
||||||
|
|
||||||
|
<h6>Passo 5: Avviare il wizard di installazione</h6>
|
||||||
|
<p>Apri il browser e visita l'indirizzo del tuo server:</p>
|
||||||
|
<pre><code>http://glastree.esempio.it/installer/</code></pre>
|
||||||
|
|
||||||
|
<p>Il wizard ti guidera attraverso 6 passi:</p>
|
||||||
|
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||||
|
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||||
|
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||||
|
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||||
|
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||||
|
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Modalita Fresh Install</h5>
|
||||||
|
<p>Seleziona questa modalita quando installi Glastree per la prima volta su un server.</p>
|
||||||
|
<p>Cosa succede durante l'installazione:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||||
|
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||||
|
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||||
|
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||||
|
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Modalita Restore da Backup</h5>
|
||||||
|
<p>Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||||
|
<p>Cosa serve:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||||
|
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||||
|
</ul>
|
||||||
|
<p>Cosa succede durante il restore:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||||
|
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||||
|
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||||
|
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Dopo l'Installazione</h5>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Configura SSL</strong> con Let's Encrypt:
|
||||||
|
<pre><code>sudo apt install -y certbot python3-certbot-apache
|
||||||
|
sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||||
|
</li>
|
||||||
|
<li><strong>Accedi</strong> con le credenziali amministratore create durante l'installazione</li>
|
||||||
|
<li><strong>Configura l'applicazione</strong> in Impostazioni:
|
||||||
|
<ul>
|
||||||
|
<li>Nome applicazione e logo</li>
|
||||||
|
<li>Email (SMTP per invio, IMAP per ricezione)</li>
|
||||||
|
<li>Repository remoti (Google Drive, Nextcloud)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>Crea utenti</strong> dal pannello Admin → Utenti</li>
|
||||||
|
<li><strong>Configura il cron</strong> per backup automatico e sync email:
|
||||||
|
<pre><code>sudo crontab -u www-data -e
|
||||||
|
# Inserisci:
|
||||||
|
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Risoluzione Problemi</h5>
|
||||||
|
<div class="callout callout-danger">
|
||||||
|
<h6>Errore "500 | Server Error" dopo l'installazione</h6>
|
||||||
|
<p>Probabilmente mancano permessi sulle cartelle <code>storage</code> o <code>bootstrap/cache</code>:</p>
|
||||||
|
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="callout callout-danger">
|
||||||
|
<h6>Errore "No application encryption key"</h6>
|
||||||
|
<p>APP_KEY non generata. Nella cartella dell'app:</p>
|
||||||
|
<pre><code>php artisan key:generate</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="callout callout-warning">
|
||||||
|
<h6>mod_rewrite non funziona</h6>
|
||||||
|
<p>Se le rotte restituiscono 404, verifica che Apache abbia AllowOverride:</p>
|
||||||
|
<pre><code>sudo a2enmod rewrite
|
||||||
|
# Nel VirtualHost: AllowOverride All
|
||||||
|
sudo systemctl reload apache2</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-pills .nav-link { border-radius: 0; }
|
||||||
|
.nav-pills .nav-link.active { background-color: #007bff; }
|
||||||
|
.callout { border-left: 3px solid; padding: 12px 16px; margin: 16px 0; background: #f8f9fa; }
|
||||||
|
.callout-info { border-color: #17a2b8; }
|
||||||
|
.callout-danger { border-color: #dc3545; }
|
||||||
|
.callout-warning { border-color: #ffc107; }
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
@@ -23,6 +23,11 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="{{ route('help') }}" title="Guida">
|
||||||
|
<i class="fas fa-question-circle"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
<i class="far fa-bell"></i>
|
<i class="far fa-bell"></i>
|
||||||
@@ -80,6 +85,12 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
|||||||
<p>Dashboard</p>
|
<p>Dashboard</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="{{ route('help') }}" class="nav-link {{ request()->routeIs('help') ? 'active' : '' }}">
|
||||||
|
<i class="nav-icon fas fa-question-circle"></i>
|
||||||
|
<p>Guida</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
@if(Auth::user()->canAccess('individui'))
|
@if(Auth::user()->canAccess('individui'))
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="{{ route('individui.index') }}" class="nav-link">
|
<a href="{{ route('individui.index') }}" class="nav-link">
|
||||||
@@ -215,6 +226,12 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
|||||||
<p>Admin</p>
|
<p>Admin</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/backup" class="nav-link {{ request()->is('admin/backup*') ? 'active' : '' }}">
|
||||||
|
<i class="nav-icon fas fa-hdd"></i>
|
||||||
|
<p>Backup</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
@endif
|
@endif
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schedule;
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
use App\Services\BackupService;
|
||||||
|
use App\Models\AppSetting;
|
||||||
|
|
||||||
Schedule::call(function () {
|
Schedule::call(function () {
|
||||||
try {
|
try {
|
||||||
@@ -10,3 +12,19 @@ Schedule::call(function () {
|
|||||||
\Illuminate\Support\Facades\Log::error('Scheduled email sync failed', ['error' => $e->getMessage()]);
|
\Illuminate\Support\Facades\Log::error('Scheduled email sync failed', ['error' => $e->getMessage()]);
|
||||||
}
|
}
|
||||||
})->name('email:sync')->everyFifteenMinutes()->withoutOverlapping();
|
})->name('email:sync')->everyFifteenMinutes()->withoutOverlapping();
|
||||||
|
|
||||||
|
// Backup automatico
|
||||||
|
Schedule::call(function () {
|
||||||
|
if (!AppSetting::getSetting('backup_auto_enabled', false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
(new BackupService())->run();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::error('Scheduled backup failed', ['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
})->name('backup:auto')
|
||||||
|
->dailyAt((string) (AppSetting::getSetting('backup_auto_hour', 3) . ':00'))
|
||||||
|
->withoutOverlapping()
|
||||||
|
->onOneServer();
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Route::get('/', function () {
|
|||||||
return redirect()->route('login');
|
return redirect()->route('login');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
Route::middleware('guest')->group(function () {
|
Route::middleware('guest')->group(function () {
|
||||||
Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
|
Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
|
||||||
@@ -54,6 +55,11 @@ Route::middleware('auth')->group(function () {
|
|||||||
// Dashboard
|
// Dashboard
|
||||||
Route::get('/dashboard', [HomeController::class, 'index'])->name('dashboard')->middleware('auth');
|
Route::get('/dashboard', [HomeController::class, 'index'])->name('dashboard')->middleware('auth');
|
||||||
|
|
||||||
|
// Help
|
||||||
|
Route::get('/help', function () {
|
||||||
|
return view('help.index', ['appName' => \App\Models\AppSetting::getAppName() ?? 'Glastree']);
|
||||||
|
})->middleware('auth')->name('help');
|
||||||
|
|
||||||
// Resource routes
|
// Resource routes
|
||||||
// Individui specific routes (MUST be before resource route)
|
// Individui specific routes (MUST be before resource route)
|
||||||
Route::get('individui/email-list', [IndividuoController::class, 'emailList'])->middleware('auth');
|
Route::get('individui/email-list', [IndividuoController::class, 'emailList'])->middleware('auth');
|
||||||
@@ -229,4 +235,13 @@ Route::prefix('admin')->middleware(['auth', AdminOnly::class])->group(function (
|
|||||||
Route::delete('/ruoli/{id}', [RuoloController::class, 'destroy'])->name('admin.ruoli.destroy');
|
Route::delete('/ruoli/{id}', [RuoloController::class, 'destroy'])->name('admin.ruoli.destroy');
|
||||||
|
|
||||||
Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('admin.activity-logs.index');
|
Route::get('/activity-logs', [ActivityLogController::class, 'index'])->name('admin.activity-logs.index');
|
||||||
|
|
||||||
|
// Backup
|
||||||
|
Route::get('/backup', [\App\Http\Controllers\Admin\BackupController::class, 'index'])->name('admin.backup.index');
|
||||||
|
Route::post('/backup/run', [\App\Http\Controllers\Admin\BackupController::class, 'run'])->name('admin.backup.run');
|
||||||
|
Route::get('/backup/download/{filename}', [\App\Http\Controllers\Admin\BackupController::class, 'download'])->name('admin.backup.download');
|
||||||
|
Route::delete('/backup/{filename}', [\App\Http\Controllers\Admin\BackupController::class, 'destroy'])->name('admin.backup.destroy');
|
||||||
|
Route::post('/backup/config', [\App\Http\Controllers\Admin\BackupController::class, 'updateConfig'])->name('admin.backup.config');
|
||||||
|
Route::post('/backup/toggle-auto', [\App\Http\Controllers\Admin\BackupController::class, 'toggleAuto'])->name('admin.backup.toggle-auto');
|
||||||
|
Route::post('/backup/save-auto', [\App\Http\Controllers\Admin\BackupController::class, 'saveAutoConfig'])->name('admin.backup.save-auto');
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<?php $__env->startSection('title', 'Backup - ' . $appName); ?>
|
||||||
|
<?php $__env->startSection('page_title', 'Backup e Migrazione'); ?>
|
||||||
|
<?php $__env->startSection('breadcrumbs'); ?>
|
||||||
|
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/utenti">Admin</a></li>
|
||||||
|
<li class="breadcrumb-item active">Backup</li>
|
||||||
|
<?php $__env->stopSection(); ?>
|
||||||
|
|
||||||
|
<?php $__env->startSection('content'); ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
|
||||||
|
<div class="card card-outline card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-hdd mr-2"></i>Backup Disponibili</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<form method="POST" action="<?php echo e(route('admin.backup.run')); ?>" style="display:inline">
|
||||||
|
<?php echo csrf_field(); ?>
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-play mr-1"></i> Esegui Backup Ora
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<?php if(count($backups) > 0): ?>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-striped mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Dimensione</th>
|
||||||
|
<th>Contenuto</th>
|
||||||
|
<th class="text-center">Azioni</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php $__currentLoopData = $backups; $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $backup): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?>
|
||||||
|
<tr>
|
||||||
|
<td><code><?php echo e($backup['filename']); ?></code></td>
|
||||||
|
<td><?php echo e(date('d/m/Y H:i', $backup['last_modified'])); ?></td>
|
||||||
|
<td><?php echo e($backup['size_formatted']); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php if($backup['manifest']): ?>
|
||||||
|
<span class="badge badge-info" title="PHP <?php echo e($backup['manifest']['php_version'] ?? '?'); ?>, Laravel <?php echo e($backup['manifest']['laravel_version'] ?? '?'); ?>">
|
||||||
|
<i class="fas fa-database mr-1"></i>DB
|
||||||
|
</span>
|
||||||
|
<?php if(isset($backup['manifest']['db_name'])): ?>
|
||||||
|
<small class="text-muted"><?php echo e($backup['manifest']['db_name']); ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-secondary">N/A</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="<?php echo e(route('admin.backup.download', $backup['filename'])); ?>" class="btn btn-success btn-sm" title="Scarica">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</a>
|
||||||
|
<form method="POST" action="<?php echo e(route('admin.backup.destroy', $backup['filename'])); ?>" style="display:inline" onsubmit="return confirm('Eliminare definitivamente questo backup?')">
|
||||||
|
<?php echo csrf_field(); ?>
|
||||||
|
<?php echo method_field('DELETE'); ?>
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm" title="Elimina">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; $__env->popLoop(); $loop = $__env->getLastLoop(); ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-hdd fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted mb-0">Nessun backup disponibile.</p>
|
||||||
|
<p class="text-muted">Clicca "Esegui Backup Ora" per creare il primo backup.</p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card card-outline card-info">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-info-circle mr-2"></i>Cosa Include il Backup</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<table class="table table-bordered mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Componente</th>
|
||||||
|
<th>Incluso</th>
|
||||||
|
<th>Dettagli</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-database text-primary mr-2"></i> Database (MySQL)</td>
|
||||||
|
<td><span class="badge badge-success">Sempre</span></td>
|
||||||
|
<td>Struttura, dati, stored procedure, eventi, trigger</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-file text-warning mr-2"></i> File caricati</td>
|
||||||
|
<td>
|
||||||
|
<?php if($config['backup_include_files']): ?>
|
||||||
|
<span class="badge badge-success">Sì</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-secondary">No</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>Documenti, avatar, gallerie — dalla cartella storage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-cog text-secondary mr-2"></i> Configurazione (.env)</td>
|
||||||
|
<td>
|
||||||
|
<?php if($config['backup_include_env']): ?>
|
||||||
|
<span class="badge badge-success">Sì</span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="badge badge-secondary">No</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>APP_KEY, DB credentials, SMTP, OAuth token</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><i class="fas fa-tag text-info mr-2"></i> Metadati</td>
|
||||||
|
<td><span class="badge badge-success">Sempre</span></td>
|
||||||
|
<td>Manifest JSON con versione app, PHP, Laravel</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<div class="card card-outline card-secondary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-sliders-h mr-2"></i>Configurazione Backup</h3>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="<?php echo e(route('admin.backup.config')); ?>">
|
||||||
|
<?php echo csrf_field(); ?>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_path">Percorso backup</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><code>storage/app/</code></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="backup_path" id="backup_path" class="form-control" value="<?php echo e($config['backup_path']); ?>">
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">Cartella dove salvare i backup (relativa a storage/app/)</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_retention_days">Ritenzione (giorni)</label>
|
||||||
|
<input type="number" name="backup_retention_days" id="backup_retention_days" class="form-control" value="<?php echo e($config['backup_retention_days']); ?>" min="1" max="365">
|
||||||
|
<small class="text-muted">I backup più vecchi vengono eliminati automaticamente</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="backup_include_files" name="backup_include_files" value="1" <?php echo e($config['backup_include_files'] ? 'checked' : ''); ?>>
|
||||||
|
<label class="custom-control-label" for="backup_include_files">Includi file caricati</label>
|
||||||
|
</div>
|
||||||
|
<div class="custom-control custom-switch mt-2">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="backup_include_env" name="backup_include_env" value="1" <?php echo e($config['backup_include_env'] ? 'checked' : ''); ?>>
|
||||||
|
<label class="custom-control-label" for="backup_include_env">Includi .env (configurazione)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button type="submit" class="btn btn-secondary btn-block">
|
||||||
|
<i class="fas fa-save mr-1"></i> Salva Configurazione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card card-outline card-warning">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-clock mr-2"></i>Backup Automatico</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<?php $autoEnabled = \App\Models\AppSetting::getSetting('backup_auto_enabled', false) ?>
|
||||||
|
<form method="POST" action="<?php echo e(route('admin.backup.toggle-auto')); ?>" style="display:inline">
|
||||||
|
<?php echo csrf_field(); ?>
|
||||||
|
<button type="submit" class="btn btn-sm <?php echo e($autoEnabled ? 'btn-success' : 'btn-secondary'); ?>">
|
||||||
|
<i class="fas <?php echo e($autoEnabled ? 'fa-check-circle' : 'fa-times-circle'); ?> mr-1"></i>
|
||||||
|
<?php echo e($autoEnabled ? 'Attivo' : 'Disattivo'); ?>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="<?php echo e(route('admin.backup.save-auto')); ?>">
|
||||||
|
<?php echo csrf_field(); ?>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="backup_auto_frequency">Frequenza</label>
|
||||||
|
<select name="backup_auto_frequency" id="backup_auto_frequency" class="form-control">
|
||||||
|
<option value="daily" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'daily' ? 'selected' : ''); ?>>Giornaliero</option>
|
||||||
|
<option value="weekly" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'weekly' ? 'selected' : ''); ?>>Settimanale</option>
|
||||||
|
<option value="monthly" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_frequency', 'daily') === 'monthly' ? 'selected' : ''); ?>>Mensile</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<label for="backup_auto_hour">Ora di esecuzione</label>
|
||||||
|
<select name="backup_auto_hour" id="backup_auto_hour" class="form-control">
|
||||||
|
<?php for($h = 0; $h <= 23; $h++): ?>
|
||||||
|
<option value="<?php echo e($h); ?>" <?php echo e(\App\Models\AppSetting::getSetting('backup_auto_hour', 3) == $h ? 'selected' : ''); ?>><?php echo e(sprintf('%02d:00', $h)); ?></option>
|
||||||
|
<?php endfor; ?>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Ora del giorno (formato 24h). Consigliato: 03:00 (notte)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<button type="submit" class="btn btn-warning btn-block">
|
||||||
|
<i class="fas fa-save mr-1"></i> Salva Automatico
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card card-outline card-danger">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-truck mr-2"></i>Migrazione Server</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted">Scarica il backup, poi esegui lo script di setup sul nuovo server per una migrazione completa.</p>
|
||||||
|
<a href="<?php echo e(route('help')); ?>#help-backup" class="btn btn-danger btn-block" target="_blank">
|
||||||
|
<i class="fas fa-book mr-1"></i> Guida alla Migrazione
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle mr-2"></i>
|
||||||
|
<strong>Importante:</strong> Per il backup automatico, assicurati che il cron di Laravel sia configurato sul server:
|
||||||
|
<code class="ml-2">* * * * * cd <?php echo e(base_path()); ?> && php artisan schedule:run >> /dev/null 2>&1</code>
|
||||||
|
</div>
|
||||||
|
<?php $__env->stopSection(); ?>
|
||||||
|
|
||||||
|
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/admin/backup/index.blade.php ENDPATH**/ ?>
|
||||||
@@ -0,0 +1,940 @@
|
|||||||
|
<?php $__env->startSection('title', 'Guida - ' . $appName); ?>
|
||||||
|
<?php $__env->startSection('page_title', 'Guida alla Configurazione'); ?>
|
||||||
|
<?php $__env->startSection('breadcrumbs'); ?>
|
||||||
|
<li class="breadcrumb-item"><a href="/">Home</a></li>
|
||||||
|
<li class="breadcrumb-item active">Guida</li>
|
||||||
|
<?php $__env->stopSection(); ?>
|
||||||
|
|
||||||
|
<?php $__env->startSection('content'); ?>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card card-outline card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Argomenti</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<ul class="nav nav-pills flex-column" id="helpTabs">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" href="#help-webdav" data-toggle="tab">
|
||||||
|
<i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-googledrive" data-toggle="tab">
|
||||||
|
<i class="fab fa-google-drive mr-2"></i> Google Drive
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-tipologie" data-toggle="tab">
|
||||||
|
<i class="fas fa-tags mr-2"></i> Tipologie e Dati
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-email" data-toggle="tab">
|
||||||
|
<i class="fas fa-envelope mr-2"></i> Email / SMTP
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-installazione" data-toggle="tab">
|
||||||
|
<i class="fas fa-terminal mr-2"></i> Installazione
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-backup" data-toggle="tab">
|
||||||
|
<i class="fas fa-hdd mr-2"></i> Backup e Migrazione
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#help-docker" data-toggle="tab">
|
||||||
|
<i class="fab fa-docker mr-2"></i> Docker
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="tab-content">
|
||||||
|
|
||||||
|
<div class="tab-pane active" id="help-webdav">
|
||||||
|
<div class="card card-outline card-info">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-cloud mr-2"></i> WebDAV / Nextcloud</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Cos'è</h5>
|
||||||
|
<p>WebDAV è un protocollo che permette di accedere a file su server remoti. Nextcloud lo utilizza per la sincronizzazione dei file.</p>
|
||||||
|
|
||||||
|
<h5>Configurazione</h5>
|
||||||
|
<p>Vai in <strong>Impostazioni → Repository</strong> e crea un nuovo repository di tipo WebDAV.</p>
|
||||||
|
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Campi obbligatori</h6>
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-3">Nome</dt>
|
||||||
|
<dd class="col-sm-9">Un nome descrittivo (es. "Nextcloud Lavoro")</dd>
|
||||||
|
<dt class="col-sm-3">Base URI</dt>
|
||||||
|
<dd class="col-sm-9">L'URL del tuo server WebDAV.
|
||||||
|
<br><em>Nextcloud:</em> <code>https://tuodominio.it/remote.php/dav/files/utente/</code>
|
||||||
|
<br><em>Altri:</em> <code>https://server.webdav.it/</code></dd>
|
||||||
|
<dt class="col-sm-3">Root</dt>
|
||||||
|
<dd class="col-sm-9">Percorso base all'interno del server (es. <code>/Documenti/</code>). Lascia <code>/</code> per usare la root.</dd>
|
||||||
|
<dt class="col-sm-3">Username</dt>
|
||||||
|
<dd class="col-sm-9">Il tuo nome utente</dd>
|
||||||
|
<dt class="col-sm-3">Password</dt>
|
||||||
|
<dd class="col-sm-9">La password dell'account</dd>
|
||||||
|
<dt class="col-sm-3">Auth Type</dt>
|
||||||
|
<dd class="col-sm-9"><code>Basic</code> per la maggior parte dei server (Nextcloud, ownCloud).</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">⚠️ Nome utente con @ (chiocciola)</h5>
|
||||||
|
<p>Se il tuo nome utente contiene il carattere <code>@</code> (es. <code>mario.rossi@azienda.com</code>), segui queste regole:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Nel campo "Base URI":</strong> l'indirizzo email deve essere <strong>URL-encoded</strong>:
|
||||||
|
<br><code>https://server.it/remote.php/dav/files/mario.rossi%40azienda.com/</code>
|
||||||
|
<br><small>(il carattere <code>@</code> diventa <code>%40</code>)</small></li>
|
||||||
|
<li><strong>Nel campo "Username":</strong> inserisci l'indirizzo email <strong>normale</strong> con la <code>@</code> vera.</li>
|
||||||
|
</ul>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Il sistema gestisce automaticamente la differenza tra l'URI (che richiede <code>%40</code>) e l'autenticazione (che usa la <code>@</code> reale).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Test connessione</h5>
|
||||||
|
<p>Dopo aver salvato, usa il pulsante <span class="badge badge-success"><i class="fas fa-plug"></i> Test</span> nella tabella dei repository per verificare che la connessione funzioni.</p>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Importare file</h5>
|
||||||
|
<p>Apri la sezione <strong>Documenti</strong>, clicca sul pulsante del repository nella toolbar per sfogliare i file remoti, poi clicca <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span> su un file per importarlo come documento locale.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="tab-pane" id="help-googledrive">
|
||||||
|
<div class="card card-outline card-danger">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fab fa-google-drive mr-2"></i> Google Drive</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Cos'è</h5>
|
||||||
|
<p>Integrazione con Google Drive per sfogliare e importare file direttamente dal tuo spazio Google Drive.</p>
|
||||||
|
|
||||||
|
<h5>Requisiti</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Un account Google / Google Workspace</li>
|
||||||
|
<li>Un progetto sulla <strong>Google Cloud Console</strong> con l'API Google Drive abilitata</li>
|
||||||
|
<li>Credenziali OAuth 2.0 (Client ID e Client Secret)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Passo 1: Creare il progetto Google Cloud</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Vai su <a href="https://console.developers.google.com/" target="_blank">Google Cloud Console</a></li>
|
||||||
|
<li>Crea un nuovo progetto o selezionane uno esistente</li>
|
||||||
|
<li>Vai a <strong>API e servizi → Libreria</strong></li>
|
||||||
|
<li>Cerca "<strong>Google Drive API</strong>" e <strong>abilitala</strong></li>
|
||||||
|
<li>Vai a <strong>API e servizi → Credenziali</strong></li>
|
||||||
|
<li>Clicca "<strong>Crea credenziali → ID client OAuth</strong>"</li>
|
||||||
|
<li>Tipo applicazione: "<strong>Applicazione Web</strong>"</li>
|
||||||
|
<li><strong>URI di reindirizzamento autorizzato:</strong> aggiungi <code><?php echo e(url('/auth/google-drive/callback')); ?></code></li>
|
||||||
|
<li>Completa la creazione e <strong>copia Client ID e Client Secret</strong></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>Passo 2: Configurazione nell'app</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Vai in <strong>Impostazioni → Repository</strong></li>
|
||||||
|
<li>Clicca "<strong>Nuovo Repository</strong>" e seleziona tipo <strong>Google Drive</strong></li>
|
||||||
|
<li>Inserisci un nome descrittivo (es. "Drive Lavoro")</li>
|
||||||
|
<li>Incolla <strong>Client ID</strong> e <strong>Client Secret</strong></li>
|
||||||
|
<li><strong>Folder ID (opzionale):</strong> se vuoi limitare l'accesso a una cartella specifica, inserisci il suo ID (lo trovi nell'URL quando apri la cartella in Google Drive: <code>https://drive.google.com/drive/folders/<strong>ID_QUI</strong></code>)</li>
|
||||||
|
<li>Clicca "<strong>Autorizza Google Drive</strong>" — verrai reindirizzato a Google per concedere i permessi</li>
|
||||||
|
<li>Dopo l'autorizzazione, il refresh token verrà salvato automaticamente</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5>⚠️ Google Drive API non abilitata</h5>
|
||||||
|
<p>Se vedi l'errore <em>"Google Drive API non abilitata"</em> nel test connessione, significa che l'API Drive non è stata attivata nel tuo progetto Google Cloud.</p>
|
||||||
|
<div class="callout callout-danger">
|
||||||
|
<h6>Soluzione</h6>
|
||||||
|
<p>Vai su <a href="https://console.developers.google.com/apis/api/drive.googleapis.com/overview" target="_blank">
|
||||||
|
https://console.developers.google.com/apis/api/drive.googleapis.com/overview
|
||||||
|
</a></p>
|
||||||
|
<p>Seleziona il progetto corretto dall'header in alto, clicca <strong>"Enable"</strong> e attendi qualche minuto prima di riprovare.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Importare file</h5>
|
||||||
|
<p>Stessa procedura di WebDAV: apri <strong>Documenti</strong>, clicca sul repository Drive, sfoglia i file e usa <span class="badge badge-success"><i class="fas fa-save"></i> Salva tra i documenti</span>.</p>
|
||||||
|
|
||||||
|
<h5 class="mt-4">⚠️ File Google (Documenti, Fogli, Presentazioni)</h5>
|
||||||
|
<p>I file creati con Google Workspace (Documenti Google, Fogli Google, Presentazioni Google, Disegni Google) non possono essere scaricati direttamente perché non hanno un formato binario nativo.</p>
|
||||||
|
<p>Il sistema li converte automaticamente in formato Office Open XML durante l'import:</p>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>File Google</th><th>Formato convertito</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Documento Google</td><td><code>.docx</code> (Word)</td></tr>
|
||||||
|
<tr><td>Foglio Google</td><td><code>.xlsx</code> (Excel)</td></tr>
|
||||||
|
<tr><td>Presentazione Google</td><td><code>.pptx</code> (PowerPoint)</td></tr>
|
||||||
|
<tr><td>Disegno Google</td><td><code>.png</code> (Immagine)</td></tr>
|
||||||
|
<tr><td>Altri (Moduli, Script)</td><td><code>.pdf</code> (PDF)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Il nome del file viene automaticamente esteso con l'estensione corretta (es. "Relazione" → "Relazione.docx").
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="tab-pane" id="help-tipologie">
|
||||||
|
<div class="card card-outline card-success">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-tags mr-2"></i> Tipologie e Gestione Dati</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Tipologie Documenti</h5>
|
||||||
|
<p>Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).</p>
|
||||||
|
<p>Si gestiscono in <strong>Impostazioni → Tipologie Documenti</strong>. Puoi aggiungere, rinominare, riordinare ed eliminare tipologie (tranne quelle con documenti associati).</p>
|
||||||
|
|
||||||
|
<h5>Tipologie Individui</h5>
|
||||||
|
<p>Classificano gli individui (es. socio, volontario, dipendente). Si gestiscono in <strong>Impostazioni → Tipologie</strong>.</p>
|
||||||
|
|
||||||
|
<h5>Tipologie Eventi</h5>
|
||||||
|
<p>Classificano gli eventi (es. riunione, assemblea, formazione). Si gestiscono in <strong>Impostazioni → Tipologie Eventi</strong>.</p>
|
||||||
|
|
||||||
|
<h5>Ruoli Individui</h5>
|
||||||
|
<p>Definiscono il ruolo o la carica di un individuo (es. presidente, segretario, tesoriere). Si gestiscono in <strong>Impostazioni → Ruoli</strong>.</p>
|
||||||
|
|
||||||
|
<h5>Visibilità Documenti</h5>
|
||||||
|
<p>Ogni documento può avere diversi livelli di visibilità:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Pubblico</strong> — visibile a tutti gli utenti</li>
|
||||||
|
<li><strong>Individuo</strong> — visibile solo a uno specifico individuo</li>
|
||||||
|
<li><strong>Gruppo</strong> — visibile solo ai membri di un gruppo</li>
|
||||||
|
<li><strong>Evento</strong> — visibile solo ai partecipanti di un evento</li>
|
||||||
|
<li><strong>Mailing</strong> — visibile solo ai membri di una mailing list</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="tab-pane" id="help-email">
|
||||||
|
<div class="card card-outline card-warning">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-envelope mr-2"></i> Email / SMTP</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Configurazione Email</h5>
|
||||||
|
<p>Vai in <strong>Impostazioni → Email</strong> per configurare la posta elettronica.</p>
|
||||||
|
|
||||||
|
<h5>Server IMAP (Ricezione)</h5>
|
||||||
|
<p>Usato per ricevere e leggere le email direttamente nell'app.</p>
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Campi consigliati per provider comuni</h6>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Gmail</td><td><code>imap.gmail.com</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Outlook/Office 365</td><td><code>outlook.office365.com</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Libero</td><td><code>imapmail.libero.it</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Aruba</td><td><code>imap.aruba.it</code></td><td><code>993</code></td><td>SSL</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Server SMTP (Invio)</h5>
|
||||||
|
<p>Usato per inviare email dall'app. Supporta account multipli (mittenti).</p>
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Campi consigliati per provider comuni</h6>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Provider</th><th>Host</th><th>Porta</th><th>Crittografia</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Gmail</td><td><code>smtp.gmail.com</code></td><td><code>587</code></td><td>TLS</td></tr>
|
||||||
|
<tr><td>Outlook/Office 365</td><td><code>smtp.office365.com</code></td><td><code>587</code></td><td>TLS</td></tr>
|
||||||
|
<tr><td>Libero</td><td><code>smtp.libero.it</code></td><td><code>465</code></td><td>SSL</td></tr>
|
||||||
|
<tr><td>Aruba</td><td><code>smtp.aruba.it</code></td><td><code>465</code></td><td>SSL</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Gmail / Google Workspace</h5>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
Gmail richiede una <strong>Password per l'app</strong> (non la password normale) se usi la verifica in due passaggi. Puoi generarla su
|
||||||
|
<a href="https://myaccount.google.com/apppasswords" target="_blank">https://myaccount.google.com/apppasswords</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Mailing</h5>
|
||||||
|
<p>Le <strong>Mailing List</strong> permettono di inviare email a gruppi predefiniti di destinatari.</p>
|
||||||
|
<ul>
|
||||||
|
<li>Crea una mailing list in <strong>Mailing List → Nuova</strong></li>
|
||||||
|
<li>Puoi popolare la lista da individui, gruppi o manualmente</li>
|
||||||
|
<li>Invia email alla lista da <strong>Email → Nuova Email</strong>, selezionando la mailing list come destinatario</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h5>Test connessione</h5>
|
||||||
|
<p>Dopo aver configurato i server, usa il pulsante <span class="badge badge-success"><i class="fas fa-vial"></i> Test Connessione</span> per verificare che IMAP e SMTP funzionino correttamente.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="tab-pane" id="help-backup">
|
||||||
|
<div class="card card-outline card-danger">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-hdd mr-2"></i> Backup e Migrazione Server</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Cos'è</h5>
|
||||||
|
<p>Il sistema di backup crea un archivio ZIP contenente tutto il necessario per ripristinare l'applicazione su un nuovo server. Include database, file caricati e configurazione.</p>
|
||||||
|
|
||||||
|
<h5>Cosa include il backup</h5>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Componente</th><th>Incluso</th><th>Dettagli</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Database (MySQL)</td><td><span class="badge badge-success">Sempre</span></td><td>Struttura, dati, stored procedure, eventi, trigger</td></tr>
|
||||||
|
<tr><td>File caricati</td><td><span class="badge badge-success">Opzionale</span></td><td>Documenti, avatar, gallerie</td></tr>
|
||||||
|
<tr><td>Configurazione .env</td><td><span class="badge badge-success">Opzionale</span></td><td>APP_KEY, credenziali DB, SMTP, token OAuth</td></tr>
|
||||||
|
<tr><td>Metadati</td><td><span class="badge badge-success">Sempre</span></td><td>Versione app, PHP, Laravel</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Come eseguire un backup</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6>Via Interfaccia Web</h6>
|
||||||
|
<ol class="mb-0">
|
||||||
|
<li>Vai su <strong>Admin → Backup</strong></li>
|
||||||
|
<li>Verifica le impostazioni (includi files/.env)</li>
|
||||||
|
<li>Clicca <span class="badge badge-primary"><i class="fas fa-play"></i> Esegui Backup Ora</span></li>
|
||||||
|
<li>Scarica il file ZIP generato</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<h6>Via Comando</h6>
|
||||||
|
<pre class="mb-0"><code>php artisan backup:run</code></pre>
|
||||||
|
<p class="mb-0 mt-2 small">Il file viene creato in <code>storage/app/backups/</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Backup automatico</h5>
|
||||||
|
<p>Puoi abilitare il backup automatico dalla pagina <strong>Admin → Backup</strong>:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Frequenza:</strong> giornaliero, settimanale o mensile</li>
|
||||||
|
<li><strong>Ora:</strong> consigliata 03:00 (notte)</li>
|
||||||
|
<li><strong>Requisito:</strong> il cron di Laravel deve essere configurato sul server</li>
|
||||||
|
</ul>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-clock mr-2"></i>
|
||||||
|
Per il backup automatico, aggiungi questo cron:
|
||||||
|
<pre class="mt-2 mb-0"><code>* * * * * cd <?php echo e(base_path()); ?> && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h4><i class="fas fa-truck mr-2"></i> Procedura di Migrazione Completa</h4>
|
||||||
|
<p>Segui questa procedura per migrare Glastree su un nuovo server LAMP (Linux, Apache, MySQL, PHP).</p>
|
||||||
|
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||||
|
<strong>ATTENZIONE: APP_KEY</strong>
|
||||||
|
<p class="mb-0 mt-2">L'APP_KEY cripta i dati sensibili (password email SMTP, token OAuth Google Drive, ecc.).</p>
|
||||||
|
<ul class="mt-2">
|
||||||
|
<li><strong>Se hai il .env originale dal backup:</strong> NON rigenerare APP_KEY — mantieni i dati criptati accessibili.</li>
|
||||||
|
<li><strong>Se hai perso il .env:</strong> DEVI rigenerare APP_KEY, ma perderai l'accesso ai dati criptati esistenti. Dovrai riconfigurare SMTP e OAuth.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 1: Preparazione del server</h5>
|
||||||
|
<pre><code>sudo apt update && sudo apt upgrade -y</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 2: Installa LAMP + PHP 8.4</h5>
|
||||||
|
<pre><code>sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
|
||||||
|
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
|
||||||
|
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
|
||||||
|
composer git unzip
|
||||||
|
|
||||||
|
sudo a2enmod rewrite</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 3: Clona l'applicazione</h5>
|
||||||
|
<pre><code>cd /var/www
|
||||||
|
git clone <URL_REPOSITORY> glastree
|
||||||
|
cd glastree
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
npm ci && npm run build</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 4: Carica il backup</h5>
|
||||||
|
<p>Copia il file <code>backup.zip</code> sul nuovo server e estrailo:</p>
|
||||||
|
<pre><code>mkdir -p /tmp/restore
|
||||||
|
unzip backup.zip -d /tmp/restore/
|
||||||
|
ls -la /tmp/restore/
|
||||||
|
# Dovresti vedere: database.sql .env files/ manifest.json</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 5: Crea database e importa</h5>
|
||||||
|
<pre><code>sudo mysql -u root <<EOF
|
||||||
|
CREATE DATABASE glastree CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
CREATE USER 'glastree'@'localhost' IDENTIFIED BY 'ScegliPasswordFortep4ss!';
|
||||||
|
GRANT ALL ON glastree.* TO 'glastree'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mysql -u glastree -p glastree < /tmp/restore/database.sql</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 6: Configura .env</h5>
|
||||||
|
<pre><code>cp /tmp/restore/.env .env
|
||||||
|
# Modifica se necessario:
|
||||||
|
# APP_URL=https://nuovo-dominio.it
|
||||||
|
# DB_HOST=localhost
|
||||||
|
# DB_DATABASE=glastree
|
||||||
|
# DB_USERNAME=glastree
|
||||||
|
# DB_PASSWORD=ScegliPasswordFortep4ss!
|
||||||
|
|
||||||
|
# DECIDI SULL'APP_KEY (leggi avviso sopra!)
|
||||||
|
# php artisan key:generate ← SOLO se non hai il .env originale</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 7: Ripristina file e permessi</h5>
|
||||||
|
<pre><code>cp -r /tmp/restore/files/* storage/app/documenti/
|
||||||
|
|
||||||
|
sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 755 storage bootstrap/cache
|
||||||
|
|
||||||
|
php artisan storage:link</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 8: Cache</h5>
|
||||||
|
<pre><code>php artisan optimize:clear
|
||||||
|
php artisan config:cache
|
||||||
|
php artisan route:cache
|
||||||
|
php artisan view:cache</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 9: Configura Apache</h5>
|
||||||
|
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName glastree.esempio.it
|
||||||
|
DocumentRoot /var/www/glastree/public
|
||||||
|
|
||||||
|
<Directory /var/www/glastree/public>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
|
||||||
|
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
APACHE
|
||||||
|
|
||||||
|
sudo a2dissite 000-default.conf
|
||||||
|
sudo a2ensite glastree.conf
|
||||||
|
sudo systemctl reload apache2</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 10: SSL Let's Encrypt</h5>
|
||||||
|
<pre><code>sudo apt install -y certbot python3-certbot-apache
|
||||||
|
sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 11: Cron per backup automatico</h5>
|
||||||
|
<pre><code>sudo crontab -u www-data -e
|
||||||
|
# Inserisci:
|
||||||
|
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Passo 12: Verifica finale</h5>
|
||||||
|
<ol>
|
||||||
|
<li>Apri https://glastree.esempio.it nel browser</li>
|
||||||
|
<li>Fai login con le credenziali esistenti</li>
|
||||||
|
<li>Verifica: Dashboard, Documenti, Impostazioni, Email</li>
|
||||||
|
<li>Se hai rigenerato APP_KEY: riconfigura SMTP e OAuth Google Drive</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h5>PHP Web Installer</h5>
|
||||||
|
<p>L'applicazione include un <strong>wizard di installazione web</strong> nella cartella <code>installer/</code>. Questo wizard gestisce sia installazioni fresh che ripristino da backup.</p>
|
||||||
|
<p>Vedi la sezione <strong>Installazione</strong> per la guida completa.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="tab-pane" id="help-docker">
|
||||||
|
<div class="card card-outline card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fab fa-docker mr-2"></i> Docker</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Creare un'immagine Docker con dati di base</h5>
|
||||||
|
<p>Puoi creare un'immagine Docker pronta all'uso che include l'applicazione e i dati di base (tipologie documenti, tipologie eventi, ruoli). Scegli la configurazione in base alle tue esigenze:</p>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6><i class="fas fa-database"></i> SQLite (Leggero)</h6>
|
||||||
|
<p class="mb-0 small">Database embedded, nessun servizio aggiuntivo. Ideale per demo, test, sviluppo locale o ambienti con poca concorrenza.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<h6><i class="fas fa-server"></i> MySQL (Standard)</h6>
|
||||||
|
<p class="mb-0 small">Database separato in un container MySQL. Configurazione standard, ideale per produzione, staging e ambienti con piu utenti.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">1. Dockerfile (comune a entrambe le modalita)</h5>
|
||||||
|
<p>Crea un file <code>Dockerfile</code> nella root del progetto:</p>
|
||||||
|
<pre><code>FROM php:8.4-apache
|
||||||
|
|
||||||
|
# Abilita mod_rewrite
|
||||||
|
RUN docker-php-ext-enable opcache \
|
||||||
|
&& a2enmod rewrite
|
||||||
|
|
||||||
|
# Installa estensioni PHP (sia SQLite che MySQL)
|
||||||
|
RUN docker-php-ext-install pdo_mysql pdo_sqlite bcmath gd zip
|
||||||
|
|
||||||
|
# Installa Composer
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
# Configura DocumentRoot per Laravel
|
||||||
|
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
|
||||||
|
RUN sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
|
||||||
|
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf
|
||||||
|
|
||||||
|
# Copia l'applicazione
|
||||||
|
COPY . /var/www/html/
|
||||||
|
|
||||||
|
# Installa dipendenze e compila asset
|
||||||
|
RUN composer install --no-dev --optimize-autoloader \
|
||||||
|
&& mkdir -p storage/app/public \
|
||||||
|
&& mkdir -p storage/framework/cache/data \
|
||||||
|
&& mkdir -p storage/framework/sessions \
|
||||||
|
&& mkdir -p storage/framework/views \
|
||||||
|
&& mkdir -p storage/logs \
|
||||||
|
&& mkdir -p bootstrap/cache \
|
||||||
|
&& chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
|
||||||
|
# Entrypoint per inizializzazione
|
||||||
|
COPY docker-entrypoint.sh /usr/local/bin/
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["docker-entrypoint.sh"]
|
||||||
|
CMD ["apache2-foreground"]</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">2. Entrypoint Script (comune)</h5>
|
||||||
|
<p>Crea <code>docker-entrypoint.sh</code>. Lo script rileva automaticamente la configurazione in base alle variabili d'ambiente:</p>
|
||||||
|
<pre><code>#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Se .env non esiste, lo genera dalle variabili d'ambiente
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
if [ "$DB_CONNECTION" = "mysql" ]; then
|
||||||
|
# Configura MySQL
|
||||||
|
sed -i "s/DB_CONNECTION=mysql/DB_CONNECTION=mysql/" .env
|
||||||
|
sed -i "s/DB_HOST=.*/DB_HOST=${DB_HOST:-mysql}/" .env
|
||||||
|
sed -i "s/DB_PORT=.*/DB_PORT=${DB_PORT:-3306}/" .env
|
||||||
|
sed -i "s/DB_DATABASE=.*/DB_DATABASE=${DB_DATABASE:-glastree}/" .env
|
||||||
|
sed -i "s/DB_USERNAME=.*/DB_USERNAME=${DB_USERNAME:-glastree}/" .env
|
||||||
|
sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=${DB_PASSWORD:-secret}/" .env
|
||||||
|
|
||||||
|
# Attende che MySQL sia pronto
|
||||||
|
echo "Attendo MySQL..."
|
||||||
|
until php -r "new PDO('mysql:host=${DB_HOST:-mysql};dbname=${DB_DATABASE:-glastree}', '${DB_USERNAME:-glastree}', '${DB_PASSWORD:-secret}');" 2>/dev/null; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "MySQL pronto."
|
||||||
|
else
|
||||||
|
# Configura SQLite
|
||||||
|
sed -i 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env
|
||||||
|
sed -i 's/DB_HOST=.*//' .env
|
||||||
|
sed -i 's/DB_PORT=.*//' .env
|
||||||
|
sed -i 's/DB_DATABASE=.*/DB_DATABASE=\/var\/www\/html\/database\/database.sqlite/' .env
|
||||||
|
sed -i 's/DB_USERNAME=.*//' .env
|
||||||
|
sed -i 's/DB_PASSWORD=.*//' .env
|
||||||
|
|
||||||
|
touch database/database.sqlite
|
||||||
|
chmod 664 database/database.sqlite
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Genera APP_KEY
|
||||||
|
php artisan key:generate --force
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Esegui migration e seed se il database e vuoto
|
||||||
|
if [ ! -f .db_initialized ]; then
|
||||||
|
php artisan migrate --force
|
||||||
|
php artisan db:seed --force
|
||||||
|
touch .db_initialized
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Configura storage link
|
||||||
|
php artisan storage:link --force 2>/dev/null || true
|
||||||
|
|
||||||
|
exec "$@"</code></pre>
|
||||||
|
<pre><code>chmod +x docker-entrypoint.sh</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">3. Seed Base Dati (comune)</h5>
|
||||||
|
<p>Per includere tipologie documenti, tipologie eventi e ruoli, crea un seeder dedicato:</p>
|
||||||
|
<pre><code>php artisan make:seeder DockerBaseDataSeeder</code></pre>
|
||||||
|
<p>Contenuto di <code>database/seeders/DockerBaseDataSeeder.php</code>:</p>
|
||||||
|
<pre><code><?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class DockerBaseDataSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Tipologie Documenti
|
||||||
|
$tipologieDoc = ['documento', 'avatar', 'galleria', 'statuto', 'programma', 'locandina', 'altro'];
|
||||||
|
foreach ($tipologieDoc as $ord => $nome) {
|
||||||
|
DB::table('tipologie_documenti')->updateOrInsert(
|
||||||
|
['nome' => $nome],
|
||||||
|
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tipologie Eventi
|
||||||
|
$tipologieEventi = [
|
||||||
|
['nome' => 'catechesi', 'descrizione' => 'Catechesi'],
|
||||||
|
['nome' => 'liturgia', 'descrizione' => 'Liturgia'],
|
||||||
|
['nome' => 'animazione', 'descrizione' => 'Animazione'],
|
||||||
|
['nome' => 'formazione', 'descrizione' => 'Formazione'],
|
||||||
|
['nome' => 'incontro', 'descrizione' => 'Incontro'],
|
||||||
|
['nome' => 'riunione', 'descrizione' => 'Riunione'],
|
||||||
|
['nome' => 'festa', 'descrizione' => 'Festa'],
|
||||||
|
['nome' => 'altro', 'descrizione' => 'Altro'],
|
||||||
|
];
|
||||||
|
foreach ($tipologieEventi as $ord => $t) {
|
||||||
|
DB::table('tipologie_eventi')->updateOrInsert(
|
||||||
|
['nome' => $t['nome']],
|
||||||
|
['descrizione' => $t['descrizione'], 'ordine' => $ord + 1, 'attiva' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ruoli
|
||||||
|
$ruoli = ['presidente', 'vicepresidente', 'segretario', 'tesoriere', 'consigliere', 'volontario', 'socio', 'altro'];
|
||||||
|
foreach ($ruoli as $ord => $nome) {
|
||||||
|
DB::table('ruoli')->updateOrInsert(
|
||||||
|
['nome' => $nome],
|
||||||
|
['descrizione' => ucfirst($nome), 'ordine' => $ord + 1, 'attiva' => true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
<p>Registralo in <code>database/seeders/DatabaseSeeder.php</code>:</p>
|
||||||
|
<pre><code>$this->call([
|
||||||
|
// ... altri seeder ...
|
||||||
|
DockerBaseDataSeeder::class,
|
||||||
|
]);</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">4. Docker Compose</h5>
|
||||||
|
|
||||||
|
<h6 class="text-info"><i class="fas fa-database mr-1"></i> Opzione A — SQLite (leggero)</h6>
|
||||||
|
<pre><code>services:
|
||||||
|
glastree:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- glastree_storage:/var/www/html/storage
|
||||||
|
- glastree_db:/var/www/html/database
|
||||||
|
environment:
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=false
|
||||||
|
- APP_URL=http://localhost:8080
|
||||||
|
- DB_CONNECTION=sqlite
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
glastree_storage:
|
||||||
|
glastree_db:</code></pre>
|
||||||
|
|
||||||
|
<h6 class="mt-3 text-success"><i class="fas fa-server mr-1"></i> Opzione B — MySQL (standard)</h6>
|
||||||
|
<pre><code>services:
|
||||||
|
glastree:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
volumes:
|
||||||
|
- glastree_storage:/var/www/html/storage
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- APP_ENV=production
|
||||||
|
- APP_DEBUG=false
|
||||||
|
- APP_URL=http://localhost:8080
|
||||||
|
- DB_CONNECTION=mysql
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_DATABASE=glastree
|
||||||
|
- DB_USERNAME=glastree
|
||||||
|
- DB_PASSWORD=secret
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
volumes:
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
environment:
|
||||||
|
- MYSQL_DATABASE=glastree
|
||||||
|
- MYSQL_USER=glastree
|
||||||
|
- MYSQL_PASSWORD=secret
|
||||||
|
- MYSQL_ROOT_PASSWORD=root_secret
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
glastree_storage:
|
||||||
|
mysql_data:</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">5. Build e avvio</h5>
|
||||||
|
<pre><code># Costruisci l'immagine (comune)
|
||||||
|
docker build -t glastree:latest .
|
||||||
|
|
||||||
|
# Avvia con SQLite
|
||||||
|
docker compose -f docker-compose.yml up -d
|
||||||
|
|
||||||
|
# Oppure avvia con MySQL
|
||||||
|
# docker compose -f docker-compose.mysql.yml up -d
|
||||||
|
|
||||||
|
# Apri nel browser
|
||||||
|
open http://localhost:8080</code></pre>
|
||||||
|
|
||||||
|
<h5 class="mt-4">6. Primo accesso</h5>
|
||||||
|
<p>Al primo avvio, l'entrypoint esegue automaticamente migration e seed. Troverai gia popolati:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Tipologie Documenti:</strong> documento, avatar, galleria, statuto, programma, locandina, altro</li>
|
||||||
|
<li><strong>Tipologie Eventi:</strong> catechesi, liturgia, animazione, formazione, incontro, riunione, festa, altro</li>
|
||||||
|
<li><strong>Ruoli:</strong> presidente, vicepresidente, segretario, tesoriere, consigliere, volontario, socio, altro</li>
|
||||||
|
</ul>
|
||||||
|
<p>Dovrai creare il primo utente amministratore via CLI:</p>
|
||||||
|
<pre><code>docker exec -it glastree php artisan tinker --execute="
|
||||||
|
\$u = new \App\Models\User;
|
||||||
|
\$u->name = 'Amministratore';
|
||||||
|
\$u->email = 'admin@esempio.it';
|
||||||
|
\$u->password = bcrypt('password-forte');
|
||||||
|
\$u->is_admin = true;
|
||||||
|
\$u->permissions = ['individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2];
|
||||||
|
\$u->save();
|
||||||
|
echo 'Utente creato con successo!';
|
||||||
|
"</code></pre>
|
||||||
|
|
||||||
|
<div class="callout callout-warning mt-4">
|
||||||
|
<h6><i class="fas fa-exclamation-triangle"></i> Note importanti</h6>
|
||||||
|
<ul class="mb-0">
|
||||||
|
<li><strong>Persistenza:</strong> i volumi Docker preservano storage e database tra i riavvii</li>
|
||||||
|
<li><strong>APP_KEY:</strong> viene generata al primo avvio — se elimini i volumi, la perdi e i dati criptati diventano inaccessibili</li>
|
||||||
|
<li><strong>MySQL in produzione:</strong> imposta password sicure per <code>MYSQL_PASSWORD</code> e <code>MYSQL_ROOT_PASSWORD</code></li>
|
||||||
|
<li><strong>File compose separati:</strong> puoi mantenere entrambi i file (<code>docker-compose.yml</code> per SQLite, <code>docker-compose.mysql.yml</code> per MySQL) e scegliere con <code>-f</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="tab-pane" id="help-installazione">
|
||||||
|
<div class="card card-outline card-dark">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title"><i class="fas fa-terminal mr-2"></i> Guida all'Installazione</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Panoramica</h5>
|
||||||
|
<p>Glastree include un <strong>wizard di installazione web</strong> che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-info">
|
||||||
|
<h6><i class="fas fa-rocket"></i> Fresh Install</h6>
|
||||||
|
<p class="mb-0">Installa l'applicazione da zero su un nuovo server: crea il database, esegue le migration, popola i dati di base e crea l'amministratore.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="callout callout-success">
|
||||||
|
<h6><i class="fas fa-upload"></i> Restore da Backup</h6>
|
||||||
|
<p class="mb-0">Ripristina un'installazione esistente da un file ZIP di backup: importa il database, ripristina i documenti e la configurazione.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Requisiti del Server</h5>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Componente</th><th>Versione Minima</th><th>Note</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Web Server</td><td>Apache 2.4+</td><td>Con mod_rewrite abilitato</td></tr>
|
||||||
|
<tr><td>PHP</td><td>8.2+ (consigliato 8.4)</td><td>Estensioni: pdo_mysql, mbstring, xml, curl, zip, gd, fileinfo</td></tr>
|
||||||
|
<tr><td>MySQL / MariaDB</td><td>MySQL 8.0+ / MariaDB 10.5+</td><td>Supporto utf8mb4</td></tr>
|
||||||
|
<tr><td>Composer</td><td>2.x</td><td>Per gestione dipendenze PHP</td></tr>
|
||||||
|
<tr><td>Node.js / npm</td><td>Node 20+ / npm 10+</td><td>Per compilazione asset frontend (opzionale se precompilati)</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Installazione Passo-Passo</h5>
|
||||||
|
|
||||||
|
<h6>Passo 1: Preparare il server LAMP</h6>
|
||||||
|
<pre><code>sudo apt update && sudo apt upgrade -y
|
||||||
|
|
||||||
|
sudo apt install -y apache2 mariadb-server php8.4 php8.4-cli \
|
||||||
|
php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl \
|
||||||
|
php8.4-zip php8.4-bcmath php8.4-gd php8.4-intl \
|
||||||
|
composer git unzip
|
||||||
|
|
||||||
|
sudo a2enmod rewrite
|
||||||
|
sudo systemctl restart apache2</code></pre>
|
||||||
|
|
||||||
|
<h6>Passo 2: Ottenere l'applicazione</h6>
|
||||||
|
<pre><code>cd /var/www
|
||||||
|
git clone <URL_REPOSITORY> glastree
|
||||||
|
cd glastree
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
npm ci && npm run build</code></pre>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="fas fa-info-circle"></i>
|
||||||
|
Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6>Passo 3: Permessi cartelle</h6>
|
||||||
|
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||||
|
|
||||||
|
<h6>Passo 4: Configurare Apache</h6>
|
||||||
|
<pre><code>sudo tee /etc/apache2/sites-available/glastree.conf <<APACHE
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName glastree.esempio.it
|
||||||
|
DocumentRoot /var/www/glastree/public
|
||||||
|
|
||||||
|
<Directory /var/www/glastree/public>
|
||||||
|
Options -Indexes +FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog \${APACHE_LOG_DIR}/glastree_error.log
|
||||||
|
CustomLog \${APACHE_LOG_DIR}/glastree_access.log combined
|
||||||
|
</VirtualHost>
|
||||||
|
APACHE
|
||||||
|
|
||||||
|
sudo a2dissite 000-default.conf
|
||||||
|
sudo a2ensite glastree.conf
|
||||||
|
sudo systemctl reload apache2</code></pre>
|
||||||
|
|
||||||
|
<h6>Passo 5: Avviare il wizard di installazione</h6>
|
||||||
|
<p>Apri il browser e visita l'indirizzo del tuo server:</p>
|
||||||
|
<pre><code>http://glastree.esempio.it/installer/</code></pre>
|
||||||
|
|
||||||
|
<p>Il wizard ti guidera attraverso 6 passi:</p>
|
||||||
|
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr><th>Passo</th><th>Descrizione</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>1. Benvenuto</td><td>Scegli Fresh Install o Restore da Backup</td></tr>
|
||||||
|
<tr><td>2. Requisiti</td><td>Verifica PHP, estensioni e permessi</td></tr>
|
||||||
|
<tr><td>3. Database</td><td>Configura MySQL (crea nuovo o usa esistente)</td></tr>
|
||||||
|
<tr><td>4. Installazione</td><td>Fresh: migration e seed. Backup: upload ZIP</td></tr>
|
||||||
|
<tr><td>5. Amministratore</td><td>Fresh: crea super-admin. Backup: verifica utenti</td></tr>
|
||||||
|
<tr><td>6. Finalizzazione</td><td>Cache, storage link, auto-eliminazione installer</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Attenzione:</strong> Al termine dell'installazione, la cartella <code>installer/</code> viene <strong>automaticamente eliminata</strong>. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Modalita Fresh Install</h5>
|
||||||
|
<p>Seleziona questa modalita quando installi Glastree per la prima volta su un server.</p>
|
||||||
|
<p>Cosa succede durante l'installazione:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Genera <code>APP_KEY</code> per la crittografia</li>
|
||||||
|
<li>Esegue tutte le migration per creare le tabelle del database</li>
|
||||||
|
<li>Popola i dati di base (diocesi, comuni, ruoli, preset)</li>
|
||||||
|
<li>Crea il collegamento <code>storage → public/storage</code></li>
|
||||||
|
<li>Crea l'utente amministratore con i permessi completi</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Modalita Restore da Backup</h5>
|
||||||
|
<p>Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.</p>
|
||||||
|
<p>Cosa serve:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Un file ZIP generato dalla pagina <strong>Admin → Backup</strong> del server originale</li>
|
||||||
|
<li>Il wizard accetta upload diretto del file o un percorso sul server</li>
|
||||||
|
</ul>
|
||||||
|
<p>Cosa succede durante il restore:</p>
|
||||||
|
<ol>
|
||||||
|
<li>Estrae il file ZIP in una cartella temporanea</li>
|
||||||
|
<li>Importa <code>database.sql</code> nel database MySQL configurato</li>
|
||||||
|
<li>Copia i file documenti in <code>storage/app/documenti/</code></li>
|
||||||
|
<li>Ripristina il file <code>.env</code> dal backup (preservando le credenziali DB del nuovo server)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Dopo l'Installazione</h5>
|
||||||
|
<ol>
|
||||||
|
<li><strong>Configura SSL</strong> con Let's Encrypt:
|
||||||
|
<pre><code>sudo apt install -y certbot python3-certbot-apache
|
||||||
|
sudo certbot --apache -d glastree.esempio.it</code></pre>
|
||||||
|
</li>
|
||||||
|
<li><strong>Accedi</strong> con le credenziali amministratore create durante l'installazione</li>
|
||||||
|
<li><strong>Configura l'applicazione</strong> in Impostazioni:
|
||||||
|
<ul>
|
||||||
|
<li>Nome applicazione e logo</li>
|
||||||
|
<li>Email (SMTP per invio, IMAP per ricezione)</li>
|
||||||
|
<li>Repository remoti (Google Drive, Nextcloud)</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>Crea utenti</strong> dal pannello Admin → Utenti</li>
|
||||||
|
<li><strong>Configura il cron</strong> per backup automatico e sync email:
|
||||||
|
<pre><code>sudo crontab -u www-data -e
|
||||||
|
# Inserisci:
|
||||||
|
* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1</code></pre>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h5 class="mt-4">Risoluzione Problemi</h5>
|
||||||
|
<div class="callout callout-danger">
|
||||||
|
<h6>Errore "500 | Server Error" dopo l'installazione</h6>
|
||||||
|
<p>Probabilmente mancano permessi sulle cartelle <code>storage</code> o <code>bootstrap/cache</code>:</p>
|
||||||
|
<pre><code>sudo chown -R www-data:www-data storage bootstrap/cache
|
||||||
|
sudo chmod -R 775 storage bootstrap/cache</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="callout callout-danger">
|
||||||
|
<h6>Errore "No application encryption key"</h6>
|
||||||
|
<p>APP_KEY non generata. Nella cartella dell'app:</p>
|
||||||
|
<pre><code>php artisan key:generate</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="callout callout-warning">
|
||||||
|
<h6>mod_rewrite non funziona</h6>
|
||||||
|
<p>Se le rotte restituiscono 404, verifica che Apache abbia AllowOverride:</p>
|
||||||
|
<pre><code>sudo a2enmod rewrite
|
||||||
|
# Nel VirtualHost: AllowOverride All
|
||||||
|
sudo systemctl reload apache2</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-pills .nav-link { border-radius: 0; }
|
||||||
|
.nav-pills .nav-link.active { background-color: #007bff; }
|
||||||
|
.callout { border-left: 3px solid; padding: 12px 16px; margin: 16px 0; background: #f8f9fa; }
|
||||||
|
.callout-info { border-color: #17a2b8; }
|
||||||
|
.callout-danger { border-color: #dc3545; }
|
||||||
|
.callout-warning { border-color: #ffc107; }
|
||||||
|
</style>
|
||||||
|
<?php $__env->stopSection(); ?>
|
||||||
|
|
||||||
|
<?php echo $__env->make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /var/www/html/glastree/resources/views/help/index.blade.php ENDPATH**/ ?>
|
||||||
@@ -23,6 +23,11 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="<?php echo e(route('help')); ?>" title="Guida">
|
||||||
|
<i class="fas fa-question-circle"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item dropdown">
|
<li class="nav-item dropdown">
|
||||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
<i class="far fa-bell"></i>
|
<i class="far fa-bell"></i>
|
||||||
@@ -81,6 +86,12 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
|||||||
<p>Dashboard</p>
|
<p>Dashboard</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="<?php echo e(route('help')); ?>" class="nav-link <?php echo e(request()->routeIs('help') ? 'active' : ''); ?>">
|
||||||
|
<i class="nav-icon fas fa-question-circle"></i>
|
||||||
|
<p>Guida</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php if(Auth::user()->canAccess('individui')): ?>
|
<?php if(Auth::user()->canAccess('individui')): ?>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="<?php echo e(route('individui.index')); ?>" class="nav-link">
|
<a href="<?php echo e(route('individui.index')); ?>" class="nav-link">
|
||||||
@@ -216,6 +227,12 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';
|
|||||||
<p>Admin</p>
|
<p>Admin</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/backup" class="nav-link <?php echo e(request()->is('admin/backup*') ? 'active' : ''); ?>">
|
||||||
|
<i class="nav-icon fas fa-hdd"></i>
|
||||||
|
<p>Backup</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Reference in New Issue
Block a user