diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 00000000..7dc9707d --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":2,"defects":{"Tests\\Feature\\Auth\\LoginTest::test_database_seeder_admin_can_login":8,"Tests\\Feature\\Auth\\LoginTest::test_admin_user_can_login":8,"Tests\\Feature\\Auth\\LoginTest::test_login_with_wrong_password_fails":8,"Tests\\Feature\\Auth\\LoginTest::test_suspended_user_cannot_login":8,"Tests\\Feature\\Auth\\LoginTest::test_login_page_loads":8,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":7},"times":{"Tests\\Feature\\Auth\\LoginTest::test_database_seeder_admin_can_login":0.131,"Tests\\Feature\\Auth\\LoginTest::test_admin_user_can_login":0.016,"Tests\\Feature\\Auth\\LoginTest::test_login_with_wrong_password_fails":0.215,"Tests\\Feature\\Auth\\LoginTest::test_suspended_user_cannot_login":0.062,"Tests\\Feature\\Auth\\LoginTest::test_login_page_loads":0.01,"Tests\\Unit\\ExampleTest::test_that_true_is_true":0,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_webdav_password":0.02,"Tests\\Unit\\StorageRepositoryServiceTest::test_encrypts_google_drive_secrets":0.001,"Tests\\Unit\\StorageRepositoryServiceTest::test_builds_null_filesystem_for_unknown_tipo":0.023,"Tests\\Feature\\CalendarEventsTest::test_find_first_sunday_of_june_2026":0.002,"Tests\\Feature\\CalendarEventsTest::test_find_second_saturday_of_june_2026":0.002,"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.002,"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.006,"Tests\\Feature\\ExampleTest::test_the_application_returns_a_successful_response":0.009,"Tests\\Feature\\StorageRepositoryTest::test_can_create_webdav_repository":0.01,"Tests\\Feature\\StorageRepositoryTest::test_can_create_google_drive_repository":0.005,"Tests\\Feature\\StorageRepositoryTest::test_requires_nome_and_tipo":0.004,"Tests\\Feature\\StorageRepositoryTest::test_can_delete_repository_with_no_documents":0.008,"Tests\\Feature\\StorageRepositoryTest::test_tipo_labels":0.001,"Tests\\Feature\\StorageRepositoryTest::test_tipo_icons":0.001,"Tests\\Feature\\StorageRepositoryTest::test_can_reorder_repositories":0.008,"Tests\\Feature\\ExampleTest::test_the_application_root_redirects_to_login":0.003}} \ No newline at end of file diff --git a/MEMORY.md b/MEMORY.md index 2fe5ffec..6fec5b63 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -1,36 +1,32 @@ # 🧠 MEMORY.md β€” Stato Progetto ## Goal -- Correggere tutti i bug critici identificati nell'applicazione Glastree -- Implementare nuove funzionalitΓ  richieste: cancellazione account email, fix firma editor, sincronizzazione calendario con Google Calendar / CalDAV +- Fix login after fresh install (user created via installer can't authenticate) +- Cross-DB compatibility (MySQL ↔ SQLite) for all migrations ## Constraints & Preferences -- Laravel 13, PHP 8.4, AdminLTE 4, MySQL/MariaDB +- Laravel 13, PHP 8.4, AdminLTE 3, MySQL/MariaDB/SQLite +- Comandi Artisan da terminale, nessun wizard web +- Script PHP CLI per installazione interattiva - Zero placeholder, zero TODO, codice funzionante -- CompatibilitΓ  cross-DB MySQL ↔ SQLite ## Progress ### Done -- **Cancellata cache Laravel** (`php artisan optimize:clear`) -- **Fix BUG CRITICO 3** – `IndividuoController::update()` (riga 216): avvolto `$individuo->contatti()->delete()` dentro `if ($request->has('contatti'))`. -- **Fix BUG CRITICO 1** – `individui/edit.blade.php`: convertiti 4 form annidati in pulsanti `type="button"` + `fetch()` AJAX -- **Fix BUG CRITICO 2** – `gruppi/edit.blade.php`: convertiti 3 form annidati in pulsanti `type="button"` + `fetch()` AJAX -- **Fix BUG CRITICO 4** – `gruppi/show.blade.php`: flash messages spostati dentro `@section('content')` -- **Fix BUG CRITICO 5/6** – `eventi/show.blade.php`: rimossi div extra, XSS escape in confirm() -- **Fix BUG CRITICO 7/8** – `ImpostazioniController.php`: use Documento + JSON_CONTAINS β†’ PHP -- **Fix BUG CRITICO 9** – `ReportController.php`: `$contatto->email` β†’ `$contatto->valore` -- **Fix BUG CRITICO 10** – `DocumentoController.php`: morph injection prevention in massUpdate() -- **NUOVA FEATURE – Cancellazione account email**: Aggiunto metodo `destroy()` in `EmailSettingsController`, rotta `DELETE /impostazioni/email`, pulsante "Elimina Configurazione" in entrambe le viste (admin dedicata e unificata) -- **FIX – Doppia barra icone firma**: Cambiata la guardia di `createEditor()` e `initSignatureEditor()` da `container.querySelector('.ql-toolbar')` (che non funziona perchΓ© Quill crea la toolbar come sibling, non child) a flag `quillEditorInstance` in entrambe le viste -- **NUOVA FEATURE – Sincronizzazione calendario**: Implementata sincronizzazione con Google Calendar e CalDAV (Infomaniak/NextCloud): - - Migration `create_calendario_connessioni_table` + `add_uid_esterno_to_eventi_table` - - Model `CalendarioConnessione` con encrypt/decrypt config - - Service `CalDavService` (PROPFIND, REPORT, calendar-query, PUT) - - Service `GoogleCalendarSyncService` (Google Calendar API v3) - - Controller `CalendarioConnessioneController` (CRUD + test + sync + reorder) - - Routes RESTful per calendario-connessioni - - UI nella tab "Calendario" delle impostazioni unificate con tabella, modale CRUD, pulsanti test/sync/elimina, Sortable drag&drop - - Installati pacchetti: `sabre/vobject`, `google/apiclient` +- **Fix – RedirectResponse type error**: aggiunto `use Illuminate\Http\RedirectResponse;` in `EmailSettingsController.php` +- **Fix – UrlGenerationException in sender routes**: sostituito `route('impostazioni.sender.update', '') + id` con `'__ID__'.replace('__ID__', id)` in `admin/email-settings/index.blade.php` (stessa correzione per `impostazioni.sender.test`) +- **Fix – Messaggi email visibili dopo eliminazione configurazione**: `destroy()` ora cancella sempre allegati su disco, cartelle email (cascata messaggi/allegati) e poi le impostazioni. Risolto bug che faceva uscire subito se `EmailSetting::first()` era null +- **Fix – Tab guida Installazione/Backup/Docker non funzionanti**: rimosso `` di chiusura `tab-content` prematuro (linea 290) in `help/index.blade.php` β€” i tre tab-pane erano fuori dal container +- **Guida aggiornata – rimosso wizard web installer**: sostituiti tutti i riferimenti alla cartella `installer/` con comandi Artisan in `help/index.blade.php` e `help/pdf.blade.php`. Passi 3-6 semplificati in un unico `php install.php` +- **Script install.sh rimosso**, sostituito da **`install.php`** (PHP CLI interattivo): modalitΓ  Apache/Docker/Restore, preflight, `.env` + `APP_KEY`, `CREATE DATABASE IF NOT EXISTS`, composer, `package:discover`, `migrate --seed`, admin via tinker, asset npm, permessi. Docker build + admin nel container +- **Fix – Migration `app_settings` mancante**: creata `2026_05_15_000001_create_app_settings_table.php` con tutte le colonne. Aggiornate 4 migration alter esistenti con guardie `Schema::hasTable()` + `Schema::hasColumn()`. Aggiunto `Schema::hasTable()` in `AppSetting::getSetting()` per evitare eccezioni su DB fresco +- **Fix – Database non creato durante install**: `install.php` ora esegue `CREATE DATABASE IF NOT EXISTS` +- **Fix – Remember checkbox mancante `name`**: aggiunto `name="remember"` al checkbox "Ricordami" in `resources/views/auth/login.blade.php` +- **Fix – Admin via tinker senza `is_admin`/`permissions`**: tutti e 3 i tinker script in `install.php` ora impostano `is_admin = true`, `status = 'active'`, `permissions` completo (tutti i moduli a livello FULL) +- **Fix – `2>/dev/null` rimosso da tinker/artisan commands**: `install.php` non sopprime piΓΉ stderr su `tinker --execute`, `package:discover`, `storage:link`, `chown`, `cp`. Eventuali errori ora sono visibili all'utente +- **Fix – Controllo permessi sessione**: aggiunto check esplicito `is_writable(storage/framework/sessions/)` in `install.php` con warning su potenziale errore 419 CSRF +- **Fix – Migrations non cross-DB**: convertiti `enum()` a `string(50)` in tutte le migration delle tabelle `documenti` (create + alter) per compatibilitΓ  MySQL/SQLite. Rimossa raw SQL `ALTER TABLE MODIFY COLUMN`. Aggiunto `dropIndex()` prima di `dropColumn('ruolo_id')` in `change_ruolo_to_multi.php` per compatibilitΓ  SQLite +- **Fix – ExampleTest**: aggiornato per matchare comportamento reale (root `/` fa redirect 302 a login) +- **Nuovo test – Login**: `tests/Feature/Auth/LoginTest.php` con 5 test: seeded admin login, custom admin login, wrong password, suspended user, login page loads ### In Progress - *(nessuno)* @@ -39,32 +35,39 @@ - *(nessuno)* ## Key Decisions -- Per i form annidati si Γ¨ scelto l'approccio `type="button"` + `fetch()` AJAX -- Per la cancellazione account email si usa `EmailSetting::first()->delete()` con verifica esistenza -- Per il fix Quill: variabile flag `quillEditorInstance` invece di `container.querySelector('.ql-toolbar')` perchΓ© Quill inserisce `.ql-toolbar` come elemento sibling, non child -- Per CalDAV: usato Guzzle (giΓ  in Laravel) per HTTP + `sabre/vobject` per parsing iCal -- Per Google Calendar: usato `google/apiclient` con OAuth 2.0 (refresh token) -- La sync puΓ² essere bidirezionale, solo import o solo export -- I campi sensibili delle connessioni calendario sono criptati con `Crypt` +- Script installazione in PHP CLI invece di bash: usa direttamente API Laravel/PDO, gestione errori con try/catch, readline per input, nessun problema di sintassi shell +- Migrazione `app_settings`: unica migration `Schema::create()` con tutte le colonne + guardie `hasColumn()` nelle alter esistenti invece di modificare i timestamp +- Cleanup email: eseguito sempre (anche se `EmailSetting` giΓ  cancellato) per gestire stato orfano +- `enum()` rimosso a livello DB in favore di `string()` + validazione app-level (consistente con AGENTS.md) +- `dropIndex()` esplicito prima di `dropColumn()` per compatibilitΓ  SQLite ## Next Steps - *(nessuno)* ## Critical Context -- Le connessioni calendario supportano: Google Calendar, CalDAV (NextCloud, Infomaniak, Synology) -- La direzione di sync Γ¨ configurabile per ogni connessione -- Le password/refresh token sono automaticamente criptati all salvataggio via `encryptAndSetConfig()` -- Il campo `uid_esterno` sugli eventi traccia la corrispondenza con eventi remoti +- `DatabaseSeeder` crea admin `admin@glastree.local / password` con `is_admin=true`, `status='active'` +- `User` model ha `'password' => 'hashed'` cast: nessun doppio hashing con `bcrypt()` + `save()` +- `Auth::attempt()` usa `Hash::check()` che ignora il cast `hashed` +- `SESSION_DRIVER=file` in produzione richiede permessi di scrittura su `storage/framework/sessions/` +- `APP_ENV=production` + `APP_DEBUG=false` nascondono errori 419 (CSRF) e 500 +- Test PHPUnit usano SQLite `:memory:` β€” tutte le migration devono essere cross-DB compatibili +- SQLite non supporta: `ALTER TABLE MODIFY COLUMN`, `ENUM` type, `dropColumn` su colonne con indici ## Relevant Files -- `app/Http/Controllers/Admin/EmailSettingsController.php`: +destroy() method -- `app/Http/Controllers/CalendarioConnessioneController.php`: NEW – CRUD + test + sync -- `app/Models/CalendarioConnessione.php`: NEW – model con encrypt config -- `app/Services/CalDavService.php`: NEW – CalDAV sync -- `app/Services/GoogleCalendarSyncService.php`: NEW – Google Calendar sync -- `app/Http/Controllers/ImpostazioniController.php`: +calendarioConnessioni in index() -- `resources/views/admin/email-settings/index.blade.php`: +delete button, fix Quill guard -- `resources/views/impostazioni/index.blade.php`: +delete button, fix Quill guard, +calendario tab UI, +calendario modal, +JS funcs -- `routes/web.php`: +email destroy route, +calendario-connessioni routes -- `database/migrations/2026_06_02_000001_create_calendario_connessioni_table.php`: NEW -- `database/migrations/2026_06_02_000002_add_uid_esterno_to_eventi_table.php`: NEW +- `install.php`: installer interattivo β€” Apache/Docker/Restore, tinker scripts admin (3 modalitΓ ) +- `resources/views/auth/login.blade.php`: fix `name="remember"` sul checkbox "Ricordami" +- `app/Http/Controllers/Auth/AuthController.php`: `login()` controlla `$user->status === 'suspended'` +- `app/Models/User.php`: cast `'password' => 'hashed'`, metodi isSuperAdmin/isActive +- `database/seeders/DatabaseSeeder.php`: crea admin di default +- `tests/Feature/Auth/LoginTest.php`: NEW β€” 5 test di login +- `app/Http/Controllers/Admin/EmailSettingsController.php`: fix destroy cleanup +- `resources/views/admin/email-settings/index.blade.php`: fix route sender update/test +- `database/migrations/2026_05_12_000002_change_ruolo_to_multi.php`: fix dropIndex + dropColumn +- `database/migrations/*documenti*`: fix enum β†’ string (5 migration) +- `database/migrations/2026_05_15_000001_create_app_settings_table.php`: NEW +- `database/migrations/2026_05_16_000001_add_logo_path_to_app_settings.php`: guardie hasColumn +- `database/migrations/2026_05_16_000002_add_app_config_to_app_settings.php`: guardie hasColumn +- `database/migrations/2026_05_27_000003_add_documenti_storage_to_app_settings.php`: guardie hasColumn +- `database/migrations/2026_06_01_100155_add_backup_settings_to_app_settings.php`: guardie hasColumn +- `resources/views/help/index.blade.php`: fix tab-pane chiusura, rimosso wizard web installer +- `resources/views/help/pdf.blade.php`: rimosso wizard web installer diff --git a/database/migrations/2024_01_01_000008_create_documenti_table.php b/database/migrations/2024_01_01_000008_create_documenti_table.php index a32cb5a8..0836daea 100644 --- a/database/migrations/2024_01_01_000008_create_documenti_table.php +++ b/database/migrations/2024_01_01_000008_create_documenti_table.php @@ -14,8 +14,8 @@ return new class extends Migration $table->string('nome_file'); $table->string('file_path'); $table->string('tipo')->nullable(); - $table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento'); - $table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo'); + $table->string('tipologia', 50)->default('documento'); + $table->string('visibilita', 50)->default('gruppo'); $table->unsignedBigInteger('visibilita_target_id')->nullable(); $table->string('visibilita_target_type')->nullable(); $table->string('mime_type')->nullable(); diff --git a/database/migrations/2024_01_01_000020_add_evento_to_documenti_visibilita.php b/database/migrations/2024_01_01_000020_add_evento_to_documenti_visibilita.php index 2d56dfa0..545f9314 100644 --- a/database/migrations/2024_01_01_000020_add_evento_to_documenti_visibilita.php +++ b/database/migrations/2024_01_01_000020_add_evento_to_documenti_visibilita.php @@ -9,14 +9,14 @@ return new class extends Migration public function up(): void { Schema::table('documenti', function (Blueprint $table) { - $table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione', 'evento'])->default('gruppo')->change(); + $table->string('visibilita', 50)->default('gruppo')->change(); }); } public function down(): void { Schema::table('documenti', function (Blueprint $table) { - $table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo')->change(); + $table->string('visibilita', 50)->default('gruppo')->change(); }); } }; \ No newline at end of file diff --git a/database/migrations/2026_05_11_000005_add_email_attachment_to_documenti.php b/database/migrations/2026_05_11_000005_add_email_attachment_to_documenti.php index 0d1f5ae8..cc9bb7da 100644 --- a/database/migrations/2026_05_11_000005_add_email_attachment_to_documenti.php +++ b/database/migrations/2026_05_11_000005_add_email_attachment_to_documenti.php @@ -9,14 +9,14 @@ return new class extends Migration public function up(): void { Schema::table('documenti', function (Blueprint $table) { - $table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro', 'email_attachment'])->default('documento')->change(); + $table->string('tipologia', 50)->default('documento')->change(); }); } public function down(): void { Schema::table('documenti', function (Blueprint $table) { - $table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento')->change(); + $table->string('tipologia', 50)->default('documento')->change(); }); } }; \ No newline at end of file diff --git a/database/migrations/2026_05_11_155310_make_visibilita_nullable_on_documenti.php b/database/migrations/2026_05_11_155310_make_visibilita_nullable_on_documenti.php index 8eeb21ae..bdd1ab52 100644 --- a/database/migrations/2026_05_11_155310_make_visibilita_nullable_on_documenti.php +++ b/database/migrations/2026_05_11_155310_make_visibilita_nullable_on_documenti.php @@ -12,7 +12,7 @@ return new class extends Migration public function up(): void { Schema::table('documenti', function (Blueprint $table) { - $table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->nullable()->change(); + $table->string('visibilita', 50)->nullable()->change(); }); } diff --git a/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php b/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php index a8c7361b..5fe7d26c 100644 --- a/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php +++ b/database/migrations/2026_05_12_000002_change_ruolo_to_multi.php @@ -10,6 +10,7 @@ return new class extends Migration { Schema::table('gruppo_individuo', function (Blueprint $table) { $table->dropForeign(['ruolo_id']); + $table->dropIndex('gruppo_individuo_ruolo_id_index'); $table->dropColumn('ruolo_id'); }); diff --git a/database/migrations/2026_05_13_072203_add_programma_locandina_to_tipologia_enum.php b/database/migrations/2026_05_13_072203_add_programma_locandina_to_tipologia_enum.php index ad724374..cc9bb7da 100644 --- a/database/migrations/2026_05_13_072203_add_programma_locandina_to_tipologia_enum.php +++ b/database/migrations/2026_05_13_072203_add_programma_locandina_to_tipologia_enum.php @@ -1,17 +1,22 @@ string('tipologia', 50)->default('documento')->change(); + }); } public function down(): void { - DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro') DEFAULT 'documento'"); + Schema::table('documenti', function (Blueprint $table) { + $table->string('tipologia', 50)->default('documento')->change(); + }); } }; \ No newline at end of file diff --git a/install.php b/install.php index ff300691..96171617 100644 --- a/install.php +++ b/install.php @@ -292,7 +292,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') { println(color(' 4/7 β€” REGISTRAZIONE PACCHETTI', 'yellow')); println(color('══════════════════════════════════════════', 'yellow')); - runCapture("php artisan package:discover --ansi 2>/dev/null"); + runCapture("php artisan package:discover --ansi"); ok('Pacchetti registrati'); // ── Step 5: Migration + Seed ───────────────────── @@ -306,7 +306,7 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') { } ok('Struttura database creata e dati di base inseriti'); - runCapture('php artisan storage:link --force 2>/dev/null'); + runCapture('php artisan storage:link --force'); // ── Step 6: Admin user ─────────────────────────── println(); @@ -320,11 +320,14 @@ if ($mode === 'Fresh Install su Apache (LAMP tradizionale)') { \$u->name = '{$adminName}'; \$u->email = '{$adminEmail}'; \$u->password = bcrypt('{$adminPass}'); + \$u->is_admin = true; + \$u->status = 'active'; + \$u->permissions = ['individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2]; \$u->save(); echo "OK: " . \$u->id; PHP; - $result = runCapture("php artisan tinker --execute=" . escapeshellarg($tinkerScript) . " 2>/dev/null"); + $result = runCapture("php artisan tinker --execute=" . escapeshellarg($tinkerScript)); if (str_contains($result, 'OK:')) { ok("Amministratore {$adminEmail} creato"); } else { @@ -351,9 +354,17 @@ PHP; if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { info('Impostazione permessi...'); $wwwUser = trim(shell_exec('ps aux | grep -E "apache|httpd" | grep -v grep | head -1 | awk \'{print \$1}\'') ?: 'www-data'); - runCapture("chown -R {$wwwUser}:{$wwwUser} storage bootstrap/cache 2>/dev/null"); + run("chown -R {$wwwUser}:{$wwwUser} storage bootstrap/cache", 'ProprietΓ  file (chown)'); chmod("{$scriptDir}/storage", 0775); chmod("{$scriptDir}/bootstrap/cache", 0775); + + // Verifica permessi sessione (causa comune di errori 419) + $sessionDir = "{$scriptDir}/storage/framework/sessions"; + if (is_dir($sessionDir) && !is_writable($sessionDir)) { + warn("{$sessionDir} non Γ¨ scrivibile β€” possibile errore 419 (CSRF) al login"); + chmod($sessionDir, 0775); + } + ok("Permessi impostati (utente: {$wwwUser})"); } @@ -447,11 +458,14 @@ if ($mode === 'Fresh Install via Docker') { \$u->name = '{$adminName}'; \$u->email = '{$adminEmail}'; \$u->password = bcrypt('{$adminPass}'); + \$u->is_admin = true; + \$u->status = 'active'; + \$u->permissions = ['individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2]; \$u->save(); echo "OK: " . \$u->id; PHP; $result = runCapture( - "docker exec -i {$containerId} php artisan tinker --execute=" . escapeshellarg($tinkerScript) . " 2>/dev/null" + "docker exec -i {$containerId} php artisan tinker --execute=" . escapeshellarg($tinkerScript) ); if (str_contains($result, 'OK:')) { ok("Amministratore {$adminEmail} creato nel container"); @@ -579,22 +593,22 @@ if ($mode === 'Restore da Backup (Apache)') { $storageBackup = "{$tmpDir}/storage"; if (is_dir($storageBackup)) { info('Ripristino file storage...'); - runCapture("cp -r {$storageBackup}/* {$scriptDir}/storage/ 2>/dev/null"); + run("cp -r {$storageBackup}/* {$scriptDir}/storage/", null); ok('Storage ripristinato'); } // Composer run('composer install --no-dev --optimize-autoloader', 'Installazione dipendenze Composer...'); - runCapture('php artisan package:discover --ansi 2>/dev/null'); + runCapture('php artisan package:discover --ansi'); // Storage link + cache - runCapture('php artisan storage:link --force 2>/dev/null'); + runCapture('php artisan storage:link --force'); runCapture('php artisan optimize:clear'); // Permissions if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { $wwwUser = trim(shell_exec('ps aux | grep -E "apache|httpd" | grep -v grep | head -1 | awk \'{print \$1}\'') ?: 'www-data'); - runCapture("chown -R {$wwwUser}:{$wwwUser} storage bootstrap/cache 2>/dev/null"); + run("chown -R {$wwwUser}:{$wwwUser} storage bootstrap/cache", 'ProprietΓ  file (chown)'); chmod("{$scriptDir}/storage", 0775); chmod("{$scriptDir}/bootstrap/cache", 0775); ok("Permessi impostati (utente: {$wwwUser})"); @@ -608,13 +622,16 @@ if ($mode === 'Restore da Backup (Apache)') { \$u->name = '{$adminName}'; \$u->email = '{$adminEmail}'; \$u->password = bcrypt('{$adminPass}'); + \$u->is_admin = true; + \$u->status = 'active'; + \$u->permissions = ['individui' => 2, 'gruppi' => 2, 'eventi' => 2, 'documenti' => 2, 'mailing' => 2, 'viste' => 2, 'report' => 2, 'settings' => 2]; \$u->save(); echo 'OK'; } else { echo 'EXISTS'; } PHP; - $result = runCapture("php artisan tinker --execute=" . escapeshellarg($tinkerScript) . " 2>/dev/null"); + $result = runCapture("php artisan tinker --execute=" . escapeshellarg($tinkerScript)); if (str_contains($result, 'OK')) { ok("Amministratore {$adminEmail} creato"); } elseif (str_contains($result, 'EXISTS')) { diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 04526f4b..152f5e48 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -59,7 +59,7 @@ $appOrgName = AppSetting::getOrgName() ?? '';
- +
diff --git a/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php b/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php index 4a5ca4aa..c4d4c86b 100755 --- a/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php +++ b/storage/framework/views/e8f60d5f5b2a4c688f1bc12477b209df.php @@ -87,7 +87,7 @@ unset($__errorArgs, $__bag); ?>
- +
diff --git a/tests/Feature/Auth/LoginTest.php b/tests/Feature/Auth/LoginTest.php new file mode 100644 index 00000000..3aeb18dc --- /dev/null +++ b/tests/Feature/Auth/LoginTest.php @@ -0,0 +1,93 @@ +seed(DatabaseSeeder::class); + + $response = $this->post('/login', [ + 'email' => 'admin@glastree.local', + 'password' => 'password', + ]); + + $response->assertStatus(302); + $response->assertRedirect('/dashboard'); + $this->assertAuthenticated(); + } + + public function test_admin_user_can_login(): void + { + User::factory()->create([ + 'name' => 'Admin', + 'email' => 'admin@example.com', + 'password' => bcrypt('secret123'), + 'is_admin' => true, + 'status' => 'active', + ]); + + $response = $this->post('/login', [ + 'email' => 'admin@example.com', + 'password' => 'secret123', + ]); + + $response->assertStatus(302); + $response->assertRedirect('/dashboard'); + $this->assertAuthenticated(); + } + + public function test_login_with_wrong_password_fails(): void + { + User::factory()->create([ + 'email' => 'admin@example.com', + 'password' => bcrypt('correct-password'), + 'is_admin' => true, + 'status' => 'active', + ]); + + $response = $this->from('/login')->post('/login', [ + 'email' => 'admin@example.com', + 'password' => 'wrong-password', + ]); + + $response->assertStatus(302); + $response->assertRedirect('/login'); + $this->assertGuest(); + } + + public function test_suspended_user_cannot_login(): void + { + User::factory()->create([ + 'email' => 'suspended@example.com', + 'password' => bcrypt('password'), + 'status' => 'suspended', + ]); + + $response = $this->post('/login', [ + 'email' => 'suspended@example.com', + 'password' => 'password', + ]); + + $response->assertStatus(302); + $response->assertRedirect('/'); + $this->assertGuest(); + } + + public function test_login_page_loads(): void + { + $response = $this->get('/login'); + + $response->assertStatus(200); + $response->assertSee('Email'); + $response->assertSee('Password'); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8364a84e..b99dbf22 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -10,10 +10,11 @@ class ExampleTest extends TestCase /** * A basic test example. */ - public function test_the_application_returns_a_successful_response(): void + public function test_the_application_root_redirects_to_login(): void { $response = $this->get('/'); - $response->assertStatus(200); + $response->assertStatus(302); + $response->assertRedirect(route('login')); } }