diff --git a/.env.example b/.env.example index c0660ea1..4088f104 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,14 @@ -APP_NAME=Laravel +APP_NAME="Glastree" APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=http://localhost -APP_LOCALE=en +APP_LOCALE=it APP_FALLBACK_LOCALE=en -APP_FAKER_LOCALE=en_US +APP_FAKER_LOCALE=it_IT APP_MAINTENANCE_DRIVER=file -# APP_MAINTENANCE_STORE=database - -# PHP_CLI_SERVER_WORKERS=4 BCRYPT_ROUNDS=12 @@ -20,14 +17,14 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug -DB_CONNECTION=sqlite -# DB_HOST=127.0.0.1 -# DB_PORT=3306 -# DB_DATABASE=laravel -# DB_USERNAME=root -# DB_PASSWORD= +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=glastree +DB_USERNAME=glastree +DB_PASSWORD= -SESSION_DRIVER=database +SESSION_DRIVER=file SESSION_LIFETIME=120 SESSION_ENCRYPT=false SESSION_PATH=/ @@ -35,17 +32,9 @@ SESSION_DOMAIN=null BROADCAST_CONNECTION=log FILESYSTEM_DISK=local -QUEUE_CONNECTION=database +QUEUE_CONNECTION=file -CACHE_STORE=database -# CACHE_PREFIX= - -MEMCACHED_HOST=127.0.0.1 - -REDIS_CLIENT=phpredis -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null -REDIS_PORT=6379 +CACHE_STORE=file MAIL_MAILER=log MAIL_SCHEME=null @@ -56,10 +45,4 @@ MAIL_PASSWORD=null MAIL_FROM_ADDRESS="hello@example.com" 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}" diff --git a/.phpunit.result.cache b/.phpunit.result.cache index f66d8cce..78fe23d1 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -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}} \ No newline at end of file +{"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}} \ No newline at end of file diff --git a/MEMORY.md b/MEMORY.md index 6e282a05..52f3bf4d 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -6,12 +6,22 @@ Laravel 13 / PHP 8.4 project for parish management (Glastree). Full codebase exi --- ## Issues Open -1. **Drive-Segreteria-ADIM (#3)**: Root ha 0 elementi. Verificare root_folder_id nelle impostazioni repository. -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). +*Nessuna issue aperta.* --- +### 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 @@ -223,8 +233,7 @@ php artisan route:list --name=email --- ## Known Issues / TODO -1. SMTP authentication con Infomaniak - verificare credenziali corrette -2. Invio email richiede password SMTP corretta nel form impostazioni +*Nessuna issue nota.* --- @@ -1002,4 +1011,79 @@ php artisan route:list --name=email - `routes/web.php`: aggiunta route `storage-repositories.import` - `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. diff --git a/app/Console/Commands/BackupRunCommand.php b/app/Console/Commands/BackupRunCommand.php new file mode 100644 index 00000000..9b02339c --- /dev/null +++ b/app/Console/Commands/BackupRunCommand.php @@ -0,0 +1,55 @@ +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; + } +} diff --git a/app/Http/Controllers/Admin/BackupController.php b/app/Http/Controllers/Admin/BackupController.php new file mode 100644 index 00000000..7db84941 --- /dev/null +++ b/app/Http/Controllers/Admin/BackupController.php @@ -0,0 +1,118 @@ +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]); + } +} diff --git a/app/Http/Controllers/StorageRepositoryController.php b/app/Http/Controllers/StorageRepositoryController.php index 5248c4df..288187e7 100644 --- a/app/Http/Controllers/StorageRepositoryController.php +++ b/app/Http/Controllers/StorageRepositoryController.php @@ -242,23 +242,43 @@ class StorageRepositoryController extends Controller $normalizedPath = $this->repoService->normalizePath($path); - try { - $stream = $filesystem->readStream($normalizedPath); - } 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); + if ($storageRepository->tipo === 'google_drive') { + try { + $fileData = $this->repoService->readGoogleDriveFile($storageRepository, $normalizedPath, $basename); + } catch (\Exception $e) { + 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) { 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(); $storagePath = \App\Models\AppSetting::getDocumentiStoragePath(); $extension = strtolower(pathinfo($basename, PATHINFO_EXTENSION)); $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; $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); } - $mimeType = null; - try { $mimeType = $filesystem->mimeType($path); } catch (\Exception) {} - $fileSize = null; - try { $fileSize = $filesystem->fileSize($path); } catch (\Exception) {} - $visibilita = $validated['visibilita'] ?? 'pubblico'; $targetType = null; $targetId = null; diff --git a/app/Services/BackupService.php b/app/Services/BackupService.php new file mode 100644 index 00000000..f355faa3 --- /dev/null +++ b/app/Services/BackupService.php @@ -0,0 +1,298 @@ +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]; + } +} diff --git a/app/Services/StorageRepositoryService.php b/app/Services/StorageRepositoryService.php index f4dfa5c8..cc354315 100644 --- a/app/Services/StorageRepositoryService.php +++ b/app/Services/StorageRepositoryService.php @@ -45,6 +45,11 @@ class StorageRepositoryService 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.']; } + + $apiError = $this->checkGoogleDriveApiStatus($config); + if ($apiError) { + return ['success' => false, 'message' => $apiError]; + } } $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 { try { @@ -198,4 +239,126 @@ class StorageRepositoryService 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; + } } diff --git a/database/migrations/2026_06_01_100155_add_backup_settings_to_app_settings.php b/database/migrations/2026_06_01_100155_add_backup_settings_to_app_settings.php new file mode 100644 index 00000000..634d1010 --- /dev/null +++ b/database/migrations/2026_06_01_100155_add_backup_settings_to_app_settings.php @@ -0,0 +1,30 @@ +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']); + }); + } +}; diff --git a/installer/assets/style.css b/installer/assets/style.css new file mode 100644 index 00000000..261faf2d --- /dev/null +++ b/installer/assets/style.css @@ -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; +} diff --git a/installer/functions.php b/installer/functions.php new file mode 100644 index 00000000..7abd31af --- /dev/null +++ b/installer/functions.php @@ -0,0 +1,533 @@ + '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}" : ''; + ?> + + + + + + <?= e($title) ?> — Glastree Installer + + + +
+
+
+

Glastree

+

Installazione Wizard

+
+
+
+ +
Passo di 6
+ +
+
+ +
+
+
+ +
+ + + +
+ Errori: + +
+ +
+ + + + +
+ +

Benvenuto nell'installazione di Glastree

+

Questo wizard ti guidera nella configurazione dell'applicazione su questo server.

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

Verifica Requisiti di Sistema

+

Il sistema controlla che il server soddisfi i requisiti minimi per eseguire Glastree.

+ + + +
+ +
+ + + + + + + + + + + + +
+ +
+ + +
+ + Requisiti critici non soddisfatti. Risolvi gli errori sopra e ricarica la pagina. +
+ +
+ + Alcuni requisiti non critici non sono soddisfatti. Puoi proseguire, ma alcune funzionalita potrebbero essere limitate. +
+ + +
+
+ ← Indietro + + + +
+
+ $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'); + ?> +

Configurazione Database

+

Configura la connessione al database MySQL.

