++ Utenti: nuova password temporanea per il sorvegliante di gruppo
Sulla riga del sorvegliante, che prima non aveva azioni: "Nuova password temporanea" (mostrata una volta, da cambiare al primo accesso) e "Apri il gruppo". La logica sta in GruppiServizio::nuovaPasswordTemporanea(), usata anche dalla pagina del gruppo; azzera anche il "ricordami". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
18
HANDOFF.md
18
HANDOFF.md
@@ -111,6 +111,17 @@ lavoro nuovi, stesso master), `php-fpm -i` riporta
|
||||
`session.upload_progress.enabled => Off` e `session.use_strict_mode => On`,
|
||||
login 200 su entrambi.
|
||||
|
||||
### Password del sorvegliante dal pannello Utenti (21 ottobre 2026, solo su dev)
|
||||
|
||||
Richiesta dell'utente: poter reimpostare la password del sorvegliante dal
|
||||
pannello di amministrazione. Prima si poteva solo dalla pagina del gruppo; in
|
||||
*Utenti* la sua riga non aveva azioni. Ora ha "Nuova password temporanea" (con
|
||||
conferma) e "Apri il gruppo"; la password compare una volta sola in cima
|
||||
all'elenco, con l'email, fino a "Ho annotato la password". La logica è una
|
||||
sola, `GruppiServizio::nuovaPasswordTemporanea()`, usata anche da
|
||||
`GruppoShow`; rifiuta gli utenti che non sono sorveglianti (per loro c'è il
|
||||
modulo di modifica). Test in `GruppiServizioTest`.
|
||||
|
||||
### Anteprima dal numero in "Il mio gruppo" (21 ottobre 2026, solo su dev)
|
||||
|
||||
Richiesta dell'utente: anche il sorvegliante deve poter vedere l'anteprima del
|
||||
@@ -187,7 +198,12 @@ qui sotto **com'è oggi**, in fondo la storia.
|
||||
e dal profilo l'email non si cambia. Ruolo `sorvegliante`, con il solo
|
||||
permesso `gruppo.gestisci`; non compare nella pagina *Utenti*, dove le
|
||||
utenze legate a un proclamatore non si modificano né si eliminano.
|
||||
"Nuova password temporanea" dalla pagina del gruppo.
|
||||
"Nuova password temporanea" dalla pagina del gruppo o dalla riga del
|
||||
sorvegliante nel pannello *Utenti* (stessa logica,
|
||||
`GruppiServizio::nuovaPasswordTemporanea()`: password provvisoria, cambio
|
||||
obbligato, "ricordami" azzerato, log `password_reimpostata`; le sessioni
|
||||
aperte si chiudono perché cambia l'hash). In *Utenti* è l'unica azione su
|
||||
quell'utenza, con "Apri il gruppo".
|
||||
- **Cosa vede**: `LimitaSorvegliante` (gruppo `web`) lascia a chi ha **solo**
|
||||
quel ruolo `mio-gruppo*`, `assegnazioni.rientra` (solo per il suo gruppo:
|
||||
`Rientra::autorizza`), profilo, cambio password e le azioni Livewire —
|
||||
|
||||
@@ -759,7 +759,10 @@ docker compose --profile dev up -d --build
|
||||
"Password temporanea", non "Ripristino da backup"). L'email non si
|
||||
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
|
||||
dalla pagina del gruppo
|
||||
temporanea dalla pagina del gruppo o dal pannello *Utenti* ("Nuova password
|
||||
temporanea" sulla riga del sorvegliante): si vede una volta sola, va
|
||||
cambiata al primo accesso, e le sessioni aperte con quella vecchia si
|
||||
chiudono
|
||||
- I territori del pacchetto si aggiungono in blocco tra quelli in reparto,
|
||||
**in ordine di priorità** come "Da assegnare" nella Home (prima i
|
||||
prioritari, poi quelli fermi da più tempo), con ricerca, filtri per zona,
|
||||
@@ -858,7 +861,8 @@ L'amministratore ha un bypass completo (`Gate::before` in `AppServiceProvider`).
|
||||
Il menu laterale mostra solo le voci per cui l'utente ha i permessi.
|
||||
|
||||
Il ruolo **sorvegliante** non si assegna dalla pagina *Utenti*: nasce creando
|
||||
un gruppo di servizio e sparisce sciogliendolo. Chi ha solo quel ruolo è
|
||||
un gruppo di servizio e sparisce sciogliendolo. Della sua utenza, in *Utenti*,
|
||||
si può solo generare una nuova password temporanea (e aprire il gruppo). Chi ha solo quel ruolo è
|
||||
confinato da `LimitaSorvegliante` alla pagina del proprio gruppo, al rientro
|
||||
dei territori del gruppo e al profilo; le sue azioni ricontrollano che
|
||||
territorio e assegnazione siano del suo gruppo.
|
||||
|
||||
@@ -157,11 +157,7 @@ class GruppoShow extends Component
|
||||
return;
|
||||
}
|
||||
|
||||
$password = Str::password(12, symbols: false);
|
||||
$utente->forceFill(['password' => $password, 'password_change_required' => true, 'password_temporanea' => true])->save();
|
||||
activity()->causedBy(auth()->user())->performedOn($utente)->log('password_reimpostata');
|
||||
|
||||
$this->passwordTemporanea = $password;
|
||||
$this->passwordTemporanea = app(GruppiServizio::class)->nuovaPasswordTemporanea($utente);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use App\Models\GruppoServizio;
|
||||
use App\Models\User;
|
||||
use App\Services\GruppiServizio;
|
||||
use Illuminate\Validation\Rule;
|
||||
use InvalidArgumentException;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
use Spatie\Permission\Models\Role;
|
||||
@@ -22,6 +25,17 @@ class UsersIndex extends Component
|
||||
#[Locked]
|
||||
public array $availableRoles = [];
|
||||
|
||||
/**
|
||||
* Nuova password temporanea di un sorvegliante di gruppo, mostrata una
|
||||
* volta sola (come nella pagina del gruppo). Locked: le scrive solo
|
||||
* nuovaPasswordSorvegliante().
|
||||
*/
|
||||
#[Locked]
|
||||
public ?int $passwordPerUtenteId = null;
|
||||
|
||||
#[Locked]
|
||||
public ?string $passwordTemporanea = null;
|
||||
|
||||
public ?int $editingUserId = null;
|
||||
public string $editName = '';
|
||||
public string $editEmail = '';
|
||||
@@ -238,10 +252,39 @@ class UsersIndex extends Component
|
||||
$this->avvisa('Utente cancellato. I log sono stati preservati.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Il sorvegliante ha perso la password: una temporanea nuova, da cambiare
|
||||
* al primo accesso. È l'unica cosa che di quell'utenza si fa da qui; il
|
||||
* resto passa dal gruppo (gestitaDaiGruppi).
|
||||
*/
|
||||
public function nuovaPasswordSorvegliante(int $userId): void
|
||||
{
|
||||
$this->authorize('settings.manage');
|
||||
$user = User::query()->findOrFail($userId);
|
||||
|
||||
try {
|
||||
$this->passwordTemporanea = app(GruppiServizio::class)->nuovaPasswordTemporanea($user);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->avvisaErrore($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->passwordPerUtenteId = $user->id;
|
||||
}
|
||||
|
||||
public function chiudiPassword(): void
|
||||
{
|
||||
$this->reset(['passwordPerUtenteId', 'passwordTemporanea']);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.users-index', [
|
||||
'users' => User::query()->with('roles')->orderBy('name')->get(),
|
||||
// Per ogni sorvegliante il suo gruppo, per il collegamento.
|
||||
'gruppiDeiSorveglianti' => GruppoServizio::pluck('id', 'sorvegliante_id'),
|
||||
'utentePassword' => $this->passwordPerUtenteId ? User::find($this->passwordPerUtenteId) : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,32 @@ class GruppiServizio
|
||||
return [$gruppo, $password];
|
||||
}
|
||||
|
||||
/**
|
||||
* Una password temporanea nuova per il sorvegliante che l'ha persa: da
|
||||
* cambiare al primo accesso ("Password temporanea"). Le sessioni aperte
|
||||
* con quella vecchia si chiudono alla richiesta successiva
|
||||
* (AuthenticateSession: è cambiato l'hash), il "ricordami" si azzera.
|
||||
* Restituisce la password, da mostrare una volta sola.
|
||||
*/
|
||||
public function nuovaPasswordTemporanea(User $utente): string
|
||||
{
|
||||
if (! $utente->proclamatore_id || ! $utente->isSorvegliante()) {
|
||||
throw new InvalidArgumentException("{$utente->name} non è il sorvegliante di un gruppo.");
|
||||
}
|
||||
|
||||
$password = Str::password(12, symbols: false);
|
||||
$utente->forceFill([
|
||||
'password' => $password,
|
||||
'password_change_required' => true,
|
||||
'password_temporanea' => true,
|
||||
'remember_token' => null,
|
||||
])->save();
|
||||
|
||||
activity()->causedBy(auth()->user())->performedOn($utente)->log('password_reimpostata');
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* L'email fittizia del sorvegliante, con cui entra: iniziale del nome e
|
||||
* cognome, minuscoli, senza accenti, spazi né apostrofi, sul dominio
|
||||
|
||||
@@ -57,6 +57,19 @@
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Utenti esistenti</h2>
|
||||
|
||||
@if($passwordTemporanea && $utentePassword)
|
||||
<div class="mb-4 rounded-xl border-2 border-green-200 bg-green-50 p-4 text-sm text-green-900" role="status">
|
||||
<p>Nuova password temporanea per <strong>{{ $utentePassword->name }}</strong>, da cambiare al primo accesso. Si vede <strong>solo ora</strong>:</p>
|
||||
<dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
|
||||
<dt class="text-green-700">Email</dt><dd><code class="select-all">{{ $utentePassword->email }}</code></dd>
|
||||
<dt class="text-green-700">Password</dt><dd><code class="select-all text-base">{{ $passwordTemporanea }}</code></dd>
|
||||
</dl>
|
||||
<div class="mt-3">
|
||||
<x-btn variante="ghost" dimensione="sm" wire:click="chiudiPassword">Ho annotato la password</x-btn>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm table-cards">
|
||||
<thead class="bg-gray-50">
|
||||
@@ -130,10 +143,16 @@
|
||||
</td>
|
||||
<td class="px-4 py-2 cell-actions">
|
||||
@if($user->proclamatore_id)
|
||||
{{-- Utenza di un sorvegliante di gruppo: si gestisce dal gruppo. --}}
|
||||
<span class="text-xs text-gray-500">
|
||||
Sorvegliante: si gestisce da Gruppi di servizio
|
||||
</span>
|
||||
{{-- Utenza di un sorvegliante di gruppo: si gestisce dal gruppo;
|
||||
da qui solo la password, se l'ha persa. --}}
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm"
|
||||
wire:click="nuovaPasswordSorvegliante({{ $user->id }})"
|
||||
wire:confirm="Generare una nuova password temporanea per {{ $user->name }}? Quella attuale smette di funzionare.">Nuova password temporanea</x-btn>
|
||||
@if(isset($gruppiDeiSorveglianti[$user->proclamatore_id]))
|
||||
<x-btn data-ritorno variante="ghost" dimensione="sm" :href="route('gruppi.show', $gruppiDeiSorveglianti[$user->proclamatore_id])">Apri il gruppo</x-btn>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="flex gap-2">
|
||||
<x-btn variante="secondary" tono="tenue" dimensione="sm" wire:click="startEdit({{ $user->id }})">Modifica</x-btn>
|
||||
|
||||
@@ -155,6 +155,33 @@ class GruppiServizioTest extends TestCase
|
||||
$this->assertNotNull($sorvegliante->fresh());
|
||||
}
|
||||
|
||||
public function test_dal_pannello_utenti_si_da_al_sorvegliante_una_nuova_password_temporanea(): void
|
||||
{
|
||||
['sorvegliante' => $s, 'gruppo' => $gruppo] = $this->gruppo();
|
||||
|
||||
$pagina = Livewire::test(UsersIndex::class)
|
||||
->assertSee('Nuova password temporanea')
|
||||
->assertSee(route('gruppi.show', $gruppo), false)
|
||||
->call('nuovaPasswordSorvegliante', $s->id);
|
||||
|
||||
// Si vede una volta, ed è quella vera: temporanea, da cambiare.
|
||||
$password = $pagina->get('passwordTemporanea');
|
||||
$this->assertNotEmpty($password);
|
||||
$pagina->assertSee($password)->assertSee('mrossi@termanager.it');
|
||||
$s->refresh();
|
||||
$this->assertTrue(\Illuminate\Support\Facades\Hash::check($password, $s->password));
|
||||
$this->assertTrue($s->password_change_required);
|
||||
$this->assertTrue($s->password_temporanea);
|
||||
$this->assertSame(1, \Spatie\Activitylog\Models\Activity::where('description', 'password_reimpostata')->count());
|
||||
|
||||
$pagina->call('chiudiPassword')->assertDontSee($password);
|
||||
|
||||
// Solo per i sorveglianti: gli altri utenti hanno il loro modulo.
|
||||
$pagina->call('nuovaPasswordSorvegliante', $this->admin->id)
|
||||
->assertDispatched('avviso', fn ($nome, $parametri) => $parametri['tipo'] === 'errore');
|
||||
$this->assertTrue(\Illuminate\Support\Facades\Hash::check('password-di-prova', $this->admin->fresh()->password));
|
||||
}
|
||||
|
||||
// ─── Pacchetto ──────────────────────────────────────────────
|
||||
|
||||
public function test_il_pacchetto_non_e_un_assegnazione_e_non_e_in_reparto(): void
|
||||
|
||||
Reference in New Issue
Block a user