++ cambio password obbligato: al primo accesso del sorvegliante dice "Password temporanea", non "Ripristino da backup"

users.password_temporanea distingue la password provvisoria data
dall'amministratore (creazione del gruppo, "Nuova password temporanea")
dal ripristino da backup. Migrazione 000040; si spegne a password
cambiata, il ripristino e l'import XML la spengono per tutti.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Francesco Picone
2026-10-20 13:52:35 +02:00
parent ec2e7bd7e9
commit d2d6b1bdce
12 changed files with 102 additions and 5 deletions

View File

@@ -210,6 +210,15 @@ Decisioni successive dell'utente (20 ottobre 2026):
obbligatoria `users.email`. XML: niente più `<username>` né obbligatoria `users.email`. XML: niente più `<username>` né
`<disattivato_at>`; da un file vecchio non si importano le utenze `<disattivato_at>`; da un file vecchio non si importano le utenze
disattivate né i gruppi con `<sciolto_at>`. disattivate né i gruppi con `<sciolto_at>`.
- **Primo accesso del sorvegliante**: la pagina del cambio password obbligato
diceva sempre "Ripristino da backup eseguito" (era l'unico motivo
previsto). Ora `users.password_temporanea` (migrazione `000040`, che la
accende per i sorveglianti che non hanno ancora cambiato la password)
distingue la password provvisoria data dall'amministratore — creazione del
gruppo, "Nuova password temporanea" — e la pagina dice "Password
temporanea". Si spegne a password cambiata (pagina obbligatoria o
profilo); il ripristino e l'import XML la spengono per tutti, perché lì il
motivo è il ripristino.
- **Anche i membri si segnalano**: `Proclamatore::gruppo_etichetta` - **Anche i membri si segnalano**: `Proclamatore::gruppo_etichetta`
("Gruppo Zisa" / "Sorvegliante del gruppo Zisa") come badge nell'elenco ("Gruppo Zisa" / "Sorvegliante del gruppo Zisa") come badge nell'elenco
(più il filtro *In un gruppo di servizio*, `filtroStato=gruppo`) e in testa (più il filtro *In un gruppo di servizio*, `filtroStato=gruppo`) e in testa

View File

