diff --git a/.env b/.env index 96520bb6..10eaf2ea 100644 --- a/.env +++ b/.env @@ -7,8 +7,12 @@ APP_PROTOCOL=https FORCE_HTTPS=true TRUSTED_PROXIES=* -GOOGLE_CLIENT_ID=980774223097-o5h7a6kepvg69te34fof2otn7ibi9uha.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=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 +#GOOGLE_CLIENT_ID=980774223097-o5h7a6kepvg69te34fof2otn7ibi9uha.apps.googleusercontent.com +#GOOGLE_CLIENT_SECRET=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 + +GOOGLE_CLIENT_ID=416164569291-mss5favrg1qdmbk343plaj1898g3200f.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 + APP_LOCALE=it diff --git a/MEMORY.md b/MEMORY.md index 6cf9d2e3..6f0688f9 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -3,6 +3,28 @@ ## Obiettivo App gestionale Laravel 13 con AdminLTE 4 per gestione Persone e Gruppi. +## Riassunto Generale + +| Area | Stato | Ultima modifica | +|------|-------|-----------------| +| **Core** (Individui, Gruppi, Eventi, Documenti, Mailing) | ✅ Completo | CRUD, relazioni, filtri, paginazione, ACL, export | +| **Tag System** | ✅ Completo | morphToMany su 5 entità, filtri OR/AND, report, ricerca unificata | +| **Viste Colonne** | ✅ Completo | Per-User column visibility/width/order, resize/drag-reorder, print | +| **Email (IMAP/SMTP)** | ✅ Completo | Sync messaggi, firma, allegati, mittenti multipli, XOAUTH2 OAuth | +| **Google OAuth 2.0** | ✅ Implementato | Unified (Email/Drive/Calendar), UI impostazioni, XOAUTH2 SMTP+IMAP | +| **Repository Remoti** | ✅ Completo | WebDAV, Google Drive, OAuth | +| **Calendario** | ✅ Completo | Google Calendar, CalDAV, sync bidirezionale, pulsante sync | +| **Report** | ✅ Completo | Custom report con colonne/filtri salvabili, tag filter | +| **Help / Documentazione** | ✅ Completo | Tab Google Drive + Calendar in help page, PDF export | +| **Distribuzione** | ✅ Completo | build-dist.sh, fix path/cache/permessi/symlink | + +### Cronologia modifiche principali +- **2026-06-07**: Fix colonne mancanti (eventi, gruppo_individuo) +- **2026-06-08**: build-dist.sh fix, logo remote fix, 18 migration guardie, help Calendar, sync pulsante, form annidato fix, checkbox hidden fallback, DiagnoseEmail command +- **2026-06-09**: Tag completato (MailingList, Report, Ricerca), @stack scripts fix, storeCustom tag_filter fix, MailingList edit JS fix +- **2026-06-10**: Per-User Column Views, ColumnManager JS, editVista data-* fix, 405 AJAX fix +- **2026-06-17**: Google OAuth 2.0 unificato (revert Socialite, unified OAuth per Email/Drive/Calendar, XOAUTH2 SMTP+IMAP, UI impostazioni) + ## Funzionalità Implementate ### Page-length selector @@ -380,3 +402,179 @@ if (!empty($contatto['individuo_id'])) { create } **Fix**: 1. `VistaReportController@update` (linea 101-103): aggiunto `if ($request->expectsJson()) { return response()->json([...]); }` — così il fetch riceve JSON 200, non un redirect 302. 2. Fetch headers in `table-settings.blade.php`: aggiunto `'Accept': 'application/json'` — necessario perché `expectsJson()` controlla l'header `Accept`, non `Content-Type`. + +--- + +## 2026-06-17 — Google OAuth 2.0 Unificato (Email/Drive/Calendar) + +**Obiettivo**: Implementare OAuth 2.0 centralizzato per tutti i servizi Google (Gmail email, Google Drive, Google Calendar), con UI nelle impostazioni e supporto XOAUTH2 per SMTP/IMAP via Symfony Mailer + ImapEngine. Sistema password attuale deve funzionare in parallelo. + +### Cosa è stato fatto + +**Enum** (`app/Enums/GoogleService.php`): +- Definisce 3 casi: `Email`, `Drive`, `Calendar` +- Metodi: `scopes()` (array scopes OAuth per servizio), `label()` (nome italiano), `icon()`, `description()` (testo help) + +**Migration** (`2026_06_17_000001_create_google_oauth_connections_table.php`): +- Tabella `google_oauth_connections`: id, user_id (FK), service, email, access_token, refresh_token, expires_at, timestamps +- Unique constraint `[user_id, service, email]` +- Già eseguita in batch 39 + +**Migration** (`2026_06_17_000002_add_oauth_columns.php`): +- `email_settings`: aggiunti `auth_method` (varchar 20, default 'password') + `google_oauth_connection_id` (FK → google_oauth_connections, nullOnDelete) +- `sender_accounts`: stesse colonne +- Guardia `Schema::hasColumn` per idempotenza +- Già eseguita in batch 39 + +**Model** (`app/Models/GoogleOAuthConnection.php`): +- `$fillable`: user_id, service, email, access_token, refresh_token, expires_at +- `$casts`: expires_at → datetime +- `user()`: BelongsTo User +- `isExpired()`: controllo scadenza token +- `refresh()`: refresh automatico via GoogleOAuthService +- `getValidAccessToken()`: refresh se expired, ritorna token valido + +**Service** (`app/Services/GoogleOAuthService.php`): +- `buildClient()`: configura Google_Client con client_id/secret/redirect_uri, access_type=offline, prompt=consent +- `getAuthUrl(service)`: URL di autorizzazione con scopes specifici per servizio +- `handleCallback(code, service)`: scambia code per token, crea/aggiorna GoogleOAuthConnection +- `getAccessToken(connection)`: cache 5 min, refresh automatico se expired +- `refreshToken(connection)`: fetch nuovo token via refresh_token, elimina se refresh fallisce +- `revoke(connection)`: revoca token Google, elimina record DB +- `createXoAuth2SmtpTransport(email, token)`: SMTP transport con solo XOAuth2Authenticator (evita tentativi PLAIN/LOGIN) +- `getConnectionStatus()`: restituisce array con stato connessioni per ogni servizio + +**Controller** (`app/Http/Controllers/GoogleOAuthController.php`): +- `redirect(service)`: reindirizza a Google OAuth +- `callback(request)`: gestisce risposta Google, crea connessione, redirect a impostazioni con success/error +- `revoke(connection)`: revoca connessione (solo proprietario) +- `status()`: JSON con stato connessioni + +**Config/Route changes**: +- `config/services.php`: sezione `google` con `client_id`, `client_secret`, `redirect` (usa `config('app.url')` per compatibilità CLI) +- `routes/web.php`: 4 nuove route (`google-oauth.redirect`, `google-oauth.callback`, `google-oauth.revoke`, `google-oauth.status`) + `use App\Http\Controllers\GoogleOAuthController` + +**View** (`resources/views/impostazioni/_google-oauth.blade.php`): +- Card con lista servizi (Gmail, Drive, Calendar), badge connesso/non connesso, pulsanti Connetti/Disconnetti +- JS: auto-click tab se `?tab=google` in URL (dopo callback) + +**Modifiche EmailSetting.php**: +- `$fillable` + `auth_method`, `google_oauth_connection_id` +- `$casts` + `google_oauth_connection_id` → integer +- `googleOAuthConnection()`: BelongsTo relationship +- `getImapConfig()`: se auth_method=oauth, imposta `authentication => 'oauth'` e password = access token +- `getDecryptedPassword()`: se OAuth, risolve token invece di decrittare password +- `getDecryptedSmtpPassword()`: stessa logica per SMTP +- `resolveOAuthToken()`: carica relazione + chiama GoogleOAuthService::getAccessToken +- `getImapClient()`: passa `authentication => 'oauth'` se auth_method=oauth +- `getSmtpMailer()`: se OAuth → `buildOAuthSmtpMailer()` (XOAuth2 solo), altrimenti DSN normale +- `buildOAuthSmtpMailer()`: EsmtpTransport + XOAuth2Authenticator +- `buildSmtpDsn()`: estratto da EmailSettingsController (password decrittata) + +**Modifiche SenderAccount.php**: +- Stesso pattern: `auth_method`, `google_oauth_connection_id`, relationship, `resolveOAuthToken()` +- `getDecryptedPassword()`: OAuth-aware +- `sendEmail()`: se OAuth → `buildOAuthMailer()` (XOAuth2), altrimenti DSN +- `buildOAuthMailer()`: EsmtpTransport + XOAuth2Authenticator + +**Modifiche EmailSettingsController.php**: +- `index()`: passa `$googleStatus` → `GoogleOAuthService::getConnectionStatus()` +- `save()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id` +- `testSmtp()`: ora usa `$settings->getSmtpMailer()` invece di costruire DSN manualmente +- `senderStore()`/`senderUpdate()`: validazione + salvataggio `auth_method`, `google_oauth_connection_id` + +**Modifiche impostazioni/index.blade.php**: +- Sidebar link "Google" (icona `fab fa-google`) prima del link "Calendario" +- Tab-pane con `@include("impostazioni._google-oauth")` prima del tab calendario + +### Note tecniche +- **XOAUTH2**: Symfony Mailer ha già `XOAuth2Authenticator` in vendor (`vendor/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php`), invia `AUTH XOAUTH2 user=\1auth=Bearer \1\1` +- **ImapEngine**: già supporta `'authentication' => 'oauth'` in config → `AUTHENTICATE XOAUTH2 ` +- **google/apiclient**: presente in vendor, usato per Google_Client (buildClient, fetchAccessTokenWithAuthCode, refresh, revoke). `Google\Service\Gmail` NON disponibile ma non necessario (XOAUTH2 bypassa API REST) +- **Permessi filesystem**: nuovi file copiati con sudo (www-data:www-data, 644), index.blade.php modificato con sed +- **refresh token**: richiede `access_type=offline` + `prompt=consent` + `includeGrantedScopes=true` per garantire refresh token sempre al primo auth +- **config('app.url')** in services.php invece di `url()` helper (url() fallisce in CLI context) + +### 2026-06-17 — Bug Fix: Tag massivo e per-riga per Eventi + Documenti + +**Problemi riscontrati**: +1. Eventi: modal mass tag HTML corrotto (`×` → ` @include('partials._tag-selector', ['selectedTags' => $selectedTags ?? [], 'label' => 'Etichette']) - -
- - - - - - - - - - - - - - - - - - @if($documento->user) - - - - - @endif -
File:{{ $documento->file_path }}
Tipo MIME:{{ $documento->mime_type ?? '-' }}
Dimensione:{{ $documento->dimensione ? number_format($documento->dimensione / 1024, 1) . ' KB' : '-' }}
Creato:{{ $documento->created_at->format('d/m/Y H:i') }}
Utente:{{ $documento->user->name }}
- @if($documento->file_path) -
- - Scarica - - @endif -
- diff --git a/resources/views/documenti/index.blade.php b/resources/views/documenti/index.blade.php index 37294264..7278d9ff 100644 --- a/resources/views/documenti/index.blade.php +++ b/resources/views/documenti/index.blade.php @@ -18,6 +18,18 @@ $tableColumnsJson = json_encode($tableColumns ?? []); @endsection @section('content') +@if(session('success')) +
+ + {{ session('success') }} +
+@endif +@if(session('error')) +
+ + {{ session('error') }} +
+@endif
{{-- Sidebar cartelle --}}
@@ -133,7 +145,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
@csrf - @if($canDeleteDocumenti) + @if($canWriteDocumenti)
Azioni: - @endif - @if($canDeleteDocumenti) + @if($canWriteDocumenti) @@ -213,13 +225,13 @@ $tableColumnsJson = json_encode($tableColumns ?? []);
- @if($canDeleteDocumenti) + @if($canWriteDocumenti)
@endif
- {{ $documento->nome_file }} + {{ $documento->nome_file }}
@@ -268,8 +280,11 @@ $tableColumnsJson = json_encode($tableColumns ?? []); + @endif - @if($canDeleteDocumenti) + @if($canWriteDocumenti) @@ -299,7 +314,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []); - @if($canDeleteDocumenti) + @if($canWriteDocumenti) @@ -316,7 +331,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []); @foreach($sottoCartelle as $cartella) - @if($canDeleteDocumenti) + @if($canWriteDocumenti) @endif - @if($canDeleteDocumenti) + @if($canWriteDocumenti) @@ -466,8 +481,11 @@ $tableColumnsJson = json_encode($tableColumns ?? []); + @endif - @if($canDeleteDocumenti) + @if($canWriteDocumenti) @@ -635,6 +653,10 @@ $tableColumnsJson = json_encode($tableColumns ?? []); @if($currentRepo) +
+ + @include("partials._tag-selector", ["selectedTags" => [], "label" => "Tag"]) +