+ + + +
+ + +
+
+ + +
+
+ + +
+
+ + + L'indirizzo pubblico con cui si accedera a Glastree +
+ +
+

Modalita A: Crea Nuovo Database

+ +
+ + +
+
+ + +
+ + +
+
Nuovo Database da Creare
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+

Modalita B: Usa Database Esistente

+ +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ ← Indietro +
+ ['--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'); + ?> +

+ + + +
+ +
+ +
+ +
+ + + + +
+ Installazione completata. Procedi al passo successivo. +
+
+ Avanti → +
+ + + +
+

Clicca "Installa" per eseguire migration, seed e generare la chiave dell'applicazione.

+
+ ← Indietro + +
+
+ +
+
+ + +
+
+

oppure specifica il percorso sul server:

+
+ + +
+
+ ← Indietro + +
+
+ + + +

+ + + +
+ + + +
+ + + Amministratore creato con successo! + + Utenti configurati. + +
+
+ Avanti → +
+ + + +
+ Utenti trovati nel database ripristinato: + +

Puoi proseguire senza creare nuovi utenti.

+
+
+ +
+
+

oppure crea un nuovo amministratore:

+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ ← Indietro + +
+
+ + [], + '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'); + ?> +

Finalizzazione

+

Completa l'installazione generando la cache e rimuovendo i file di setup.

+ + + +
+
+ ← Indietro + +
+
+ +

Installazione Completata!

+ + + + +
+ +
+ ' : '' ?> + +
+ +
+ + + +
+ + Glastree e stato installato con successo! +

La cartella installer/ e stata eliminata per sicurezza.

+
+ +
+

Prossimi passi:

+
    +
  1. Configura SSL con Let's Encrypt per connessioni sicure
  2. +
  3. Accedi con le credenziali amministratore
  4. +
  5. Vai su Impostazioni per configurare: +
      +
    • Nome applicazione e logo
    • +
    • Email (SMTP e IMAP)
    • +
    • Repository remoti (Google Drive, Nextcloud)
    • +
    +
  6. +
  7. Vai su Admin per creare altri utenti
  8. +
  9. Configura il cron per backup automatico: + * * * * * cd && php artisan schedule:run >> /dev/null 2>&1 +
  10. +
+
+ +
+ + Vai al Login → + +
+ + Home + + +@endsection + +@section('content') +
+
+ {{-- Lista backup --}} +
+
+

Backup Disponibili

+
+
+ @csrf + +
+
+
+
+ @if(count($backups) > 0) +
+ + + + + + + + + + + + @foreach($backups as $backup) + + + + + + + + @endforeach + +
FileDataDimensioneContenutoAzioni
{{ $backup['filename'] }}{{ date('d/m/Y H:i', $backup['last_modified']) }}{{ $backup['size_formatted'] }} + @if($backup['manifest']) + + DB + + @if(isset($backup['manifest']['db_name'])) + {{ $backup['manifest']['db_name'] }} + @endif + @else + N/A + @endif + + + + +
+ @csrf + @method('DELETE') + +
+
+
+ @else +
+ +

Nessun backup disponibile.

+

Clicca "Esegui Backup Ora" per creare il primo backup.

+
+ @endif +
+
+ + {{-- Info sul backup --}} +
+
+

Cosa Include il Backup

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponenteInclusoDettagli
Database (MySQL)SempreStruttura, dati, stored procedure, eventi, trigger
File caricati + @if($config['backup_include_files']) + + @else + No + @endif + Documenti, avatar, gallerie — dalla cartella storage
Configurazione (.env) + @if($config['backup_include_env']) + + @else + No + @endif + APP_KEY, DB credentials, SMTP, OAuth token
MetadatiSempreManifest JSON con versione app, PHP, Laravel
+
+
+
+ +
+ {{-- Configurazione --}} +
+
+

Configurazione Backup

+
+
+ @csrf +
+
+ +
+
+ storage/app/ +
+ +
+ Cartella dove salvare i backup (relativa a storage/app/) +
+
+ + + I backup più vecchi vengono eliminati automaticamente +
+
+
+ + +
+
+ + +
+
+
+ +
+
+ + {{-- Backup automatico --}} +
+
+

Backup Automatico

+
+ @php $autoEnabled = \App\Models\AppSetting::getSetting('backup_auto_enabled', false) @endphp +
+ @csrf + +
+
+
+
+ @csrf +
+
+ + +
+
+ + + Ora del giorno (formato 24h). Consigliato: 03:00 (notte) +
+
+ +
+
+ + {{-- Migrazione --}} +
+
+

Migrazione Server

+
+
+

Scarica il backup, poi esegui lo script di setup sul nuovo server per una migrazione completa.

+ + Guida alla Migrazione + +
+
+
+
+ +{{-- Alert notifica cron --}} +
+ + Importante: Per il backup automatico, assicurati che il cron di Laravel sia configurato sul server: + * * * * * cd {{ base_path() }} && php artisan schedule:run >> /dev/null 2>&1 +
+@endsection diff --git a/resources/views/help/index.blade.php b/resources/views/help/index.blade.php new file mode 100644 index 00000000..24c0e5d6 --- /dev/null +++ b/resources/views/help/index.blade.php @@ -0,0 +1,940 @@ +@extends('layouts.adminlte') + +@section('title', 'Guida - ' . $appName) +@section('page_title', 'Guida alla Configurazione') +@section('breadcrumbs') + + +@endsection + +@section('content') +
+
+ +
+ +
+
+ {{-- WebDAV / Nextcloud --}} +
+
+
+

WebDAV / Nextcloud

+
+
+
Cos'è
+

WebDAV è un protocollo che permette di accedere a file su server remoti. Nextcloud lo utilizza per la sincronizzazione dei file.

+ +
Configurazione
+

Vai in Impostazioni → Repository e crea un nuovo repository di tipo WebDAV.

+ +
+
Campi obbligatori
+
+
Nome
+
Un nome descrittivo (es. "Nextcloud Lavoro")
+
Base URI
+
L'URL del tuo server WebDAV. +
Nextcloud: https://tuodominio.it/remote.php/dav/files/utente/ +
Altri: https://server.webdav.it/
+
Root
+
Percorso base all'interno del server (es. /Documenti/). Lascia / per usare la root.
+
Username
+
Il tuo nome utente
+
Password
+
La password dell'account
+
Auth Type
+
Basic per la maggior parte dei server (Nextcloud, ownCloud).
+
+
+ +
⚠️ Nome utente con @ (chiocciola)
+

Se il tuo nome utente contiene il carattere @ (es. mario.rossi@azienda.com), segui queste regole:

+
    +
  • Nel campo "Base URI": l'indirizzo email deve essere URL-encoded: +
    https://server.it/remote.php/dav/files/mario.rossi%40azienda.com/ +
    (il carattere @ diventa %40)
  • +
  • Nel campo "Username": inserisci l'indirizzo email normale con la @ vera.
  • +
+
+ + Il sistema gestisce automaticamente la differenza tra l'URI (che richiede %40) e l'autenticazione (che usa la @ reale). +
+ +
Test connessione
+

Dopo aver salvato, usa il pulsante Test nella tabella dei repository per verificare che la connessione funzioni.

+ +
Importare file
+