@@ -745,7 +745,8 @@ docker compose --profile dev up -d --build
fittizia** creata dall'applicazione: iniziale del nome e cognome sul fittizia** creata dall'applicazione: iniziale del nome e cognome sul
dominio `termanager.it` (Francesco Fontana → `ffontana@termanager.it`; se dominio `termanager.it` (Francesco Fontana → `ffontana@termanager.it`; se
è già presa, un numero in coda), e una password temporanea. Si vedono una è già presa, un numero in coda), e una password temporanea. Si vedono una
volta sola; la password va cambiata al primo accesso. L'email non si volta sola; la password va cambiata al primo accesso (la pagina dice
"Password temporanea", non "Ripristino da backup"). L'email non si
chiede, non riceve posta e non va confermata. Due gruppi non possono avere chiede, non riceve posta e non va confermata. Due gruppi non possono avere
lo stesso nome. Se perde la password, l'amministratore ne genera un'altra lo stesso nome. Se perde la password, l'amministratore ne genera un'altra
dalla pagina del gruppo dalla pagina del gruppo
@@ -894,7 +895,7 @@ una revoca ha effetto immediato senza bisogno di ricaricare la pagina.
| Tabella | Descrizione | | Tabella | Descrizione |
|-------------------|--------------------------------------------------------------| |-------------------|--------------------------------------------------------------|
| `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova; `proclamatore_id` lega l'utenza del sorvegliante (email fittizia) al suo proclamatore; sciolto il gruppo l'utenza si elimina | | `users` | Utenti; `password_change_required` blocca l'accesso finché non si sceglie una password nuova (`password_temporanea` dice se il motivo è una password provvisoria data dall'amministratore invece di un ripristino); `proclamatore_id` lega l'utenza del sorvegliante (email fittizia) al suo proclamatore; sciolto il gruppo l'utenza si elimina |
| `settings` | Configurazione singleton | | `settings` | Configurazione singleton |
| `zone` | Zone territoriali | | `zone` | Zone territoriali |
| `tipologie` | Tipologie di territorio | | `tipologie` | Tipologie di territorio |

View File

@@ -9,7 +9,9 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;
/** /**
* Cambio password obbligatorio, imposto dopo il ripristino di un backup. * Cambio password obbligatorio, imposto dopo il ripristino di un backup o al
* primo accesso con una password temporanea data dall'amministratore
* (sorvegliante di gruppo): `password_temporanea` dice quale dei due.
* *
* Volutamente NON e' un componente Livewire: la pagina deve restare * Volutamente NON e' un componente Livewire: la pagina deve restare
* raggiungibile mentre RequirePasswordChange respinge tutto il resto, incluse * raggiungibile mentre RequirePasswordChange respinge tutto il resto, incluse
@@ -27,6 +29,8 @@ class PasswordChangeController extends Controller
// Chi arriva qui spontaneamente (dal profilo) puo' tornare indietro; // Chi arriva qui spontaneamente (dal profilo) puo' tornare indietro;
// chi e' stato dirottato dal middleware no. // chi e' stato dirottato dal middleware no.
'forced' => (bool) $user->password_change_required, 'forced' => (bool) $user->password_change_required,
// Perché: una password temporanea, oppure un ripristino da backup.
'temporanea' => (bool) $user->password_temporanea,
// Il ripristino gira in coda e non ha una sessione in cui lasciare // Il ripristino gira in coda e non ha una sessione in cui lasciare
// un flash; deposita l'esito in cache a nome di chi l'ha avviato e // un flash; deposita l'esito in cache a nome di chi l'ha avviato e
// lo si legge qui, che e' la prima pagina che quella persona vede. // lo si legge qui, che e' la prima pagina che quella persona vede.
@@ -71,6 +75,7 @@ class PasswordChangeController extends Controller
$user->update([ $user->update([
'password' => $validated['password'], 'password' => $validated['password'],
'password_change_required' => false, 'password_change_required' => false,
'password_temporanea' => false,
]); ]);
// La sessione corrente sopravvive al cambio, le altre no: dopo un // La sessione corrente sopravvive al cambio, le altre no: dopo un

View File

@@ -250,6 +250,9 @@ class RestoreBackupArchive implements ShouldQueue
{ {
DB::table('users')->update([ DB::table('users')->update([
'password_change_required' => true, 'password_change_required' => true,
// Il motivo ora è il ripristino, anche per chi aveva una
// password temporanea (pagina del cambio password).
'password_temporanea' => false,
'remember_token' => null, 'remember_token' => null,
]); ]);

View File

@@ -109,7 +109,7 @@ class GruppoShow extends Component
} }
$password = Str::password(12, symbols: false); $password = Str::password(12, symbols: false);
$utente->forceFill(['password' => $password, 'password_change_required' => true])->save(); $utente->forceFill(['password' => $password, 'password_change_required' => true, 'password_temporanea' => true])->save();
activity()->causedBy(auth()->user())->performedOn($utente)->log('password_reimpostata'); activity()->causedBy(auth()->user())->performedOn($utente)->log('password_reimpostata');
$this->passwordTemporanea = $password; $this->passwordTemporanea = $password;

View File

@@ -80,6 +80,7 @@ class Profile extends Component
auth()->user()->update([ auth()->user()->update([
'password' => $this->password, 'password' => $this->password,
'password_change_required' => false, 'password_change_required' => false,
'password_temporanea' => false,
]); ]);
$this->reset(['current_password', 'password', 'password_confirmation']); $this->reset(['current_password', 'password', 'password_confirmation']);

View File

@@ -582,6 +582,7 @@ class XmlExchange extends Component
if ($passwordHash !== '' && ! $eAttore) { if ($passwordHash !== '' && ! $eAttore) {
$meta['password'] = $passwordHash; $meta['password'] = $passwordHash;
$meta['password_change_required'] = true; $meta['password_change_required'] = true;
$meta['password_temporanea'] = false;
} }
if (! $eAttore) { if (! $eAttore) {
$meta['remember_token'] = null; $meta['remember_token'] = null;

View File

@@ -26,6 +26,7 @@ class User extends Authenticatable implements MustVerifyEmail
'email', 'email',
'password', 'password',
'password_change_required', 'password_change_required',
'password_temporanea',
'proclamatore_id', 'proclamatore_id',
]; ];
@@ -40,6 +41,9 @@ class User extends Authenticatable implements MustVerifyEmail
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'password_change_required' => 'boolean', 'password_change_required' => 'boolean',
// Il cambio obbligatorio viene da una password temporanea data
// dall'amministratore, non da un ripristino (pagina del cambio).
'password_temporanea' => 'boolean',
'proclamatore_id' => 'integer', 'proclamatore_id' => 'integer',
]; ];
} }

View File

@@ -65,6 +65,7 @@ class GruppiServizio
'email' => self::emailFittizia($sorvegliante), 'email' => self::emailFittizia($sorvegliante),
'password' => $password, 'password' => $password,
'password_change_required' => true, 'password_change_required' => true,
'password_temporanea' => true,
'proclamatore_id' => $sorvegliante->id, 'proclamatore_id' => $sorvegliante->id,
]); ]);
$utente->assignRole('sorvegliante'); $utente->assignRole('sorvegliante');

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Perché un utente deve cambiare la password: dopo un ripristino da backup
* (com'era finora l'unico caso) o perché l'amministratore gli ha dato una
* password temporanea (sorvegliante di gruppo, alla creazione del gruppo o
* con "Nuova password temporanea"). La pagina del cambio password spiega il
* motivo giusto.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->boolean('password_temporanea')->default(false)->after('password_change_required');
});
// I sorveglianti che non hanno ancora cambiato la password di
// creazione del gruppo: la loro è temporanea, non da ripristino.
DB::table('users')
->whereNotNull('proclamatore_id')
->where('password_change_required', true)
->update(['password_temporanea' => true]);
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('password_temporanea');
});
}
};

View File

@@ -21,7 +21,15 @@
</div> </div>
@endif @endif
@if($forced) @if($forced && $temporanea)
{{-- Primo accesso del sorvegliante, o password nuova data dall'amministratore --}}
<div class="rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-800">
<strong>Password temporanea.</strong>
Stai usando la password provvisoria che ti ha dato l&rsquo;amministratore:
scegline una tua per continuare. Fino ad allora il resto
dell&rsquo;applicazione resta bloccato.
</div>
@elseif($forced)
<div class="rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-800"> <div class="rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-800">
<strong>Ripristino da backup eseguito.</strong> <strong>Ripristino da backup eseguito.</strong>
Il pacchetto di backup contiene le password di tutti gli account, quindi Il pacchetto di backup contiene le password di tutti gli account, quindi

View File

@@ -219,6 +219,33 @@ class GruppiServizioTest extends TestCase
$this->get(route('dashboard'))->assertOk()->assertDontSee('Il mio gruppo')->assertSee('Gruppi di servizio'); $this->get(route('dashboard'))->assertOk()->assertDontSee('Il mio gruppo')->assertSee('Gruppi di servizio');
} }
public function test_al_primo_accesso_il_sorvegliante_cambia_la_password_temporanea(): void
{
$rossi = $this->proclamatore('Rossi', 'Mario');
[$gruppo] = app(GruppiServizio::class)->crea('Zisa', $rossi);
$s = User::where('email', 'mrossi@termanager.it')->sole();
$this->assertTrue($s->password_temporanea);
$this->flushSession();
$this->actingAs($s);
$this->get(route('mio-gruppo'))->assertRedirect(route('password.change'));
$this->get(route('password.change'))->assertOk()
->assertSee('Password temporanea')
->assertDontSee('Ripristino da backup');
$this->post(route('password.change.store'), ['password' => 'una-password-mia-1', 'password_confirmation' => 'una-password-mia-1'])
->assertRedirect(route('dashboard'));
$this->assertFalse($s->fresh()->password_temporanea);
$this->assertFalse($s->fresh()->password_change_required);
$this->get(route('mio-gruppo'))->assertOk();
// "Nuova password temporanea" dalla pagina del gruppo: di nuovo temporanea.
$this->flushSession();
Livewire::actingAs($this->admin)->test(GruppoShow::class, ['gruppo' => $gruppo])->call('nuovaPassword');
$this->assertTrue($s->fresh()->password_temporanea);
$this->assertTrue($s->fresh()->password_change_required);
}
public function test_il_sorvegliante_entra_con_l_email_fittizia_senza_conferma_e_va_al_suo_gruppo(): void public function test_il_sorvegliante_entra_con_l_email_fittizia_senza_conferma_e_va_al_suo_gruppo(): void
{ {
$this->gruppo(); $this->gruppo();