++ fix permessi assegnazioni

This commit is contained in:
Francesco Picone
2026-07-25 23:05:20 +02:00
parent c505708ae2
commit 24a6d3d59f
9 changed files with 63 additions and 3 deletions

View File

@@ -123,6 +123,30 @@ i pulsanti Modifica/Elimina in `campagna-index`/`campagna-show` sono sotto
`@can('settings.manage')`. Nota: `Campagna` non ha soft delete — l'eliminazione `@can('settings.manage')`. Nota: `Campagna` non ha soft delete — l'eliminazione
è definitiva. è definitiva.
Per i **territori** vale un modello leggermente diverso, perché la
visualizzazione va aperta a tutti gli operativi: lista (`territori.index`) e
dettaglio (`territori.show`) sono ora su `territori.assign` (che hanno admin,
assistente e operatore); creazione/modifica/cestino restano su `territori.manage`
(solo admin). Attenzione a due punti:
- **Ordine delle rotte.** Le rotte letterali `territori/create` e
`territori/cestino` devono restare registrate PRIMA del wildcard
`territori/{territorio}`, altrimenti verrebbero catturate dalla rotta di
dettaglio.
- **`TerritorioIndex` perde la copertura del middleware persistente.** Il suo
componente è ora servito da una rotta rilassata (`territori.assign`), quindi i
suoi metodi di mutazione (`toggleActive`, `togglePriority`, `deleteTerritorio`)
NON sono più protetti dal middleware di route e hanno un
`$this->authorize('territori.manage')` interno — senza, un operativo potrebbe
invocarli via `/livewire/update` col pulsante nascosto. Per difesa in
profondità lo stesso check è stato aggiunto anche a `TerritorioCreate::save`,
`TerritorioEdit::save`/`removePdf`, `TerritorioCestino::restore`/`forceDelete`
(che restano comunque su rotte `territori.manage`). I pulsanti
Modifica/toggle/Elimina/Nuovo/Cestino in `territorio-index`/`territorio-show`
sono sotto `@can('territori.manage')`; il link "Dettaglio" resta visibile a
tutti. Il menu laterale mostra "Territori" a chi ha `territori.assign`.
### Export/Import XML: backup completo e round-trip ### Export/Import XML: backup completo e round-trip
`Livewire/Settings/XmlExchange` esporta e reimporta **tutti** i dati gestionali, `Livewire/Settings/XmlExchange` esporta e reimporta **tutti** i dati gestionali,

View File