Apri la sezione Documenti, clicca sul pulsante del repository nella toolbar per sfogliare i file remoti, poi clicca Salva tra i documenti su un file per importarlo come documento locale.

+
+
+
+ + {{-- Google Drive --}} +
+
+
+

Google Drive

+
+
+
Cos'è
+

Integrazione con Google Drive per sfogliare e importare file direttamente dal tuo spazio Google Drive.

+ +
Requisiti
+
    +
  1. Un account Google / Google Workspace
  2. +
  3. Un progetto sulla Google Cloud Console con l'API Google Drive abilitata
  4. +
  5. Credenziali OAuth 2.0 (Client ID e Client Secret)
  6. +
+ +
Passo 1: Creare il progetto Google Cloud
+
    +
  1. Vai su Google Cloud Console
  2. +
  3. Crea un nuovo progetto o selezionane uno esistente
  4. +
  5. Vai a API e servizi → Libreria
  6. +
  7. Cerca "Google Drive API" e abilitala
  8. +
  9. Vai a API e servizi → Credenziali
  10. +
  11. Clicca "Crea credenziali → ID client OAuth"
  12. +
  13. Tipo applicazione: "Applicazione Web"
  14. +
  15. URI di reindirizzamento autorizzato: aggiungi {{ url('/auth/google-drive/callback') }}
  16. +
  17. Completa la creazione e copia Client ID e Client Secret
  18. +
+ +
Passo 2: Configurazione nell'app
+
    +
  1. Vai in Impostazioni → Repository
  2. +
  3. Clicca "Nuovo Repository" e seleziona tipo Google Drive
  4. +
  5. Inserisci un nome descrittivo (es. "Drive Lavoro")
  6. +
  7. Incolla Client ID e Client Secret
  8. +
  9. Folder ID (opzionale): se vuoi limitare l'accesso a una cartella specifica, inserisci il suo ID (lo trovi nell'URL quando apri la cartella in Google Drive: https://drive.google.com/drive/folders/ID_QUI)
  10. +
  11. Clicca "Autorizza Google Drive" — verrai reindirizzato a Google per concedere i permessi
  12. +
  13. Dopo l'autorizzazione, il refresh token verrà salvato automaticamente
  14. +
+ +
⚠️ Google Drive API non abilitata
+

Se vedi l'errore "Google Drive API non abilitata" nel test connessione, significa che l'API Drive non è stata attivata nel tuo progetto Google Cloud.

+
+
Soluzione
+

Vai su + https://console.developers.google.com/apis/api/drive.googleapis.com/overview +

+

Seleziona il progetto corretto dall'header in alto, clicca "Enable" e attendi qualche minuto prima di riprovare.

+
+ +
Importare file
+

Stessa procedura di WebDAV: apri Documenti, clicca sul repository Drive, sfoglia i file e usa Salva tra i documenti.

+ +
⚠️ File Google (Documenti, Fogli, Presentazioni)
+

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.

+

Il sistema li converte automaticamente in formato Office Open XML durante l'import:

+ + + + + + + + + +
File GoogleFormato convertito
Documento Google.docx (Word)
Foglio Google.xlsx (Excel)
Presentazione Google.pptx (PowerPoint)
Disegno Google.png (Immagine)
Altri (Moduli, Script).pdf (PDF)
+
+ + Il nome del file viene automaticamente esteso con l'estensione corretta (es. "Relazione" → "Relazione.docx"). +
+
+
+
+ + {{-- Tipologie e Dati --}} +
+
+
+

Tipologie e Gestione Dati

+
+
+
Tipologie Documenti
+

Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).

+

Si gestiscono in Impostazioni → Tipologie Documenti. Puoi aggiungere, rinominare, riordinare ed eliminare tipologie (tranne quelle con documenti associati).

+ +
Tipologie Individui
+

Classificano gli individui (es. socio, volontario, dipendente). Si gestiscono in Impostazioni → Tipologie.

+ +
Tipologie Eventi
+

Classificano gli eventi (es. riunione, assemblea, formazione). Si gestiscono in Impostazioni → Tipologie Eventi.

+ +
Ruoli Individui
+

Definiscono il ruolo o la carica di un individuo (es. presidente, segretario, tesoriere). Si gestiscono in Impostazioni → Ruoli.

+ +
Visibilità Documenti
+

Ogni documento può avere diversi livelli di visibilità:

+
    +
  • Pubblico — visibile a tutti gli utenti
  • +
  • Individuo — visibile solo a uno specifico individuo
  • +
  • Gruppo — visibile solo ai membri di un gruppo
  • +
  • Evento — visibile solo ai partecipanti di un evento
  • +
  • Mailing — visibile solo ai membri di una mailing list
  • +
+
+
+
+ + {{-- Email / SMTP --}} +
+
+
+

Email / SMTP

+
+
+
Configurazione Email
+

Vai in Impostazioni → Email per configurare la posta elettronica.

+ +
Server IMAP (Ricezione)
+

Usato per ricevere e leggere le email direttamente nell'app.

+
+
Campi consigliati per provider comuni
+ + + + + + + + +
ProviderHostPortaCrittografia
Gmailimap.gmail.com993SSL
Outlook/Office 365outlook.office365.com993SSL
Liberoimapmail.libero.it993SSL
Arubaimap.aruba.it993SSL
+
+ +
Server SMTP (Invio)
+

Usato per inviare email dall'app. Supporta account multipli (mittenti).

+
+
Campi consigliati per provider comuni
+ + + + + + + + +
ProviderHostPortaCrittografia
Gmailsmtp.gmail.com587TLS
Outlook/Office 365smtp.office365.com587TLS
Liberosmtp.libero.it465SSL
Arubasmtp.aruba.it465SSL
+
+ +
Gmail / Google Workspace
+
+ + Gmail richiede una Password per l'app (non la password normale) se usi la verifica in due passaggi. Puoi generarla su + https://myaccount.google.com/apppasswords. +
+ +
Mailing
+

Le Mailing List permettono di inviare email a gruppi predefiniti di destinatari.

+
    +
  • Crea una mailing list in Mailing List → Nuova
  • +
  • Puoi popolare la lista da individui, gruppi o manualmente
  • +
  • Invia email alla lista da Email → Nuova Email, selezionando la mailing list come destinatario
  • +
+ +
Test connessione
+

Dopo aver configurato i server, usa il pulsante Test Connessione per verificare che IMAP e SMTP funzionino correttamente.

+
+
+
+
+ + {{-- Backup e Migrazione --}} +
+
+
+

Backup e Migrazione Server

+
+
+
Cos'è
+

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.

+ +
Cosa include il backup
+ + + + + + + + +
ComponenteInclusoDettagli
Database (MySQL)SempreStruttura, dati, stored procedure, eventi, trigger
File caricatiOpzionaleDocumenti, avatar, gallerie
Configurazione .envOpzionaleAPP_KEY, credenziali DB, SMTP, token OAuth
MetadatiSempreVersione app, PHP, Laravel
+ +
Come eseguire un backup
+
+
+
+
Via Interfaccia Web
+
    +
  1. Vai su Admin → Backup
  2. +
  3. Verifica le impostazioni (includi files/.env)
  4. +
  5. Clicca Esegui Backup Ora
  6. +
  7. Scarica il file ZIP generato
  8. +
