diff --git a/app/Console/Commands/SendPasswordResetEmailCommand.php b/app/Console/Commands/SendPasswordResetEmailCommand.php new file mode 100644 index 0000000..a646014 --- /dev/null +++ b/app/Console/Commands/SendPasswordResetEmailCommand.php @@ -0,0 +1,61 @@ +argument('email')); + + $this->info("Verifica esistenza utente per email: {$email}..."); + + $user = User::query()->where('email', $email)->first(); + + if (! $user instanceof User) { + $this->error("Errore: nessun utente trovato con l'indirizzo email {$email}."); + return self::FAILURE; + } + + $this->info("Utente trovato: {$user->name} (ID: {$user->id}, Ruoli: " . $user->roles->pluck('name')->join(', ') . ")"); + + $broker = Password::broker(Filament::getAuthPasswordBroker()); + $token = $broker->createToken($user); + + $notification = app(NetGesconResetPasswordNotification::class, ['token' => $token]); + + try { + $panel = Filament::getPanel('admin-filament'); + $url = $panel->getResetPasswordUrl($token, $user); + } catch (\Throwable) { + $url = url(route('filament.admin-filament.auth.password-reset.reset', [ + 'token' => $token, + 'email' => $user->getEmailForPasswordReset(), + ], false)); + } + + $notification->url = $url; + + $this->info("Invio email in corso a {$email} via SMTP (Google Workspace / NetGescon)..."); + + try { + $user->notify($notification); + $this->info("Email inviata con successo!"); + $this->line("Link di ripristino generato: {$url}"); + return self::SUCCESS; + } catch (\Throwable $e) { + $this->error("Errore durante l'invio dell'email: " . $e->getMessage()); + return self::FAILURE; + } + } +} diff --git a/app/Console/Commands/TestMailCommand.php b/app/Console/Commands/TestMailCommand.php new file mode 100644 index 0000000..bf782a8 --- /dev/null +++ b/app/Console/Commands/TestMailCommand.php @@ -0,0 +1,41 @@ +argument('email')); + + $this->info("Diagnostica Configurazione Posta NetGescon:"); + $this->line("- MAILER: " . config('mail.default')); + $this->line("- HOST: " . config('mail.mailers.smtp.host') . ":" . config('mail.mailers.smtp.port')); + $this->line("- USERNAME: " . config('mail.mailers.smtp.username')); + $this->line("- MITTENTE: " . config('mail.from.address') . " (" . config('mail.from.name') . ")"); + $this->line("- CODA (QUEUE_CONNECTION): " . config('queue.default')); + $this->line("- DESTINATARIO TEST: " . $recipient); + + $this->info("Invio in corso..."); + + try { + Mail::raw("Questa e un'email di diagnostica inviata dal server NetGescon (.205) per confermare la corretta attivazione del servizio SMTP con account Google Workspace ({$recipient}). Data/ora: " . now()->toIso8601String(), function ($message) use ($recipient) { + $message->to($recipient) + ->subject('NetGescon - Test Connessione Servizio Email'); + }); + + $this->info("Email di test recapitata con successo a {$recipient}!"); + return self::SUCCESS; + } catch (\Throwable $e) { + $this->error("Errore invio email: " . $e->getMessage()); + return self::FAILURE; + } + } +} diff --git a/app/Models/User.php b/app/Models/User.php index d5d26a9..dc29c27 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -192,6 +192,17 @@ public function canAccessPanel(Panel $panel): bool ]); } + public function sendPasswordResetNotification($token): void + { + $notification = app(\Filament\Auth\Notifications\ResetPassword::class, ['token' => $token]); + $notification->url = url(route('filament.admin-filament.auth.password-reset.reset', [ + 'token' => $token, + 'email' => $this->getEmailForPasswordReset(), + ], false)); + + $this->notify($notification); + } + // public function roles() // { // return $this->belongsToMany(\App\Models\Role::class, 'role_user')->withTimestamps(); diff --git a/app/Notifications/NetGesconResetPasswordNotification.php b/app/Notifications/NetGesconResetPasswordNotification.php new file mode 100644 index 0000000..ec51fd9 --- /dev/null +++ b/app/Notifications/NetGesconResetPasswordNotification.php @@ -0,0 +1,50 @@ +resetUrl($notifiable); + $userName = $notifiable->name ? trim((string) $notifiable->name) : 'Utente NetGescon'; + + return (new MailMessage) + ->subject('NetGescon - Reimpostazione della Password') + ->greeting("Ciao {$userName},") + ->line('Abbiamo ricevuto una richiesta di reimpostazione della password per il tuo account su NetGescon.') + ->action('Reimposta Password', $url) + ->line('Questo link di sicurezza per la reimpostazione della password scadrà tra 60 minuti.') + ->line('Se non sei stato tu a richiedere la reimpostazione della password, non preoccuparti: puoi ignorare questo messaggio e la tua password attuale rimarrà invariata.') + ->salutation('Cordiali saluti, lo Staff di NetGescon'); + } + + protected function resetUrl($notifiable): string + { + if (! empty($this->url)) { + return $this->url; + } + + try { + $panel = Filament::getCurrentOrDefaultPanel(); + if ($panel && method_exists($panel, 'getResetPasswordUrl')) { + return $panel->getResetPasswordUrl($this->token, $notifiable); + } + } catch (\Throwable) { + // fallback al route nominato + } + + return url(route('filament.admin-filament.auth.password-reset.reset', [ + 'token' => $this->token, + 'email' => $notifiable->getEmailForPasswordReset(), + ], false)); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7b01c86..664a558 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -36,6 +36,11 @@ public function register(): void \App\Http\Responses\FortifyVerifyEmailViewResponse::class ); } + + $this->app->bind( + \Filament\Auth\Notifications\ResetPassword::class, + \App\Notifications\NetGesconResetPasswordNotification::class + ); } /** diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index 45ba4e1..bda9103 100755 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -42,6 +42,7 @@ public function panel(Panel $panel): Panel ->login(\App\Filament\Auth\Login::class) ->passwordReset() ->registration() + ->profile() ->colors([ 'primary' => Color::Amber, ]) diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 6dfa292..be98302 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -1,71 +1,66 @@ # CURRENT-205 -TASK_ID: task-tecnorepair-fornitore-sync-workflows +TASK_ID: task-email-service-password-reset MACHINE: .205 STATO: completato ## Obiettivo Completato -1. **Re-sync e Aggiornamento Continuo Seriali e Prodotti da Contabilità Esterna**: - - Aggiunto pulsante esplicito di aggiornamento "Aggiorna da Contabilità Esterna" in `SerialiCatalogo` e `ProdottiCatalogo`. - - Introdotta la verifica automatica e notifica real-time Filament all'apertura delle pagine fornitore con tracciamento `last_sync`. +1. **Attivazione e Diagnostica Servizio Posta SMTP (Google Workspace / Gmail)**: + - Configurato e bonificato `.env` con credenziali dedicate Google Workspace: + - `MAIL_MAILER=smtp` + - `MAIL_HOST=smtp.gmail.com` + - `MAIL_PORT=587` + - `MAIL_ENCRYPTION=tls` + - `MAIL_USERNAME=netgescon@gmail.com` + - `MAIL_FROM_ADDRESS=michele@netgescon.it` (NetGescon) + - Risolto il blocco delle email in coda: impostato `QUEUE_CONNECTION=sync` ed eliminata la dipendenza da daemon `queue:work` non presenti a sistema. + - Svuotata la tabella `jobs` evadendo le notifiche pendenti. -2. **Riproduzione Web TecnoRepair e Wireframe ASCII**: - - Analizzate le schermate desktop legacy in `/home/michele/netgescon-day0-backup/docs/images/Tecnorepair`. - - Redatto e archiviato il documento ufficiale dei wireframe ASCII: `skill-netgescon/ui-wireframes/tecnorepair-schede.md`. - - Creata la pagina Filament dedicata `PraticheTecnorepair` (`/admin-filament/fornitore/pratiche`): - - Griglia elenco pratiche fedele alla versione desktop con badge colorati di stato (verde, rosso, blu, ambra). - - Modale interattivo "Scheda Apparecchio" a 7 schede (Entrata, Lavorazione, Uscita, Preventivo, Spese, Note/Comunicazioni, Allegati). - - Navigazione crociata rapida verso Ticket Operativi, Lavorazioni, Seriali e Prodotti Catalogo. +2. **Notifica di Reimpostazione Password Personalizzata in Italiano (`NetGesconResetPasswordNotification`)**: + - Creata la notifica `App\Notifications\NetGesconResetPasswordNotification`: + - Invio sincrono immediato (senza code asincrone). + - Testo in italiano, grafica formattata, pulsante "Reimposta Password", token univoco e link per copia-incolla con scadenza 60 minuti. + - Mittente ufficiale `michele@netgescon.it` (NetGescon). + - Registrato il binding in `AppServiceProvider` su `\Filament\Auth\Notifications\ResetPassword`. + - Agganciato il metodo `sendPasswordResetNotification()` su `App\Models\User`. -3. **Integrazione Archivio Access TecnoRepairDB.mdb e Fallback Resiliente**: - - Implementato `TecnoRepairArchiveService` con gestione trasparente del percorso UNC Windows `\\192.168.0.36\CServerGO\LunaSoftware_TecnoRepair\Archivi\TecnoRepairDB.mdb` e fallback immediato sul mirror locale Linux (`/home/michele/netgescon-day0-backup/Miki-Bug-workspace/screenshot/Assistenza gestionale/Archivi/TecnoRepairDB.mdb`). - - Importazione cronologica decrescente (le pratiche più recenti 2025/2026 caricate per prime con codici cliente e apparecchi collegati). - - Importazione ricambi `TRicambiAnag` nel catalogo prodotti unificato (`Product`, `ProductIdentifier`, `ProductOffer`) con seriali associati. +3. **Autonomia Cambio Password Utente**: + - **Da non autenticato**: abilitato il flusso completo su `/admin-filament/password-reset/request` (link "Hai dimenticato la password?" nella maschera di login). + - **Da autenticato**: abilitata la schermata profilo `/admin-filament/profile` tramite `->profile()` in `AdminFilamentPanelProvider`, consentendo il cambio password autonomo direttamente dal menu utente in alto a destra. -4. **Automazione Workflow di Riparazione (1-Click)**: - - **Riparato**: aggiorna stato a RIPARATO (cod. 4), imposta data collaudo, tecnico e archivia la pratica. - - **Rottamato / Ricambi**: stato NON CONVENIENTE / ROTTAMATO (cod. 23), marca la scheda per estrazione ricambi e rende i pezzi disponibili a catalogo. - - **Reso al Fornitore (RMA)**: stato MANDATO IN GARANZIA AL PRODUTTORE (cod. 33), generazione codice RMA deterministico e assegnazione al fornitore garante. +4. **Allineamento Utenze Google Workspace / NetGescon**: + - Utenza Workspace `michele@netgescon.it` (User ID 25): attiva con ruoli `super-admin`, `amministratore`, `admin`. + - Utenza Google `netgescon@gmail.com` (User ID 35): registrata e attiva con ruolo `super-admin`. + - Entrambe le utenze possono richiedere e ricevere la reimpostazione password o accedere in autonomia. -5. **Isolamento e Sicurezza Impostazioni Fornitore (Nethome Only)**: - - In `FornitoreScheda` (`anagrafica/fornitori/{record}`), le sezioni di configurazione MySQL esterno e percorsi TecnoRepair sono visibili e azionabili TASSATIVAMENTE solo per Nethome (ID 236 / P.IVA 10055221005). - - Per qualsiasi altro fornitore le chiamate di sincronizzazione e salvataggio restituiscono errore HTTP 403 Forbidden. - -6. **Allineamento Utenti e Ruoli**: - - `michele@nethome.it` (ID 33) rigorosamente allineato al solo ruolo `super-admin`. - - `nethomestore@gmail.com` (ID 34) rigorosamente allineato al solo ruolo `fornitore` e associato a Fornitore 236 (`fornitore_dipendenti`). +5. **Comandi Artisan di Diagnostica e Invio**: + - `php artisan netgescon:test-mail {email?}`: invia un'email diagnostica SMTP per testare la connettività Google Workspace. + - `php artisan netgescon:send-password-reset {email}`: genera il token crittografato e invia il link di ripristino all'email richiesta. ## Output del Giro Operativo ESITO_205: riuscito -TASK_ID: task-tecnorepair-fornitore-sync-workflows +TASK_ID: task-email-service-password-reset REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: a8435c0 +COMMIT: 1970594 FILE_O_AREE_TOCCATE: -- app/Services/Tecnorepair/TecnoRepairArchiveService.php -- app/Console/Commands/TecnoRepairImportLegacyArchiveCommand.php -- app/Filament/Pages/Fornitore/PraticheTecnorepair.php -- resources/views/filament/pages/fornitore/pratiche-tecnorepair.blade.php -- skill-netgescon/ui-wireframes/tecnorepair-schede.md -- app/Filament/Pages/Gescon/FornitoreScheda.php -- resources/views/filament/pages/gescon/fornitore-scheda.blade.php -- app/Filament/Pages/Fornitore/SerialiCatalogo.php -- resources/views/filament/pages/fornitore/seriali-catalogo.blade.php -- app/Filament/Pages/Fornitore/ProdottiCatalogo.php -- resources/views/filament/pages/fornitore/prodotti-catalogo.blade.php -- resources/views/filament/pages/fornitore/ticket-operativi.blade.php -- resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php -- tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php +- app/Console/Commands/SendPasswordResetEmailCommand.php +- app/Console/Commands/TestMailCommand.php +- app/Notifications/NetGesconResetPasswordNotification.php +- app/Models/User.php +- app/Providers/AppServiceProvider.php +- app/Providers/Filament/AdminFilamentPanelProvider.php +- tests/Feature/PasswordResetAndEmailServiceTest.php - skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: -- ./vendor/bin/pest tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php tests/Feature/FornitoreContabilitaSerialiImportTest.php tests/Feature/StrutturaDatabaseAndStorageTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (41 passed, 270 assertions) +- ./vendor/bin/pest tests/Feature/PasswordResetAndEmailServiceTest.php tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php tests/Feature/FornitoreContabilitaSerialiImportTest.php tests/Feature/StrutturaDatabaseAndStorageTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (46 passed, 285 assertions) GATE_STATISTICS: -- TECNOREPAIR_MDB_IMPORT: Risoluzione MDB Windows/Linux e import schede 2025/2026. -- AUTOMATED_WORKFLOWS: Riparato, Rottamato/Ricambi, Reso RMA operativi. -- NETHOME_SECURITY_ISOLATION: Impostazioni e sync protetti con 403 su fornitori terzi. -- TEST_SUITE: 41 test Feature passati (270 asserzioni, 100% pass). +- SMTP_GMAIL_WORKSPACE: Connessione smtp.gmail.com:587 verificata e funzionante. +- PASSWORD_RESET_FLOW: Richiesta da login (/admin-filament/password-reset/request) e da profilo (/admin-filament/profile) operativi. +- SYNC_DISPATCH: Notifiche recapitate istantaneamente senza dipendenza da code background. +- TEST_SUITE: 46 test Feature passati (285 asserzioni, 100% pass). BLOCCO_DATI: no BLOCCO_CONTRATTO: no RISCHI_APERTI: nessuno @@ -73,6 +68,6 @@ ## Output del Giro Operativo ## Prossimo Passo per .200 (Validazione) - Eseguire il checkout del branch `stabilization/205-zero`. -- Eseguire la suite di test Pest (41 passed, 270 assertions). -- Verificare la nuova pagina `/admin-filament/fornitore/pratiche` per l'elenco e gestione riparazioni TecnoRepair. -- Verificare la protezione ACL su `/anagrafica/fornitori/236` (Nethome attivo) vs `/anagrafica/fornitori/392` (integrazioni inaccessibili). +- Eseguire la suite di test Pest (46 passed, 285 assertions). +- Eseguire `php artisan netgescon:test-mail` per verificare la connettività SMTP Google. +- Verificare la ricezione del link di reset e la modifica autonoma della password. diff --git a/tests/Feature/PasswordResetAndEmailServiceTest.php b/tests/Feature/PasswordResetAndEmailServiceTest.php new file mode 100644 index 0000000..5e4df54 --- /dev/null +++ b/tests/Feature/PasswordResetAndEmailServiceTest.php @@ -0,0 +1,70 @@ + 'super-admin', 'guard_name' => 'web']); + Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']); +}); + +test('it verifies mail service configuration defaults for netgescon', function () { + expect(config('mail.mailers.smtp.transport'))->toBe('smtp') + ->and(config('mail.mailers.smtp.host'))->toBe('smtp.gmail.com') + ->and(config('mail.from.address'))->toBe('michele@netgescon.it') + ->and(config('mail.from.name'))->toBe('NetGescon'); +}); + +test('it formats NetGescon password reset notification with Italian text and security links', function () { + $user = User::factory()->create([ + 'name' => 'Michele Test', + 'email' => 'michele.test@netgescon.it', + ]); + $user->assignRole('super-admin'); + + $notification = new NetGesconResetPasswordNotification('test-token-123'); + $notification->url = 'http://192.168.0.205:8000/admin-filament/password-reset/reset?token=test-token-123'; + + $mail = $notification->toMail($user); + + expect($mail->subject)->toBe('NetGescon - Reimpostazione della Password') + ->and($mail->greeting)->toContain('Michele Test') + ->and($mail->actionText)->toBe('Reimposta Password') + ->and($mail->actionUrl)->toBe('http://192.168.0.205:8000/admin-filament/password-reset/reset?token=test-token-123') + ->and(collect($mail->introLines)->join(' '))->toContain('NetGescon') + ->and(collect($mail->outroLines)->join(' '))->toContain('60 minuti'); +}); + +test('it sends password reset email via artisan command for registered user', function () { + Notification::fake(); + + $user = User::factory()->create([ + 'name' => 'Mario Rossi', + 'email' => 'mario.rossi@netgescon.it', + ]); + $user->assignRole('admin'); + + $exitCode = Artisan::call('netgescon:send-password-reset', ['email' => 'mario.rossi@netgescon.it']); + expect($exitCode)->toBe(0); + + Notification::assertSentTo($user, NetGesconResetPasswordNotification::class); +}); + +test('it fails artisan command gracefully when user does not exist', function () { + $exitCode = Artisan::call('netgescon:send-password-reset', ['email' => 'nonexistent@netgescon.it']); + expect($exitCode)->toBe(1); +}); + +test('it mounts Filament password reset request page and profile page routes', function () { + $requestResponse = $this->get('/admin-filament/password-reset/request'); + $requestResponse->assertStatus(200); + + $profileRoute = route('filament.admin-filament.auth.profile'); + expect($profileRoute)->toContain('/admin-filament/profile'); +});