Caricamento su repository remoto: {{ $currentRepo->nome }}

@endif @@ -1287,11 +1309,38 @@ document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', func document.getElementById('massMoveCount').textContent = ids.length; }); -document.getElementById('massTagModal')?.addEventListener('show.bs.modal', function() { - const ids = getSelectedIds(); +function showMassTagModal() { + var ids = getSelectedIds(); + + if (ids.length === 0) { + const preSelected = document.querySelector('.doc-checkbox[data-pre-selected="true"]'); + if (preSelected) { + ids = [preSelected.value]; + preSelected.removeAttribute('data-pre-selected'); + } + } + + if (ids.length === 0) { + alert('Seleziona almeno un documento'); + return; + } + document.getElementById('massTagIds').value = ids.join(','); document.getElementById('massTagCount').textContent = ids.length; -}); + $('#massTagModal').modal('show'); +} + +function openSingleTag(docId) { + document.querySelectorAll('.doc-checkbox').forEach(cb => cb.checked = false); + + let cb = document.querySelector('.doc-checkbox[value="' + docId + '"]'); + if (cb) { + cb.checked = true; + cb.dispatchEvent(new Event('change')); + } + + showMassTagModal(); +} document.getElementById('massMoveNewFolderBtn')?.addEventListener('click', function() { Swal.fire({ diff --git a/resources/views/eventi/index.blade.php b/resources/views/eventi/index.blade.php index 0000e9bf..e058495a 100644 --- a/resources/views/eventi/index.blade.php +++ b/resources/views/eventi/index.blade.php @@ -19,6 +19,12 @@ $entityType = $entityType ?? 'eventi'; {{ session('success') }} @endif +@if(session('error')) +
+ + {{ session('error') }} +
+@endif
@@ -33,12 +39,15 @@ $entityType = $entityType ?? 'eventi'; + @if($canWriteEventi) + + @endif @if($canDeleteEventi) -
- -
+ @endif @if($canWriteEventi) @@ -86,7 +95,7 @@ $entityType = $entityType ?? 'eventi';
@@ -341,7 +356,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []); @endif - @if($canDeleteDocumenti) + @if($canWriteDocumenti) @@ -362,7 +377,7 @@ $tableColumnsJson = json_encode($tableColumns ?? []); }; @endphp
- @if($canDeleteEventi) + @if($canWriteEventi)
@@ -187,7 +196,7 @@ $entityType = $entityType ?? 'eventi'; @forelse($eventi as $evento)
- @if($canDeleteEventi) + @if($canWriteEventi)
@@ -277,6 +286,9 @@ $entityType = $entityType ?? 'eventi'; + @csrf @method('DELETE') +
+ + @csrf + + + + + +