+
+
+
+
+
Via Comando
+
php artisan backup:run
+

Il file viene creato in storage/app/backups/

+
+
+
+ +
Backup automatico
+

Puoi abilitare il backup automatico dalla pagina Admin → Backup:

+
    +
  • Frequenza: giornaliero, settimanale o mensile
  • +
  • Ora: consigliata 03:00 (notte)
  • +
  • Requisito: il cron di Laravel deve essere configurato sul server
  • +
+
+ + Per il backup automatico, aggiungi questo cron: +
* * * * * cd {{ base_path() }} && php artisan schedule:run >> /dev/null 2>&1
+
+ +
+ +

Procedura di Migrazione Completa

+

Segui questa procedura per migrare Glastree su un nuovo server LAMP (Linux, Apache, MySQL, PHP).

+ +
+ + ATTENZIONE: APP_KEY +

L'APP_KEY cripta i dati sensibili (password email SMTP, token OAuth Google Drive, ecc.).

+
    +
  • Se hai il .env originale dal backup: NON rigenerare APP_KEY — mantieni i dati criptati accessibili.
  • +
  • Se hai perso il .env: DEVI rigenerare APP_KEY, ma perderai l'accesso ai dati criptati esistenti. Dovrai riconfigurare SMTP e OAuth.
  • +
+
+ +
Passo 1: Preparazione del server
+
sudo apt update && sudo apt upgrade -y
+ +
Passo 2: Installa LAMP + PHP 8.4
+
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
+ +
Passo 3: Clona l'applicazione
+
cd /var/www
+git clone <URL_REPOSITORY> glastree
+cd glastree
+composer install --no-dev --optimize-autoloader
+npm ci && npm run build
+ +
Passo 4: Carica il backup
+

Copia il file backup.zip sul nuovo server e estrailo:

+
mkdir -p /tmp/restore
+unzip backup.zip -d /tmp/restore/
+ls -la /tmp/restore/
+# Dovresti vedere: database.sql  .env  files/  manifest.json
+ +
Passo 5: Crea database e importa
+
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
+ +
Passo 6: Configura .env
+
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
+ +
Passo 7: Ripristina file e permessi
+
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
+ +
Passo 8: Cache
+
php artisan optimize:clear
+php artisan config:cache
+php artisan route:cache
+php artisan view:cache
+ +
Passo 9: Configura Apache
+
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
+ +
Passo 10: SSL Let's Encrypt
+
sudo apt install -y certbot python3-certbot-apache
+sudo certbot --apache -d glastree.esempio.it
+ +
Passo 11: Cron per backup automatico
+
sudo crontab -u www-data -e
+# Inserisci:
+* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1
+ +
Passo 12: Verifica finale
+
    +
  1. Apri https://glastree.esempio.it nel browser
  2. +
  3. Fai login con le credenziali esistenti
  4. +
  5. Verifica: Dashboard, Documenti, Impostazioni, Email
  6. +
  7. Se hai rigenerato APP_KEY: riconfigura SMTP e OAuth Google Drive
  8. +
+ +
+ +
PHP Web Installer
+

L'applicazione include un wizard di installazione web nella cartella installer/. Questo wizard gestisce sia installazioni fresh che ripristino da backup.

+

Vedi la sezione Installazione per la guida completa.

+
+
+
+ + {{-- Docker --}} +
+
+
+

Docker

+
+
+
Creare un'immagine Docker con dati di base
+

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:

+ +
+
+
+
SQLite (Leggero)
+

Database embedded, nessun servizio aggiuntivo. Ideale per demo, test, sviluppo locale o ambienti con poca concorrenza.

+
+
+
+
+
MySQL (Standard)
+

Database separato in un container MySQL. Configurazione standard, ideale per produzione, staging e ambienti con piu utenti.

+
+
+
+ +
1. Dockerfile (comune a entrambe le modalita)
+

Crea un file Dockerfile nella root del progetto:

+
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"]
+ +
2. Entrypoint Script (comune)
+

Crea docker-entrypoint.sh. Lo script rileva automaticamente la configurazione in base alle variabili d'ambiente:

+
#!/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 "$@"
+
chmod +x docker-entrypoint.sh
+ +
3. Seed Base Dati (comune)
+

Per includere tipologie documenti, tipologie eventi e ruoli, crea un seeder dedicato:

+
php artisan make:seeder DockerBaseDataSeeder
+

Contenuto di database/seeders/DockerBaseDataSeeder.php:

+
<?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]
+            );
+        }
+    }
+}
+

Registralo in database/seeders/DatabaseSeeder.php:

+
$this->call([
+    // ... altri seeder ...
+    DockerBaseDataSeeder::class,
+]);
+ +
4. Docker Compose
+ +
Opzione A — SQLite (leggero)
+
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:
+ +
Opzione B — MySQL (standard)
+
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:
+ +
5. Build e avvio
+
# 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
+ +
6. Primo accesso
+

Al primo avvio, l'entrypoint esegue automaticamente migration e seed. Troverai gia popolati:

+
    +
  • Tipologie Documenti: documento, avatar, galleria, statuto, programma, locandina, altro
  • +
  • Tipologie Eventi: catechesi, liturgia, animazione, formazione, incontro, riunione, festa, altro
  • +
  • Ruoli: presidente, vicepresidente, segretario, tesoriere, consigliere, volontario, socio, altro
  • +
+

Dovrai creare il primo utente amministratore via CLI:

+
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!';
+"
+ +
+
Note importanti
+
    +
  • Persistenza: i volumi Docker preservano storage e database tra i riavvii
  • +
  • APP_KEY: viene generata al primo avvio — se elimini i volumi, la perdi e i dati criptati diventano inaccessibili
  • +
  • MySQL in produzione: imposta password sicure per MYSQL_PASSWORD e MYSQL_ROOT_PASSWORD
  • +
  • File compose separati: puoi mantenere entrambi i file (docker-compose.yml per SQLite, docker-compose.mysql.yml per MySQL) e scegliere con -f
  • +
+
+
+
+
+ + {{-- Installazione --}} +
+
+
+

Guida all'Installazione

+
+
+
Panoramica
+

Glastree include un wizard di installazione web che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:

+
+
+
+
Fresh Install
+

Installa l'applicazione da zero su un nuovo server: crea il database, esegue le migration, popola i dati di base e crea l'amministratore.

+
+
+
+
+
Restore da Backup
+

Ripristina un'installazione esistente da un file ZIP di backup: importa il database, ripristina i documenti e la configurazione.

+
+
+
+ +
Requisiti del Server
+ + + + + + + + + +
ComponenteVersione MinimaNote
Web ServerApache 2.4+Con mod_rewrite abilitato
PHP8.2+ (consigliato 8.4)Estensioni: pdo_mysql, mbstring, xml, curl, zip, gd, fileinfo
MySQL / MariaDBMySQL 8.0+ / MariaDB 10.5+Supporto utf8mb4
Composer2.xPer gestione dipendenze PHP
Node.js / npmNode 20+ / npm 10+Per compilazione asset frontend (opzionale se precompilati)
+ +
Installazione Passo-Passo
+ +
Passo 1: Preparare il server LAMP
+
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
+ +
Passo 2: Ottenere l'applicazione
+
cd /var/www
+git clone <URL_REPOSITORY> glastree
+cd glastree
+composer install --no-dev --optimize-autoloader
+npm ci && npm run build
+
+ + Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN. +
+ +
Passo 3: Permessi cartelle
+
sudo chown -R www-data:www-data storage bootstrap/cache
+sudo chmod -R 775 storage bootstrap/cache
+ +
Passo 4: Configurare Apache
+
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
+ +
Passo 5: Avviare il wizard di installazione
+