@@ -12,6 +12,8 @@ class TerritorioCestino extends Component
public function restore(int $id) public function restore(int $id)
{ {
$this->authorize('territori.manage');
$territorio = Territorio::onlyTrashed()->findOrFail($id); $territorio = Territorio::onlyTrashed()->findOrFail($id);
$territorio->restore(); $territorio->restore();
activity()->causedBy(auth()->user()) activity()->causedBy(auth()->user())
@@ -22,6 +24,8 @@ class TerritorioCestino extends Component
public function forceDelete(int $id) public function forceDelete(int $id)
{ {
$this->authorize('territori.manage');
$territorio = Territorio::onlyTrashed()->findOrFail($id); $territorio = Territorio::onlyTrashed()->findOrFail($id);
if ($territorio->assegnazioni()->exists()) { if ($territorio->assegnazioni()->exists()) {

View File

@@ -36,6 +36,8 @@ class TerritorioCreate extends Component
public function save() public function save()
{ {
$this->authorize('territori.manage');
$this->validate(); $this->validate();
$data = [ $data = [

View File

@@ -49,6 +49,8 @@ class TerritorioEdit extends Component
public function save() public function save()
{ {
$this->authorize('territori.manage');
$this->validate(); $this->validate();
$data = [ $data = [
@@ -82,6 +84,8 @@ class TerritorioEdit extends Component
public function removePdf() public function removePdf()
{ {
$this->authorize('territori.manage');
if ($this->territorio->pdf_path) { if ($this->territorio->pdf_path) {
Storage::disk('public')->delete($this->territorio->pdf_path); Storage::disk('public')->delete($this->territorio->pdf_path);
if ($this->territorio->thumbnail_path) { if ($this->territorio->thumbnail_path) {

View File

@@ -96,6 +96,11 @@ class TerritorioIndex extends Component
public function toggleActive(Territorio $territorio) public function toggleActive(Territorio $territorio)
{ {
// La lista è ora visibile agli utenti operativi (territori.assign), ma
// modificare/eliminare i territori resta riservato all'amministratore.
// Il check DEVE stare nel metodo: la rotta index non lo garantisce più.
$this->authorize('territori.manage');
$territorio->update(['attivo' => !$territorio->attivo]); $territorio->update(['attivo' => !$territorio->attivo]);
activity()->causedBy(auth()->user()) activity()->causedBy(auth()->user())
->performedOn($territorio) ->performedOn($territorio)
@@ -105,6 +110,8 @@ class TerritorioIndex extends Component
public function togglePriority(Territorio $territorio) public function togglePriority(Territorio $territorio)
{ {
$this->authorize('territori.manage');
$territorio->update(['prioritario' => !$territorio->prioritario]); $territorio->update(['prioritario' => !$territorio->prioritario]);
activity()->causedBy(auth()->user()) activity()->causedBy(auth()->user())
->performedOn($territorio) ->performedOn($territorio)
@@ -114,6 +121,8 @@ class TerritorioIndex extends Component
public function deleteTerritorio(Territorio $territorio) public function deleteTerritorio(Territorio $territorio)
{ {
$this->authorize('territori.manage');
activity()->causedBy(auth()->user()) activity()->causedBy(auth()->user())
->performedOn($territorio) ->performedOn($territorio)
->log('soft_deleted'); ->log('soft_deleted');

View File

@@ -70,7 +70,7 @@
Home Home
</a> </a>
@can('territori.manage') @can('territori.assign')
<a href="{{ route('territori.index') }}" <a href="{{ route('territori.index') }}"
class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg transition-all duration-150 {{ request()->routeIs('territori.*') ? 'sidebar-active' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900' }}"> class="flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-lg transition-all duration-150 {{ request()->routeIs('territori.*') ? 'sidebar-active' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900' }}">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg> <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>

View File

@@ -4,6 +4,7 @@
<h1 class="text-2xl font-bold text-gray-900">Territori</h1> <h1 class="text-2xl font-bold text-gray-900">Territori</h1>
<p class="text-sm text-gray-500 mt-1">Gestione dei territori della congregazione</p> <p class="text-sm text-gray-500 mt-1">Gestione dei territori della congregazione</p>
</div> </div>
@can('territori.manage')
<div class="mt-4 sm:mt-0 flex gap-2"> <div class="mt-4 sm:mt-0 flex gap-2">
<a href="{{ route('territori.cestino') }}" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition"> <a href="{{ route('territori.cestino') }}" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition">
<svg class="h-4 w-4" style="color:#9ca3af" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg> <svg class="h-4 w-4" style="color:#9ca3af" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
@@ -14,6 +15,7 @@
Nuovo territorio Nuovo territorio
</a> </a>
</div> </div>
@endcan
</div> </div>
{{-- Filters --}} {{-- Filters --}}
@@ -159,6 +161,7 @@
<a href="{{ route('territori.show', $territorio) }}" class="btn-action btn-action-indigo" title="Dettaglio"> <a href="{{ route('territori.show', $territorio) }}" class="btn-action btn-action-indigo" title="Dettaglio">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
</a> </a>
@can('territori.manage')
<a href="{{ route('territori.edit', $territorio) }}" class="btn-action btn-action-gray" title="Modifica"> <a href="{{ route('territori.edit', $territorio) }}" class="btn-action btn-action-gray" title="Modifica">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
</a> </a>
@@ -172,6 +175,7 @@
<button wire:click="deleteTerritorio({{ $territorio->id }})" wire:confirm="Spostare il territorio {{ $territorio->numero }} nel cestino?" class="btn-action btn-action-red" title="Elimina"> <button wire:click="deleteTerritorio({{ $territorio->id }})" wire:confirm="Spostare il territorio {{ $territorio->numero }} nel cestino?" class="btn-action btn-action-red" title="Elimina">
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg> <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
</button> </button>
@endcan
</div> </div>
</td> </td>
</tr> </tr>

View File

@@ -24,10 +24,12 @@
</a> </a>
@endif @endif
@endcan @endcan
@can('territori.manage')
<a href="{{ route('territori.edit', $territorio) }}" class="btn-action btn-action-indigo" style="padding:8px 16px;font-size:14px;border-radius:8px"> <a href="{{ route('territori.edit', $territorio) }}" class="btn-action btn-action-indigo" style="padding:8px 16px;font-size:14px;border-radius:8px">
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg> <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Modifica Modifica
</a> </a>
@endcan
</div> </div>
</div> </div>

View File

@@ -66,12 +66,23 @@ Route::middleware('auth')->group(function () {
// Home Dashboard // Home Dashboard
Route::get('/', Home::class)->name('dashboard'); Route::get('/', Home::class)->name('dashboard');
// Territori // Territori — visualizzazione (lista + dettaglio) per tutti gli utenti
Route::middleware('permission:territori.manage')->group(function () { // operativi, cioè chi può assegnare i territori.
Route::middleware('permission:territori.assign')->group(function () {
Route::get('territori', TerritorioIndex::class)->name('territori.index'); Route::get('territori', TerritorioIndex::class)->name('territori.index');
});
// Gestione territori (crea/modifica/cestino): solo amministratore
// (territori.manage). Le rotte LETTERALI (create/cestino) devono essere
// registrate PRIMA del wildcard {territorio}, altrimenti "create"/"cestino"
// verrebbero catturate dalla rotta di dettaglio.
Route::middleware('permission:territori.manage')->group(function () {
Route::get('territori/create', TerritorioCreate::class)->name('territori.create'); Route::get('territori/create', TerritorioCreate::class)->name('territori.create');
Route::get('territori/cestino', TerritorioCestino::class)->name('territori.cestino'); Route::get('territori/cestino', TerritorioCestino::class)->name('territori.cestino');
});
Route::middleware('permission:territori.assign')->group(function () {
Route::get('territori/{territorio}', TerritorioShow::class)->name('territori.show'); Route::get('territori/{territorio}', TerritorioShow::class)->name('territori.show');
});
Route::middleware('permission:territori.manage')->group(function () {
Route::get('territori/{territorio}/edit', TerritorioEdit::class)->name('territori.edit'); Route::get('territori/{territorio}/edit', TerritorioEdit::class)->name('territori.edit');
}); });