feat(auth): attivazione servizio email google workspace netgescon e flusso cambio password autonomo

This commit is contained in:
michele 2026-09-11 18:09:26 +02:00
parent 00d4417dc7
commit a0c441638e
8 changed files with 284 additions and 50 deletions

View File

@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Notifications\NetGesconResetPasswordNotification;
use Filament\Facades\Filament;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Password;
class SendPasswordResetEmailCommand extends Command
{
protected $signature = 'netgescon:send-password-reset {email : Indirizzo email dell\'utente}';
protected $description = 'Invia l\'email con il link di reimpostazione password a un utente registrato su NetGescon';
public function handle(): int
{
$email = trim((string) $this->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;
}
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class TestMailCommand extends Command
{
protected $signature = 'netgescon:test-mail {email=netgescon@gmail.com : Destinatario di test}';
protected $description = 'Verifica la connessione SMTP Google Workspace / NetGescon e invia un\'email di diagnostica';
public function handle(): int
{
$recipient = trim((string) $this->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;
}
}
}

View File

@ -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();

View File

@ -0,0 +1,50 @@
<?php
namespace App\Notifications;
use Filament\Auth\Notifications\ResetPassword as FilamentResetPassword;
use Filament\Facades\Filament;
use Illuminate\Notifications\Messages\MailMessage;
class NetGesconResetPasswordNotification extends FilamentResetPassword
{
/**
* Non implementa ShouldQueue per garantire l'invio immediato e sincrono via SMTP,
* eliminando il rischio di blocco nella tabella jobs in assenza di daemon queue:work.
*/
public function toMail($notifiable): MailMessage
{
$url = $this->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));
}
}

View File

@ -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
);
}
/**

View File

@ -42,6 +42,7 @@ public function panel(Panel $panel): Panel
->login(\App\Filament\Auth\Login::class)
->passwordReset()
->registration()
->profile()
->colors([
'primary' => Color::Amber,
])

View File

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

View File

@ -0,0 +1,70 @@
<?php
use App\Models\User;
use App\Notifications\NetGesconResetPasswordNotification;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Password;
use Spatie\Permission\Models\Role;
beforeEach(function () {
Role::firstOrCreate(['name' => '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');
});