Apri il browser e visita l'indirizzo del tuo server:

+
http://glastree.esempio.it/installer/
+ +

Il wizard ti guidera attraverso 6 passi:

+ + + + + + + + + + + +
PassoDescrizione
1. BenvenutoScegli Fresh Install o Restore da Backup
2. RequisitiVerifica PHP, estensioni e permessi
3. DatabaseConfigura MySQL (crea nuovo o usa esistente)
4. InstallazioneFresh: migration e seed. Backup: upload ZIP
5. AmministratoreFresh: crea super-admin. Backup: verifica utenti
6. FinalizzazioneCache, storage link, auto-eliminazione installer
+ +
+ + Attenzione: Al termine dell'installazione, la cartella installer/ viene automaticamente eliminata. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella. +
+ +
Modalita Fresh Install
+

Seleziona questa modalita quando installi Glastree per la prima volta su un server.

+

Cosa succede durante l'installazione:

+
    +
  1. Genera APP_KEY per la crittografia
  2. +
  3. Esegue tutte le migration per creare le tabelle del database
  4. +
  5. Popola i dati di base (diocesi, comuni, ruoli, preset)
  6. +
  7. Crea il collegamento storage → public/storage
  8. +
  9. Crea l'utente amministratore con i permessi completi
  10. +
+ +
Modalita Restore da Backup
+

Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.

+

Cosa serve:

+
    +
  • Un file ZIP generato dalla pagina Admin → Backup del server originale
  • +
  • Il wizard accetta upload diretto del file o un percorso sul server
  • +
+

Cosa succede durante il restore:

+
    +
  1. Estrae il file ZIP in una cartella temporanea
  2. +
  3. Importa database.sql nel database MySQL configurato
  4. +
  5. Copia i file documenti in storage/app/documenti/
  6. +
  7. Ripristina il file .env dal backup (preservando le credenziali DB del nuovo server)
  8. +
+ +
Dopo l'Installazione
+
    +
  1. Configura SSL con Let's Encrypt: +
    sudo apt install -y certbot python3-certbot-apache
    +sudo certbot --apache -d glastree.esempio.it
    +
  2. +
  3. Accedi con le credenziali amministratore create durante l'installazione
  4. +
  5. Configura l'applicazione in Impostazioni: +
      +
    • Nome applicazione e logo
    • +
    • Email (SMTP per invio, IMAP per ricezione)
    • +
    • Repository remoti (Google Drive, Nextcloud)
    • +
    +
  6. +
  7. Crea utenti dal pannello Admin → Utenti
  8. +
  9. Configura il cron per backup automatico e sync email: +
    sudo crontab -u www-data -e
    +# Inserisci:
    +* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1
    +
  10. +
+ +
Risoluzione Problemi
+
+
Errore "500 | Server Error" dopo l'installazione
+

Probabilmente mancano permessi sulle cartelle storage o bootstrap/cache:

+
sudo chown -R www-data:www-data storage bootstrap/cache
+sudo chmod -R 775 storage bootstrap/cache
+
+
+
Errore "No application encryption key"
+

APP_KEY non generata. Nella cartella dell'app:

+
php artisan key:generate
+
+
+
mod_rewrite non funziona
+

Se le rotte restituiscono 404, verifica che Apache abbia AllowOverride:

+
sudo a2enmod rewrite
+# Nel VirtualHost: AllowOverride All
+sudo systemctl reload apache2
+
+
+
+
+
+
+ + + +@endsection diff --git a/resources/views/layouts/adminlte.blade.php b/resources/views/layouts/adminlte.blade.php index 2e5a6403..71a273fe 100644 --- a/resources/views/layouts/adminlte.blade.php +++ b/resources/views/layouts/adminlte.blade.php @@ -23,6 +23,11 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree'; diff --git a/routes/console.php b/routes/console.php index eb899017..769ee418 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,6 +1,8 @@ $e->getMessage()]); } })->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(); diff --git a/routes/web.php b/routes/web.php index 728461f7..73ba8c86 100644 --- a/routes/web.php +++ b/routes/web.php @@ -34,6 +34,7 @@ Route::get('/', function () { return redirect()->route('login'); }); + // Auth Route::middleware('guest')->group(function () { Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login'); @@ -54,6 +55,11 @@ Route::middleware('auth')->group(function () { // Dashboard 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 // Individui specific routes (MUST be before resource route) 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::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'); }); \ No newline at end of file diff --git a/storage/framework/views/8e3d156f62cc75d4f1c4c5338e84e21d.php b/storage/framework/views/8e3d156f62cc75d4f1c4c5338e84e21d.php new file mode 100755 index 00000000..be30c078 --- /dev/null +++ b/storage/framework/views/8e3d156f62cc75d4f1c4c5338e84e21d.php @@ -0,0 +1,247 @@ +startSection('title', 'Backup - ' . $appName); ?> +startSection('page_title', 'Backup e Migrazione'); ?> +startSection('breadcrumbs'); ?> + + + +stopSection(); ?> + +startSection('content'); ?> +
+
+ +
+
+

Backup Disponibili

+
+
+ + +
+
+
+
+ 0): ?> +
+ + + + + + + + + + + + addLoop($__currentLoopData); foreach($__currentLoopData as $backup): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> + + + + + + + + popLoop(); $loop = $__env->getLastLoop(); ?> + +
FileDataDimensioneContenutoAzioni
+ + + DB + + + + + + N/A + + + + + +
+ + + +
+
+
+ +
+ +

Nessun backup disponibile.

+

Clicca "Esegui Backup Ora" per creare il primo backup.

+
+ +
+
+ + +
+
+

Cosa Include il Backup

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ComponenteInclusoDettagli
Database (MySQL)SempreStruttura, dati, stored procedure, eventi, trigger
File caricati + + + + No + + Documenti, avatar, gallerie — dalla cartella storage
Configurazione (.env) + + + + No + + APP_KEY, DB credentials, SMTP, OAuth token
MetadatiSempreManifest JSON con versione app, PHP, Laravel
+
+
+
+ +
+ +
+
+

Configurazione Backup

+
+
+ +
+
+ +
+
+ storage/app/ +
+ +
+ Cartella dove salvare i backup (relativa a storage/app/) +
+
+ + + I backup più vecchi vengono eliminati automaticamente +
+
+
+ > + +
+
+ > + +
+
+
+ +
+
+ + +
+
+

Backup Automatico

+
+ +
+ + +
+
+
+
+ +
+
+ + +
+
+ + + Ora del giorno (formato 24h). Consigliato: 03:00 (notte) +
+
+ +
+
+ + +
+
+

Migrazione Server

+
+
+

Scarica il backup, poi esegui lo script di setup sul nuovo server per una migrazione completa.

+ + Guida alla Migrazione + +
+
+
+
+ + +
+ + Importante: Per il backup automatico, assicurati che il cron di Laravel sia configurato sul server: + * * * * * cd && php artisan schedule:run >> /dev/null 2>&1 +
+stopSection(); ?> + +make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/a54cb906b71ecddfe8edf55b3671046a.php b/storage/framework/views/a54cb906b71ecddfe8edf55b3671046a.php new file mode 100755 index 00000000..f7eb35c3 --- /dev/null +++ b/storage/framework/views/a54cb906b71ecddfe8edf55b3671046a.php @@ -0,0 +1,940 @@ +startSection('title', 'Guida - ' . $appName); ?> +startSection('page_title', 'Guida alla Configurazione'); ?> +startSection('breadcrumbs'); ?> + + +stopSection(); ?> + +startSection('content'); ?> +
+
+ +
+ +
+
+ +
+
+
+

WebDAV / Nextcloud

+
+
+
Cos'è
+

WebDAV è un protocollo che permette di accedere a file su server remoti. Nextcloud lo utilizza per la sincronizzazione dei file.

+ +
Configurazione
+

Vai in Impostazioni → Repository e crea un nuovo repository di tipo WebDAV.

+ +
+
Campi obbligatori
+
+
Nome
+
Un nome descrittivo (es. "Nextcloud Lavoro")
+
Base URI
+
L'URL del tuo server WebDAV. +
Nextcloud: https://tuodominio.it/remote.php/dav/files/utente/ +
Altri: https://server.webdav.it/
+
Root
+
Percorso base all'interno del server (es. /Documenti/). Lascia / per usare la root.
+
Username
+
Il tuo nome utente
+
Password
+
La password dell'account
+
Auth Type
+
Basic per la maggior parte dei server (Nextcloud, ownCloud).
+
+
+ +
⚠️ Nome utente con @ (chiocciola)
+

Se il tuo nome utente contiene il carattere @ (es. mario.rossi@azienda.com), segui queste regole:

+
    +
  • Nel campo "Base URI": l'indirizzo email deve essere URL-encoded: +
    https://server.it/remote.php/dav/files/mario.rossi%40azienda.com/ +
    (il carattere @ diventa %40)
  • +
  • Nel campo "Username": inserisci l'indirizzo email normale con la @ vera.
  • +
+
+ + Il sistema gestisce automaticamente la differenza tra l'URI (che richiede %40) e l'autenticazione (che usa la @ reale). +
+ +
Test connessione
+

Dopo aver salvato, usa il pulsante Test nella tabella dei repository per verificare che la connessione funzioni.

+ +
Importare file
+

Apri la sezione Documenti, clicca sul pulsante del repository nella toolbar per sfogliare i file remoti, poi clicca Salva tra i documenti su un file per importarlo come documento locale.

+
+
+
+ + +
+
+
+

Google Drive

+
+
+
Cos'è
+

Integrazione con Google Drive per sfogliare e importare file direttamente dal tuo spazio Google Drive.

+ +
Requisiti
+
    +
  1. Un account Google / Google Workspace
  2. +
  3. Un progetto sulla Google Cloud Console con l'API Google Drive abilitata
  4. +
  5. Credenziali OAuth 2.0 (Client ID e Client Secret)
  6. +
+ +
Passo 1: Creare il progetto Google Cloud
+
    +
  1. Vai su Google Cloud Console
  2. +
  3. Crea un nuovo progetto o selezionane uno esistente
  4. +
  5. Vai a API e servizi → Libreria
  6. +
  7. Cerca "Google Drive API" e abilitala
  8. +
  9. Vai a API e servizi → Credenziali
  10. +
  11. Clicca "Crea credenziali → ID client OAuth"
  12. +
  13. Tipo applicazione: "Applicazione Web"
  14. +
  15. URI di reindirizzamento autorizzato: aggiungi
  16. +
  17. Completa la creazione e copia Client ID e Client Secret
  18. +
+ +
Passo 2: Configurazione nell'app
+
    +
  1. Vai in Impostazioni → Repository
  2. +
  3. Clicca "Nuovo Repository" e seleziona tipo Google Drive
  4. +
  5. Inserisci un nome descrittivo (es. "Drive Lavoro")
  6. +
  7. Incolla Client ID e Client Secret
  8. +
  9. Folder ID (opzionale): se vuoi limitare l'accesso a una cartella specifica, inserisci il suo ID (lo trovi nell'URL quando apri la cartella in Google Drive: https://drive.google.com/drive/folders/ID_QUI)
  10. +
  11. Clicca "Autorizza Google Drive" — verrai reindirizzato a Google per concedere i permessi
  12. +
  13. Dopo l'autorizzazione, il refresh token verrà salvato automaticamente
  14. +
+ +
⚠️ Google Drive API non abilitata
+

Se vedi l'errore "Google Drive API non abilitata" nel test connessione, significa che l'API Drive non è stata attivata nel tuo progetto Google Cloud.

+
+
Soluzione
+

Vai su + https://console.developers.google.com/apis/api/drive.googleapis.com/overview +

+

Seleziona il progetto corretto dall'header in alto, clicca "Enable" e attendi qualche minuto prima di riprovare.

+
+ +
Importare file
+

Stessa procedura di WebDAV: apri Documenti, clicca sul repository Drive, sfoglia i file e usa Salva tra i documenti.

+ +
⚠️ File Google (Documenti, Fogli, Presentazioni)
+

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.

+

Il sistema li converte automaticamente in formato Office Open XML durante l'import:

+ + + + + + + + + +
File GoogleFormato convertito
Documento Google.docx (Word)
Foglio Google.xlsx (Excel)
Presentazione Google.pptx (PowerPoint)
Disegno Google.png (Immagine)
Altri (Moduli, Script).pdf (PDF)
+
+ + Il nome del file viene automaticamente esteso con l'estensione corretta (es. "Relazione" → "Relazione.docx"). +
+
+
+
+ + +
+
+
+

Tipologie e Gestione Dati

+
+
+
Tipologie Documenti
+

Le tipologie classificano i documenti (avatar, galleria, documento, statuto, altro).

+

Si gestiscono in Impostazioni → Tipologie Documenti. Puoi aggiungere, rinominare, riordinare ed eliminare tipologie (tranne quelle con documenti associati).

+ +
Tipologie Individui
+

Classificano gli individui (es. socio, volontario, dipendente). Si gestiscono in Impostazioni → Tipologie.

+ +
Tipologie Eventi
+

Classificano gli eventi (es. riunione, assemblea, formazione). Si gestiscono in Impostazioni → Tipologie Eventi.

+ +
Ruoli Individui
+

Definiscono il ruolo o la carica di un individuo (es. presidente, segretario, tesoriere). Si gestiscono in Impostazioni → Ruoli.

+ +
Visibilità Documenti
+

Ogni documento può avere diversi livelli di visibilità:

+
    +
  • Pubblico — visibile a tutti gli utenti
  • +
  • Individuo — visibile solo a uno specifico individuo
  • +
  • Gruppo — visibile solo ai membri di un gruppo
  • +
  • Evento — visibile solo ai partecipanti di un evento
  • +
  • Mailing — visibile solo ai membri di una mailing list
  • +
+
+
+
+ + +
+
+
+

Email / SMTP

+
+
+
Configurazione Email
+

Vai in Impostazioni → Email per configurare la posta elettronica.

+ +
Server IMAP (Ricezione)
+

Usato per ricevere e leggere le email direttamente nell'app.

+
+
Campi consigliati per provider comuni
+ + + + + + + + +
ProviderHostPortaCrittografia
Gmailimap.gmail.com993SSL
Outlook/Office 365outlook.office365.com993SSL
Liberoimapmail.libero.it993SSL
Arubaimap.aruba.it993SSL
+
+ +
Server SMTP (Invio)
+

Usato per inviare email dall'app. Supporta account multipli (mittenti).

+
+
Campi consigliati per provider comuni
+ + + + + + + + +
ProviderHostPortaCrittografia
Gmailsmtp.gmail.com587TLS
Outlook/Office 365smtp.office365.com587TLS
Liberosmtp.libero.it465SSL
Arubasmtp.aruba.it465SSL
+
+ +
Gmail / Google Workspace
+
+ + Gmail richiede una Password per l'app (non la password normale) se usi la verifica in due passaggi. Puoi generarla su + https://myaccount.google.com/apppasswords. +
+ +
Mailing
+

Le Mailing List permettono di inviare email a gruppi predefiniti di destinatari.

+
    +
  • Crea una mailing list in Mailing List → Nuova
  • +
  • Puoi popolare la lista da individui, gruppi o manualmente
  • +
  • Invia email alla lista da Email → Nuova Email, selezionando la mailing list come destinatario
  • +
+ +
Test connessione
+

Dopo aver configurato i server, usa il pulsante Test Connessione per verificare che IMAP e SMTP funzionino correttamente.

+
+
+
+
+ + +
+
+
+

Backup e Migrazione Server

+
+
+
Cos'è
+

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.

+ +
Cosa include il backup
+ + + + + + + + +
ComponenteInclusoDettagli
Database (MySQL)SempreStruttura, dati, stored procedure, eventi, trigger
File caricatiOpzionaleDocumenti, avatar, gallerie
Configurazione .envOpzionaleAPP_KEY, credenziali DB, SMTP, token OAuth
MetadatiSempreVersione app, PHP, Laravel
+ +
Come eseguire un backup
+
+
+
+
Via Interfaccia Web
+
    +
  1. Vai su Admin → Backup
  2. +
  3. Verifica le impostazioni (includi files/.env)
  4. +
  5. Clicca Esegui Backup Ora
  6. +
  7. Scarica il file ZIP generato
  8. +
+
+
+
+
+
Via Comando
+
php artisan backup:run
+

Il file viene creato in storage/app/backups/

+
+
+
+ +
Backup automatico
+

Puoi abilitare il backup automatico dalla pagina Admin → Backup:

+
    +
  • Frequenza: giornaliero, settimanale o mensile
  • +
  • Ora: consigliata 03:00 (notte)
  • +
  • Requisito: il cron di Laravel deve essere configurato sul server
  • +
+
+ + Per il backup automatico, aggiungi questo cron: +
* * * * * cd  && php artisan schedule:run >> /dev/null 2>&1
+
+ +
+ +

Procedura di Migrazione Completa

+

Segui questa procedura per migrare Glastree su un nuovo server LAMP (Linux, Apache, MySQL, PHP).

+ +
+ + ATTENZIONE: APP_KEY +

L'APP_KEY cripta i dati sensibili (password email SMTP, token OAuth Google Drive, ecc.).

+
    +
  • Se hai il .env originale dal backup: NON rigenerare APP_KEY — mantieni i dati criptati accessibili.
  • +
  • Se hai perso il .env: DEVI rigenerare APP_KEY, ma perderai l'accesso ai dati criptati esistenti. Dovrai riconfigurare SMTP e OAuth.
  • +
+
+ +
Passo 1: Preparazione del server
+
sudo apt update && sudo apt upgrade -y
+ +
Passo 2: Installa LAMP + PHP 8.4
+
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
+ +
Passo 3: Clona l'applicazione
+
cd /var/www
+git clone <URL_REPOSITORY> glastree
+cd glastree
+composer install --no-dev --optimize-autoloader
+npm ci && npm run build
+ +
Passo 4: Carica il backup
+

Copia il file backup.zip sul nuovo server e estrailo:

+
mkdir -p /tmp/restore
+unzip backup.zip -d /tmp/restore/
+ls -la /tmp/restore/
+# Dovresti vedere: database.sql  .env  files/  manifest.json
+ +
Passo 5: Crea database e importa
+
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
+ +
Passo 6: Configura .env
+
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
+ +
Passo 7: Ripristina file e permessi
+
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
+ +
Passo 8: Cache
+
php artisan optimize:clear
+php artisan config:cache
+php artisan route:cache
+php artisan view:cache
+ +
Passo 9: Configura Apache
+
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
+ +
Passo 10: SSL Let's Encrypt
+
sudo apt install -y certbot python3-certbot-apache
+sudo certbot --apache -d glastree.esempio.it
+ +
Passo 11: Cron per backup automatico
+
sudo crontab -u www-data -e
+# Inserisci:
+* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1
+ +
Passo 12: Verifica finale
+
    +
  1. Apri https://glastree.esempio.it nel browser
  2. +
  3. Fai login con le credenziali esistenti
  4. +
  5. Verifica: Dashboard, Documenti, Impostazioni, Email
  6. +
  7. Se hai rigenerato APP_KEY: riconfigura SMTP e OAuth Google Drive
  8. +
+ +
+ +
PHP Web Installer
+

L'applicazione include un wizard di installazione web nella cartella installer/. Questo wizard gestisce sia installazioni fresh che ripristino da backup.

+

Vedi la sezione Installazione per la guida completa.

+
+
+
+ + +
+
+
+

Docker

+
+
+
Creare un'immagine Docker con dati di base
+

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:

+ +
+
+
+
SQLite (Leggero)
+

Database embedded, nessun servizio aggiuntivo. Ideale per demo, test, sviluppo locale o ambienti con poca concorrenza.

+
+
+
+
+
MySQL (Standard)
+

Database separato in un container MySQL. Configurazione standard, ideale per produzione, staging e ambienti con piu utenti.

+
+
+
+ +
1. Dockerfile (comune a entrambe le modalita)
+

Crea un file Dockerfile nella root del progetto:

+
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"]
+ +
2. Entrypoint Script (comune)
+

Crea docker-entrypoint.sh. Lo script rileva automaticamente la configurazione in base alle variabili d'ambiente:

+
#!/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 "$@"
+
chmod +x docker-entrypoint.sh
+ +
3. Seed Base Dati (comune)
+

Per includere tipologie documenti, tipologie eventi e ruoli, crea un seeder dedicato:

+
php artisan make:seeder DockerBaseDataSeeder
+

Contenuto di database/seeders/DockerBaseDataSeeder.php:

+
<?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]
+            );
+        }
+    }
+}
+

Registralo in database/seeders/DatabaseSeeder.php:

+
$this->call([
+    // ... altri seeder ...
+    DockerBaseDataSeeder::class,
+]);
+ +
4. Docker Compose
+ +
Opzione A — SQLite (leggero)
+
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:
+ +
Opzione B — MySQL (standard)
+
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:
+ +
5. Build e avvio
+
# 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
+ +
6. Primo accesso
+

Al primo avvio, l'entrypoint esegue automaticamente migration e seed. Troverai gia popolati:

+
    +
  • Tipologie Documenti: documento, avatar, galleria, statuto, programma, locandina, altro
  • +
  • Tipologie Eventi: catechesi, liturgia, animazione, formazione, incontro, riunione, festa, altro
  • +
  • Ruoli: presidente, vicepresidente, segretario, tesoriere, consigliere, volontario, socio, altro
  • +
+

Dovrai creare il primo utente amministratore via CLI:

+
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!';
+"
+ +
+
Note importanti
+
    +
  • Persistenza: i volumi Docker preservano storage e database tra i riavvii
  • +
  • APP_KEY: viene generata al primo avvio — se elimini i volumi, la perdi e i dati criptati diventano inaccessibili
  • +
  • MySQL in produzione: imposta password sicure per MYSQL_PASSWORD e MYSQL_ROOT_PASSWORD
  • +
  • File compose separati: puoi mantenere entrambi i file (docker-compose.yml per SQLite, docker-compose.mysql.yml per MySQL) e scegliere con -f
  • +
+
+
+
+
+ + +
+
+
+

Guida all'Installazione

+
+
+
Panoramica
+

Glastree include un wizard di installazione web che guida passo-passo nella configurazione su un nuovo server LAMP. Supporta due modalita:

+
+
+
+
Fresh Install
+

Installa l'applicazione da zero su un nuovo server: crea il database, esegue le migration, popola i dati di base e crea l'amministratore.

+
+
+
+
+
Restore da Backup
+

Ripristina un'installazione esistente da un file ZIP di backup: importa il database, ripristina i documenti e la configurazione.

+
+
+
+ +
Requisiti del Server
+ + + + + + + + + +
ComponenteVersione MinimaNote
Web ServerApache 2.4+Con mod_rewrite abilitato
PHP8.2+ (consigliato 8.4)Estensioni: pdo_mysql, mbstring, xml, curl, zip, gd, fileinfo
MySQL / MariaDBMySQL 8.0+ / MariaDB 10.5+Supporto utf8mb4
Composer2.xPer gestione dipendenze PHP
Node.js / npmNode 20+ / npm 10+Per compilazione asset frontend (opzionale se precompilati)
+ +
Installazione Passo-Passo
+ +
Passo 1: Preparare il server LAMP
+
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
+ +
Passo 2: Ottenere l'applicazione
+
cd /var/www
+git clone <URL_REPOSITORY> glastree
+cd glastree
+composer install --no-dev --optimize-autoloader
+npm ci && npm run build
+
+ + Se non hai accesso a npm, puoi saltare il build. L'interfaccia usera comunque AdminLTE via CDN. +
+ +
Passo 3: Permessi cartelle
+
sudo chown -R www-data:www-data storage bootstrap/cache
+sudo chmod -R 775 storage bootstrap/cache
+ +
Passo 4: Configurare Apache
+
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
+ +
Passo 5: Avviare il wizard di installazione
+

Apri il browser e visita l'indirizzo del tuo server:

+
http://glastree.esempio.it/installer/
+ +

Il wizard ti guidera attraverso 6 passi:

+ + + + + + + + + + + +
PassoDescrizione
1. BenvenutoScegli Fresh Install o Restore da Backup
2. RequisitiVerifica PHP, estensioni e permessi
3. DatabaseConfigura MySQL (crea nuovo o usa esistente)
4. InstallazioneFresh: migration e seed. Backup: upload ZIP
5. AmministratoreFresh: crea super-admin. Backup: verifica utenti
6. FinalizzazioneCache, storage link, auto-eliminazione installer
+ +
+ + Attenzione: Al termine dell'installazione, la cartella installer/ viene automaticamente eliminata. Non puoi rieseguire l'installazione se non ricreando manualmente la cartella. +
+ +
Modalita Fresh Install
+

Seleziona questa modalita quando installi Glastree per la prima volta su un server.

+

Cosa succede durante l'installazione:

+
    +
  1. Genera APP_KEY per la crittografia
  2. +
  3. Esegue tutte le migration per creare le tabelle del database
  4. +
  5. Popola i dati di base (diocesi, comuni, ruoli, preset)
  6. +
  7. Crea il collegamento storage → public/storage
  8. +
  9. Crea l'utente amministratore con i permessi completi
  10. +
+ +
Modalita Restore da Backup
+

Seleziona questa modalita quando vuoi migrare un'installazione esistente su un nuovo server.

+

Cosa serve:

+
    +
  • Un file ZIP generato dalla pagina Admin → Backup del server originale
  • +
  • Il wizard accetta upload diretto del file o un percorso sul server
  • +
+

Cosa succede durante il restore:

+
    +
  1. Estrae il file ZIP in una cartella temporanea
  2. +
  3. Importa database.sql nel database MySQL configurato
  4. +
  5. Copia i file documenti in storage/app/documenti/
  6. +
  7. Ripristina il file .env dal backup (preservando le credenziali DB del nuovo server)
  8. +
+ +
Dopo l'Installazione
+
    +
  1. Configura SSL con Let's Encrypt: +
    sudo apt install -y certbot python3-certbot-apache
    +sudo certbot --apache -d glastree.esempio.it
    +
  2. +
  3. Accedi con le credenziali amministratore create durante l'installazione
  4. +
  5. Configura l'applicazione in Impostazioni: +
      +
    • Nome applicazione e logo
    • +
    • Email (SMTP per invio, IMAP per ricezione)
    • +
    • Repository remoti (Google Drive, Nextcloud)
    • +
    +
  6. +
  7. Crea utenti dal pannello Admin → Utenti
  8. +
  9. Configura il cron per backup automatico e sync email: +
    sudo crontab -u www-data -e
    +# Inserisci:
    +* * * * * cd /var/www/glastree && php artisan schedule:run >> /dev/null 2>&1
    +
  10. +
+ +
Risoluzione Problemi
+
+
Errore "500 | Server Error" dopo l'installazione
+

Probabilmente mancano permessi sulle cartelle storage o bootstrap/cache:

+
sudo chown -R www-data:www-data storage bootstrap/cache
+sudo chmod -R 775 storage bootstrap/cache
+
+
+
Errore "No application encryption key"
+

APP_KEY non generata. Nella cartella dell'app:

+
php artisan key:generate
+
+
+
mod_rewrite non funziona
+

Se le rotte restituiscono 404, verifica che Apache abbia AllowOverride:

+
sudo a2enmod rewrite
+# Nel VirtualHost: AllowOverride All
+sudo systemctl reload apache2
+
+
+
+
+
+
+ + + +stopSection(); ?> + +make('layouts.adminlte', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php b/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php index 9e033cd8..6cd2322a 100755 --- a/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php +++ b/storage/framework/views/f5ef96019b613b67dddc87baed21eb0c.php @@ -23,6 +23,11 @@ $appName = \App\Models\AppSetting::getAppName() ?? 'Glastree';