Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 557bf3fcdf | |||
| f2ea5e160b | |||
| def8d22544 | |||
| c5127da750 | |||
| baea181159 | |||
| d2084b9d07 | |||
| 3b2ad1a0d5 | |||
| 92ba47e99f | |||
| 9d649180a6 | |||
| 6909e21b67 | |||
| c333bbce4e | |||
| 6001c2e3b8 | |||
| 69f4d6a602 | |||
| 7973a94498 |
@@ -2,10 +2,18 @@ APP_NAME=Glastree
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
|
||||
APP_DEBUG=true
|
||||
APP_URL=
|
||||
APP_PROTOCOL=https
|
||||
FORCE_HTTPS=true
|
||||
TRUSTED_PROXIES=*
|
||||
APP_URL=http://glastree.soon.it
|
||||
APP_PROTOCOL=http
|
||||
FORCE_HTTPS=false
|
||||
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
|
||||
|
||||
#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
|
||||
APP_FALLBACK_LOCALE=it
|
||||
@@ -30,9 +38,11 @@ DB_DATABASE=glastree
|
||||
DB_USERNAME=glastree
|
||||
DB_PASSWORD=glastree
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_SECURE_COOKIE=false
|
||||
SESSION_SAME_SITE=lax
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
|
||||
+9
-3
@@ -1,8 +1,12 @@
|
||||
APP_NAME="Glastree"
|
||||
APP_ENV=local
|
||||
APP_ENV=production
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_DEBUG=false
|
||||
APP_URL=http://localhost
|
||||
APP_PROTOCOL=https
|
||||
APP_SUBFOLDER=
|
||||
FORCE_HTTPS=false
|
||||
TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1"
|
||||
|
||||
APP_LOCALE=it
|
||||
APP_FALLBACK_LOCALE=en
|
||||
@@ -24,9 +28,11 @@ DB_DATABASE=glastree
|
||||
DB_USERNAME=glastree
|
||||
DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_SECURE_COOKIE=false
|
||||
SESSION_SAME_SITE=lax
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"version":2,"defects":{"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":7,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":7,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":8,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":8,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":8,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":8,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":7},"times":{"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_true_when_tipo_is_link":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_upload":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_allegato":0.001,"Tests\\Unit\\DocumentoLinkTest::test_is_link_returns_false_when_tipo_is_null":0.002,"Tests\\Unit\\DocumentoLinkTest::test_url_is_fillable":0.001,"Tests\\Unit\\DocumentoLinkTest::test_link_doc_has_no_file_fields":0.001,"Tests\\Feature\\DocumentoLinkTest::test_admin_can_create_link_document":0.064,"Tests\\Feature\\DocumentoLinkTest::test_create_link_requires_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_validates_url_format":0.007,"Tests\\Feature\\DocumentoLinkTest::test_create_link_does_not_require_file":0.006,"Tests\\Feature\\DocumentoLinkTest::test_create_link_sets_tipo_to_link":0.014,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_individuo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_create_link_with_visibility_gruppo":0.008,"Tests\\Feature\\DocumentoLinkTest::test_download_link_redirects_to_url":0.005,"Tests\\Feature\\DocumentoLinkTest::test_preview_link_returns_fallback_view":0.009,"Tests\\Feature\\DocumentoLinkTest::test_update_link_url":0.008,"Tests\\Feature\\DocumentoLinkTest::test_update_link_preserves_url_when_not_sent":0.006,"Tests\\Feature\\DocumentoLinkTest::test_update_link_validates_url_format":0.006,"Tests\\Feature\\DocumentoLinkTest::test_destroy_link_does_not_call_storage":0.004,"Tests\\Feature\\DocumentoLinkTest::test_mass_download_skips_links":0.004}}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Fix: tabella `google_o_auth_connections` mancante
|
||||
|
||||
## Problema
|
||||
|
||||
Migration `2026_06_15_000001_create_google_oauth_connections_table.php` segnata come "Ran" (batch 39) ma tabella fisica inesistente sul database MySQL/MariaDB. Causa 500 su GET `/impostazioni`.
|
||||
|
||||
## Comandi (eseguire sul server `192.168.222.177`)
|
||||
|
||||
```bash
|
||||
# 1. Creare la tabella mancante con la stessa definizione della migration
|
||||
php artisan tinker --execute="
|
||||
Schema::create('google_o_auth_connections', function (\Illuminate\Database\Schema\Blueprint \$table) {
|
||||
\$table->id();
|
||||
\$table->string('service', 20)->unique();
|
||||
\$table->string('client_id', 255);
|
||||
\$table->text('client_secret');
|
||||
\$table->text('refresh_token');
|
||||
\$table->timestamps();
|
||||
});
|
||||
"
|
||||
|
||||
# 2. Pulire la cache delle viste (stale compiled view)
|
||||
php artisan view:clear
|
||||
|
||||
# 3. Verificare che la tabella esista
|
||||
php artisan tinker --execute="echo Schema::hasTable('google_o_auth_connections') ? 'OK' : 'ERRORE';"
|
||||
```
|
||||
@@ -3,6 +3,59 @@
|
||||
## 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, fix salvataggio campi sensibili |
|
||||
| **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)
|
||||
- **2026-06-17**: Fix CalendarioConnessione — `encryptAndSetConfig()` perdeva campi sensibili in edit, `is_active` checkbox senza hidden fallback, query duplicata nella view
|
||||
- **2026-06-18**: Fix import CSV — `declare(strict_types=1)` mancante in IndividuoController, header check assente, Log::warning() su errori; fix freeze server (set_time_limit, session_write_close, DB::transaction, fgetcsv length illimitato) in GruppoController e IndividuoController
|
||||
- **2026-06-23**: Email compose: firma spostata dopo il corpo; Mailing list: aggiunto `sender_account_id` (mittente predefinito salvato in creazione, usato all'invio come fallback)
|
||||
- **2026-06-23**: Fix formati nome: responsabili gruppi e individui email ora mostrano "Nome Cognome"; mailing list edit: aggiunto pulsante "Rimuovi selezionati"
|
||||
- **2026-06-22**: Aggiunto supporto Email Mittente Ufficiale (from_email/from_name) per invio con indirizzo diverso dalle credenziali SMTP/IMAP
|
||||
- Migration: `from_email`, `from_name` columns su `email_settings`
|
||||
- `EmailSetting`: metodi `getEffectiveFromAddress()`, `getEffectiveFromName()`, `getEffectiveReplyTo()`
|
||||
- Tutti i metodi di invio (`sendViaImap`, `sendViaSystem`, `sendPasswordResetNotification`, `storeDraft`, `storeSentMessage`, `testSmtp`) ora usano `getEffectiveFromAddress()`/`getEffectiveFromName()` per il mittente
|
||||
- Reply-To automatico impostato su `from_email` quando disponibile
|
||||
- UI: nuovi campi "Email Mittente Ufficiale" e "Nome Mittente Ufficiale" in entrambe le pagine impostazioni email
|
||||
- **2026-06-22**: Backup system enhancements:
|
||||
- `BackupService::run()` ora accetta `$options` (include_files, include_env) per override temporanei senza persistere su DB
|
||||
- `BackupRunCommand --no-files/--no-env` non salva più permanentemente la configurazione (era un bug)
|
||||
- `manifest.json` ora include `included_components` array (database, env, files)
|
||||
- Vista backup: badge .env (grigio) e Files (giallo) nella colonna "Contenuto" basati sul manifest
|
||||
- `run()` restituisce `steps` array; mostrato in flash message (controller) e output (command)
|
||||
- **2026-06-18**: Diagnose script (`diagnose.php`), .env.example aggiornato (SESSION_SECURE_COOKIE, FORCE_HTTPS, TRUSTED_PROXIES), build-dist.sh aggiornato con passo diagnose
|
||||
- **2026-06-23**: Usabilità viste colonne: se non si clicca ✏️ su una vista, la vista attiva corrente viene auto-assunta come target di modifica. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rilevamento rename via `currentVistaOriginalNome`)
|
||||
- **2026-06-23**: Aggiunto campo "Anno fondazione" (anno_fondazione) a Gruppo — migration, model (+fillable/+casts), controller (validazione store/update, import CSV, template CSV), create/edit/show/index views, import view field list
|
||||
- **2026-06-23**: Implementato "Documento come URL esterno" (link a risorsa esterna) — piena integrazione in email, mailing, preview, download, UI
|
||||
- Migration: `url` text nullable after `note`
|
||||
- Model: `url` in `$fillable` + metodo `isLink(): bool` (controlla `tipo === 'link'`)
|
||||
- DocumentoController: store() validazione condizionale file/URL via radio `tipo_caricamento`; update() campo URL condizionale; download() redirect away per link; preview() mostra fallback con pulsante "Apri link esterno"; massDownload() skip link-docs con counter warning; destroy() skip rimozione file
|
||||
- EmailController: processAttachments/resolveAttachmentPaths skip URL-docs dall'allegato fisico, accumulano links array; `appendLinksToBody()` inserisce link come testo nel corpo email prima della firma
|
||||
- MailingController: resolveMailingAttachmentPaths skip URL-docs, accumula links; `appendMailingLinksToBody()` stesso pattern
|
||||
- Upload modal (index.blade.php): toggle radio File/URL con campi condizionali (JS `toggleTipoCaricamento()`)
|
||||
- Grid/list view: icona link (`fa-link`) invece di mime icon per URL-docs; pulsante "Apri link esterno" invece di preview/download; move button hidden per link
|
||||
- Edit view (edit.blade.php): se link, mostra campo URL editabile + info URL nel sidebar invece di file_path/mime/dimensione
|
||||
- Preview fallback (preview-fallback.blade.php): supporto URL-docs con icona link + pulsante "Apri link esterno"
|
||||
- Email/mailing document selection table: badge "URL" nella riga documento
|
||||
- Route `documenti/{id}/download` per URL-docs: redirect HTTP 302 all'URL esterno
|
||||
|
||||
## Funzionalità Implementate
|
||||
|
||||
### Page-length selector
|
||||
@@ -291,8 +344,7 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
```
|
||||
|
||||
## Prossimi Passi
|
||||
- [DONE] ... (existing items)
|
||||
- Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
|
||||
- [DONE] Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
|
||||
- [DONE] Fix performance: spostata query `VistaReport::where(...)->get()` da `@php` nel partial `table-settings.blade.php` a tutti i 5 controller (prima veniva eseguita 1 query per ogni pagina load per ogni entity, anche senza mai aprire la modale)
|
||||
- [DONE] Rimosse vecchie modal legacy (`#saveVistaModal`, `#colonneModal`, `#vistaListModal`) da `individui` e `gruppi` — duplicate rispetto al nuovo modal unificato
|
||||
- [DONE] Rimosse vecchie funzioni JS (`saveVista()`, `toggleColumn()`, `showSaveVistaModal()`) da `individui` e `gruppi`
|
||||
@@ -300,9 +352,10 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
- [DONE] Individui: `getColumnIndex()` introdotta per lookup dinamico dell'indice colonna (sostituisce hardcoded `{codice:1, cognome:2, ...}` in `sortTable()` e `applyColumnFilter()`)
|
||||
- [DONE] I controller per eventi, documenti, mailing-liste hanno già `$tableColumns` con flag `visible` — stesso meccanismo di gruppi. Nessuna modifica necessaria.
|
||||
- [DONE] Fix: pulsante edit vista usa `data-*` attributi invece di `onclick` con `addslashes(json_encode(...))` — previene rottura HTML per JSON contenente `"`
|
||||
- [DA FARE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print
|
||||
- [DA FARE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload)
|
||||
- [DA FARE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista
|
||||
- [DONE] Test browser: caricare ogni pagina entity, verificare default vista, toggle colonne, resize, drag-reorder, print
|
||||
- [DONE] Test browser: flusso edit vista (✏️ → popola form → modifica → Aggiorna → reload)
|
||||
- [DONE] Test browser: salvare nuova vista, switching tra viste, cancellazione vista
|
||||
- [DONE] Usabilità viste colonne: se non si seleziona una vista esplicita (✏️), la vista attiva corrente viene auto-assunta come target. Se il nome viene cambiato, crea nuova vista invece di aggiornare (rename detection)
|
||||
|
||||
## 2026-06-10 — Per-User Column Views (colonne visibili, larghezze, ordine)
|
||||
|
||||
@@ -380,3 +433,741 @@ 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=<email>\1auth=Bearer <token>\1\1`
|
||||
- **ImapEngine**: già supporta `'authentication' => 'oauth'` in config → `AUTHENTICATE XOAUTH2 <base64>`
|
||||
- **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 (`×` → `<div class="modal fade" id="deleteModal"`)
|
||||
2. Eventi + Documenti: nessun feedback visivo dopo submit mass tag (mancava `session('error')` su Eventi; mancavano ENTRAMBI su Documenti)
|
||||
3. Eventi + Documenti: nessuna icona TAG per-riga nelle azioni
|
||||
4. Documenti: mancava funzione `openSingleTag()` JS
|
||||
|
||||
**Fix**:
|
||||
1. `resources/views/eventi/index.blade.php:340` — Ripristinato `×` nel close button del mass tag modal
|
||||
2. `resources/views/eventi/index.blade.php:22-27` — Aggiunto `@if(session('error'))` alert danger
|
||||
3. `resources/views/eventi/index.blade.php:283-285` — Aggiunto pulsante per-riga `fa-tags` → `openSingleTag(eventId)`
|
||||
4. `resources/views/eventi/index.blade.php:427-431` — Aggiunta funzione JS `openSingleTag(eventId)`
|
||||
5. `resources/views/documenti/index.blade.php:21-32` — Aggiunti `@if(session('success'))` + `@if(session('error'))` alert
|
||||
6. `resources/views/documenti/index.blade.php:283-285` e `484-486` — Aggiunto pulsante per-riga `fa-tags` in vista griglia + lista
|
||||
7. `resources/views/documenti/index.blade.php:1318-1323` — Aggiunta funzione JS `openSingleTag(docId)` con querySelector per `.doc-checkbox[value=...]`
|
||||
8. Verifica JS brace balance: OK su entrambi (diff=0)
|
||||
9. Verifica PHP lint: OK su entrambi i controller
|
||||
|
||||
## 2026-06-17 — Bug Fix: Documenti + Eventi (syntax error, strict_types, validazione)
|
||||
|
||||
### Problemi trovati e fixati
|
||||
|
||||
**1. CRITICO — Syntax error PHP in DocumentoController.php:239**
|
||||
- **Problema**: Parentesi `)` extra alla fine del ternario: `/documenti');` invece di `/documenti';`
|
||||
- **Root cause**: `$redirect = ... : '/documenti');` — parentesi di chiusura in eccesso
|
||||
- **Effetto**: PHP parse error: `Unclosed '{' on line 197 does not match ')'` — l'intero metodo `update()` era rotto
|
||||
- **Fix**: Rimosso `)` extra
|
||||
|
||||
**2. declare(strict_types=1) mancante** (violazione AGENTS.md)
|
||||
- **Problema**: 5 file del dominio documenti/eventi non avevano `declare(strict_types=1)`, obbligatorio per PHP 8.4
|
||||
- **File fixati**: `Documento.php`, `Evento.php`, `EventoController.php`, `TipologiaDocumento.php`, `EventoDocumentoController.php`
|
||||
|
||||
**3. Validazione assente `contesto_tipo` in DocumentoController@update**
|
||||
- **Problema**: Il campo `contesto_tipo` veniva letto dal raw request (`$request->contesto_tipo`) senza validazione. Un utente poteva forzare valori arbitrari.
|
||||
- **Fix**: Aggiunto `'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing'` alle regole di validazione. Ora si usa il valore validato (`$contestoTipo`) invece del raw request.
|
||||
|
||||
**4. Tipologia hardcoded in DocumentoController@store**
|
||||
- **Problema**: `store()` usava `'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro'` (hardcoded), mentre `update()` usava `TipologiaDocumento::opzioni()` (dinamico). Nuove tipologie aggiunte via UI non sarebbero state accettate da `store()`.
|
||||
- **Fix**: Allineato `store()` a usare `TipologiaDocumento::opzioni()` come `update()`.
|
||||
|
||||
**5. JS brace balance verificato**
|
||||
- `resources/views/documenti/index.blade.php` → OK (diff=0)
|
||||
- `resources/views/eventi/index.blade.php` → OK (diff=0)
|
||||
|
||||
### Verifica
|
||||
- `php -l` su tutti e 6 i file modificati: nessun errore di sintassi
|
||||
|
||||
## 2026-06-17 — Fix: Eventi mass tag "nessun evento selezionato"
|
||||
**Problema**: La toolbar button chiamava `$('#massTagModal').modal('show')` direttamente, affidandosi a un listener `show.bs.modal` per popolare l'hidden field `massTagIds`. Se l'evento non veniva intercettato, `ids` restava vuoto e il server rispondeva "Nessun evento selezionato".
|
||||
|
||||
**Fix** (`resources/views/eventi/index.blade.php`):
|
||||
1. Toolbar button: `onclick="$('#massTagModal').modal('show')"` → `onclick="showMassTagModal()"`
|
||||
2. Rimosso listener `show.bs.modal` che popolava i campi
|
||||
3. Aggiunta funzione `showMassTagModal()` (stesso pattern di Gruppi/Individui) che valida `getSelectedIds()` e popola i campi **prima** di aprire la modale
|
||||
4. `openSingleTag()` ora chiama `showMassTagModal()` invece di mostrare la modale direttamente
|
||||
|
||||
## 2026-06-17 — Fix: Documenti — root mostra documenti di tutte le cartelle
|
||||
**Problema**: Alla root (`/documenti`), la query includeva tutti i documenti locali (`WHERE repository_id IS NULL`), mostrando documenti di tutte le cartelle in un unico elenco piatto.
|
||||
|
||||
**Fix** (`app/Http/Controllers/DocumentoController.php`):
|
||||
- Aggiunto `->whereNull('cartella_id')` alla query di root, così mostra solo documenti **senza cartella** (radice). Per vedere i documenti dentro una cartella bisogna navigarci dentro con `?folder_id=X`.
|
||||
|
||||
## 2026-06-17 — Fix: CalendarioConnessione — salvataggio perdeva password/client_secret in edit + is_active sempre true
|
||||
|
||||
**Problema**: Salvando una connessione calendario esistente (CalDAV o Google Calendar), i campi sensibili (password, client_secret) venivano sovrascritti con stringa vuota. Il test connessione e la sincronizzazione fallivano silenziosamente.
|
||||
|
||||
**Root cause 1 — `encryptAndSetConfig()` sovrascrive campi vuoti**:
|
||||
- Il JS di edit (`editCalendario()`) azzera `config[password]` e `config[client_secret]` per sicurezza (valore `''`, placeholder `......`)
|
||||
- `encryptAndSetConfig()` in `CalendarioConnessione.php` controllava `$config[$field] !== ''` e saltava la crittografia, ma poi eseguiva `$this->config = $config` — la stringa vuota sovrascriveva il valore crittato precedente
|
||||
- `StorageRepository` NON aveva questo bug: `StorageRepositoryService::encryptSensitiveConfig()` preserva i campi esistenti con `} elseif (empty($config[$field]) && !empty($existingConfig[$field])) { $config[$field] = $existingConfig[$field]; }`
|
||||
|
||||
**Root cause 2 — `is_active` checkbox senza hidden fallback**:
|
||||
- Stesso bug già fixato per email (2026-06-08): checkbox `is_active` senza `<input type="hidden" name="is_active" value="0">`
|
||||
- Quando non spuntato, il campo non veniva inviato → `$request->boolean('is_active', true)` restituiva sempre `true`
|
||||
- La connessione restava sempre attiva indipendentemente dallo switch
|
||||
|
||||
**Root cause 3 — Query duplicata nella view**:
|
||||
- `resources/views/impostazioni/index.blade.php:773` ri-eseguiva `CalendarioConnessione::orderBy('ordine')->get()` nonostante il controller lo passasse già via `compact()`
|
||||
|
||||
**Fix**:
|
||||
1. **`app/Models/CalendarioConnessione.php:56`** — `encryptAndSetConfig()` ora accetta `array $existingConfig = []` e preserva i valori crittati esistenti quando il campo submitted è vuoto (stesso pattern di `StorageRepositoryService::encryptSensitiveConfig()`)
|
||||
2. **`app/Http/Controllers/CalendarioConnessioneController.php:67`** — `update()` passa `$connessione->config` come secondo parametro a `encryptAndSetConfig()`
|
||||
3. **`resources/views/impostazioni/index.blade.php:1576`** — Aggiunto `<input type="hidden" name="is_active" value="0">` prima della checkbox
|
||||
4. **`resources/views/impostazioni/index.blade.php:773`** — Rimossa query duplicata `@php $calendarioConnessioni = ...`
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su entrambi i file modificati
|
||||
- JS brace balance: OK
|
||||
|
||||
## 2026-06-17 — Fix: Documenti mass tag "nessun documento selezionato"
|
||||
**Problema**: Stesso identico bug di Eventi — toolbar button chiamava `$('#massTagModal').modal('show')` e il listener `show.bs.modal` non sempre popolava i campi.
|
||||
|
||||
**Fix** (`resources/views/documenti/index.blade.php`):
|
||||
1. Rimosso listener `show.bs.modal`
|
||||
2. Creata funzione `showMassTagModal()` (con fallback per `data-pre-selected`)
|
||||
3. `openSingleTag()` ora usa `showMassTagModal()` invece di mostrare la modale direttamente
|
||||
|
||||
## 2026-06-17 — Fix: ColumnManager pin colonne select/azioni con `cells[]` invece di `querySelector`
|
||||
**Problema**: Il pinning di `select` e `azioni` usava `row.querySelector('[data-column="..."]')` che funziona solo sul `<thead>` (dove `<th>` ha `data-column`), ma non sul `<tbody>` (dove i `<td>` non hanno `data-column`).
|
||||
|
||||
**Fix** (`public/js/column-manager.js`): Sostituito con `cells[keyToThIndex['select']]` che usa l'indice della colonna, valido sia per `th` che per `td`.
|
||||
|
||||
### DA FARE
|
||||
- Configurare `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` in `.env`
|
||||
- Test end-to-end: flusso OAuth completo (redirect → auth → callback → connessione creata)
|
||||
- Test XOAUTH2 SMTP: invio email via connessione OAuth
|
||||
- Test IMAP OAuth: sync email via connessione OAuth
|
||||
- Verificare refresh token automatico allo scadere
|
||||
- Verificare revoca e riconnessione
|
||||
|
||||
## 2026-06-17 — Fix: tutte le migration + seeders idempotenti
|
||||
|
||||
### Problema
|
||||
`php artisan migrate` falliva su tabelle già esistenti (`tenants`, `tipologie_documenti`, ecc.).
|
||||
`php artisan db:seed` creava duplicati su re-run (admin user, diocesi, comuni).
|
||||
Migrations con `insert` dopo il guard `hasTable` violavano unique constraint al re-run.
|
||||
|
||||
### Migration: hasTable guard su 28 CREATE mancanti
|
||||
Aggiunto `if (!Schema::hasTable('table_name')) { ... }` wrapper a 28 migration file (45 `Schema::create()` calls) via script Node.js, più `2026_06_17_000001_create_google_oauth_connections_table.php` manualmente.
|
||||
|
||||
### Fix: insert fuori dai guard (4 file)
|
||||
- `2026_05_10_000001_create_tipologie_documenti_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_26_000001_create_tipologie_eventi_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_12_000001_create_ruoli_table.php` — spostato insert dentro l'if
|
||||
- `2026_05_11_000006_add_email_attachment_tipologia.php` — `insert()` → `updateOrInsert()`
|
||||
|
||||
### Seeder: firstOrCreate
|
||||
- `DiocesiSeeder.php`: `Diocesi::create()` → `::firstOrCreate(['nome' => ...])`
|
||||
- `ComuniSeeder.php`: `Comune::create()` → `::firstOrCreate(['codice_istat' => ...])`
|
||||
- `DatabaseSeeder.php`: `User::create()` → `::firstOrCreate(['email' => ...])` + guardia `role_preset_id`
|
||||
|
||||
### Risultato
|
||||
`php artisan migrate --seed` è ora completamente idempotente. Zero errori su 64 migration + 6 seeder. Re-run è un no-op.
|
||||
|
||||
## 2026-06-17 — Fix: MailingList tag management (mass tag + per-riga + attiva checkbox)
|
||||
|
||||
**Problema**: Nella GUI non era possibile assegnare tag alle mailing list né singolarmente né via azione massiva.
|
||||
|
||||
**Root cause**:
|
||||
1. **Nessuna route mass-tag per mailing-liste** — Eventi, Documenti, Individui, Gruppi avevano `POST /{entity}/mass-tag` ma MailingList no
|
||||
2. **Nessun metodo `massTag()` in MailingListController** — assente rispetto agli altri 4 controller
|
||||
3. **Nessun pulsante per-riga tag** nella index view (mancava `<button type="button" onclick="openSingleTag()">`)
|
||||
4. **Nessun pulsante toolbar "Tag"** nella index view (mancava il mass tag button)
|
||||
5. **Checkbox `attiva` senza hidden fallback** in create/edit — stesso bug ricorrente: deselezionando, il campo non veniva inviato
|
||||
|
||||
**Fix**:
|
||||
1. **`app/Http/Controllers/MailingListController.php:189-214`** — Aggiunto metodo `massTag()` (stesso pattern di `EventoController@massTag`):
|
||||
- Autorizzazione via `$this->authorizeWrite('mailing')`
|
||||
- Validazione: `tags` required array, `mode` required in:assign,remove
|
||||
- Processa in chunk(100) via `syncWithoutDetaching()` (assign) / `detach()` (remove)
|
||||
- Restituisce `back()->with('success', ...)`
|
||||
2. **`routes/web.php:177`** — Aggiunta route `POST mailing-liste/mass-tag` → `MailingListController@massTag` (name: `mailing-liste.mass-tag`)
|
||||
3. **`resources/views/mailing-liste/index.blade.php`**:
|
||||
- `select-all` checkbox: permesso esteso da `canDeleteMailing` a `canWriteMailing || canDeleteMailing`
|
||||
- `row-checkbox`: stessa estensione permesso
|
||||
- Toolbar: aggiunto pulsante "Tag" (btn-sm btn-info, `showMassTagModal()`) prima di "Elimina Selezionati"
|
||||
- Azioni per-riga: aggiunto pulsante `fa-tags` con `openSingleTag(lista.id)` dopo il pulsante show
|
||||
- Aggiunto `@if(session('error'))` alert
|
||||
- Aggiunta modale `#massTagModal` (stesso pattern di eventi: titolo, counter, radio assign/remove, `_tag-selector`, hidden `ids`)
|
||||
- Aggiunte funzioni JS: `showMassTagModal()`, `openSingleTag(listId)`
|
||||
4. **`resources/views/mailing-liste/create.blade.php:23`** — Aggiunto `<input type="hidden" name="attiva" value="0">` prima della checkbox
|
||||
5. **`resources/views/mailing-liste/edit.blade.php:23`** — Stesso hidden fallback
|
||||
6. **`resources/views/mailing-liste/edit.blade.php:11`** — Fix percorso hardcoded: `url('/mailing-liste/' . ...)` → `route('mailing-liste.update', ...)`
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su controller + routes
|
||||
- JS brace balance: OK su index view
|
||||
- Route list: `POST mailing-liste/mass-tag` → `MailingListController@massTag` (name: `mailing-liste.mass-tag`)
|
||||
|
||||
## 2026-06-18 — Fix import CSV: freeze server + bug IndividuoController
|
||||
|
||||
### Problema freeze server (Gruppi + Individui)
|
||||
Importando CSV con molte righe, il server si bloccava:
|
||||
1. **PHP max_execution_time (30s)** — killava il processo su import grandi
|
||||
2. **Session lock** — altre richieste dello stesso utente restavano in attesa
|
||||
3. **Nessuna transazione DB** — ogni `create()` era una INSERT individuale autocommit
|
||||
4. **fgetcsv length=1000** — righe CSV più lunghe di 1000 caratteri venivano troncate
|
||||
|
||||
### Fix applicati a `GruppoController@importStore` e `IndividuoController@importStore`
|
||||
```php
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
$header = fgetcsv($handle, 0, ','); // length=0 = nessun limite
|
||||
DB::beginTransaction();
|
||||
// ... loop ...
|
||||
DB::commit();
|
||||
```
|
||||
|
||||
### Bug trovati in `IndividuoController@importStore`
|
||||
| Bug | Fix |
|
||||
|-----|-----|
|
||||
| `declare(strict_types=1)` mancante | ✅ Aggiunto |
|
||||
| Indentazione errata negli import (spazi extra su 8 righe) | ✅ Corretto |
|
||||
| `use Illuminate\Support\Facades\Log` mancante | ✅ Aggiunto |
|
||||
| Nessun controllo `$header === false` — CSV vuoto causava TypeError | ✅ Aggiunto |
|
||||
| Nessun `array_map('trim', $header)` | ✅ Aggiunto |
|
||||
| Nessun `Log::warning()` su eccezioni riga | ✅ Aggiunto |
|
||||
| `fgetcsv` length=1000 (vs 0) | ✅ Portato a 0 |
|
||||
| Nessun `set_time_limit(0)` / `session_write_close()` | ✅ Aggiunto |
|
||||
| Nessun `DB::beginTransaction()` / `DB::commit()` | ✅ Aggiunto |
|
||||
|
||||
### File modificati
|
||||
- `app/Http/Controllers/GruppoController.php` — aggiunti `use DB`, set_time_limit, session_write_close, fgetcsv length=0, DB::transaction
|
||||
- `app/Http/Controllers/IndividuoController.php` — strict_types, Log import, DB import, header check, trim, set_time_limit, session_write_close, DB::transaction
|
||||
|
||||
### Verifica
|
||||
- `php -l` su entrambi: nessun errore di sintassi
|
||||
|
||||
## 2026-06-18 — Diagnose script + .env.example + build-dist.sh
|
||||
|
||||
### `diagnose.php` — Script standalone di diagnostica
|
||||
Creato script standalone per verificare installazione su server remoto. Non richiede Laravel.
|
||||
|
||||
**Cosa verifica**:
|
||||
- PHP version (≥ 8.2), 15 estensioni required, 4 opzionali
|
||||
- INI settings (execution_time, memory, upload/post max)
|
||||
- .env: APP_KEY decodifica (32 bytes), APP_URL, SESSION_SECURE_COOKIE, DB, FORCE_HTTPS
|
||||
- 13 directory in `storage/` e `bootstrap/cache`: esistenza, permessi, scrivibilità
|
||||
- Symlink `public/storage` (supporto sia link assoluti che relativi)
|
||||
- Test scrittura/lettura file sessione
|
||||
- Connessione DB via PDO, tabelle principali, conteggio records, engine InnoDB
|
||||
- Middleware bootstrap/app.php: ForceHttps registrato, TrustProxies, duplicazioni web group
|
||||
- Vendor: autoload, composer, node_modules, public/build, artisan
|
||||
- CSRF: HTTP GET login page, estrazione token, POST con token, verifica 419
|
||||
- Spazio disco: totale, libero, percentuale
|
||||
|
||||
**Uso**:
|
||||
```bash
|
||||
php diagnose.php
|
||||
php diagnose.php --verbose # output dettagliato
|
||||
```
|
||||
Exit code: 0 = OK, 1 = errori critici.
|
||||
|
||||
### `.env.example` aggiornato
|
||||
Aggiunte variabili mancanti:
|
||||
- `APP_PROTOCOL`, `APP_SUBFOLDER` — supporto subfolder
|
||||
- `FORCE_HTTPS`, `TRUSTED_PROXIES` — proxy/HTTPS configurabile
|
||||
- `SESSION_SECURE_COOKIE`, `SESSION_SAME_SITE` — sessione cross-protocol
|
||||
- `APP_ENV=production`, `APP_DEBUG=false` — safe defaults per produzione
|
||||
|
||||
### `build-dist.sh`
|
||||
- Aggiunto `php diagnose.php` come passo #0 (pre-installazione) e #7 (verifica finale)
|
||||
- Istruzioni post-estrazione complete
|
||||
|
||||
### File modificati
|
||||
- `diagnose.php` (nuovo) — standalone diagnostica
|
||||
- `.env.example` — variabili mancanti aggiunte
|
||||
- `build-dist.sh` — passo diagnose
|
||||
|
||||
### Verifica
|
||||
- `php -l diagnose.php`: OK
|
||||
- `php diagnose.php`: eseguito con 58/68 check superati su server locale
|
||||
|
||||
## 2026-06-19 — Fix 419: Duplicated middleware + session env vars + diagnose.php fixes
|
||||
|
||||
### Problema
|
||||
`POST /login` restituiva **419 Page Expired** su HTTP. La causa era doppia:
|
||||
|
||||
1. **Critico — Middleware duplicato in `bootstrap/app.php`**: `EncryptCookies`, `AddQueuedCookiesToResponse`, `StartSession`, `ShareErrorsFromSession` erano appesi al web group via `$middleware->web(append: [...])`. Questi middleware sono **già attivi di default** in Laravel 13. Appenderli una seconda volta causava doppia crittografia/decrittografia dei cookie di sessione → session ID corrotto → token CSRF non riconosciuto → 419.
|
||||
2. **`SESSION_SECURE_COOKIE` non impostato** nel `.env` — con `APP_URL=https` + `FORCE_HTTPS=true`, Laravel impostava il flag `Secure` sul cookie di sessione. Poiché il server serviva solo HTTP, il browser non inviava il cookie → session persa → 419.
|
||||
|
||||
### Fix apportati
|
||||
|
||||
**`bootstrap/app.php`** (righe 22-27):
|
||||
- Rimosso l'intero blocco `$middleware->web(append: [...])` che duplicava EncryptCookies, AddQueuedCookiesToResponse, StartSession, ShareErrorsFromSession.
|
||||
- Sostituito con commento esplicativo per prevenire future reintroduzioni.
|
||||
|
||||
**`.env`**:
|
||||
- Aggiunto `SESSION_SECURE_COOKIE=false` — impedisce il flag Secure su HTTP
|
||||
- Aggiunto `SESSION_SAME_SITE=lax` — esplicito per consistenza cross-protocol
|
||||
- Cambiato `APP_URL` da HTTPS a HTTP (ambiente locale senza HTTPS)
|
||||
- Cambiato `APP_PROTOCOL` da `https` a `http`
|
||||
- Cambiato `FORCE_HTTPS` da `true` a `false`
|
||||
- Cambiato `TRUSTED_PROXIES` da `*` a CIDR espliciti
|
||||
|
||||
**`diagnose.php`** — 4 bug fix:
|
||||
1. **`unlink()` senza `file_exists()`** nelle linee 863 e 900: causava PHP Warning quando curl non creava il cookie jar (es. HTTPS non raggiungibile). Aggiunto `file_exists($cookieJar) && unlink($cookieJar)`.
|
||||
2. **Regex middleware duplicato non matchava array multi-riga**: il pattern `[^\]]*` non matcha newline. Sostituito con `(.+?)` con flag `s`. Classe name extraction semplificata a `([a-zA-Z]+)::class`.
|
||||
3. **`CURLOPT_COOKIEJAR` non funzionante** in questo ambiente (curl 8.14.1 + PHP 8.4 — il file non viene creato da `curl_close()`). Riscritto `testCsrfLogin()` per parsare manualmente gli header `Set-Cookie` dalla prima risposta e reinviarli via `CURLOPT_COOKIE` nella POST. Eliminata dipendenza da `COOKIEJAR`/`COOKIEFILE`.
|
||||
4. Aggiunto verbose debug per codici HTTP POST anomali (≠ 302/200).
|
||||
|
||||
### Risultato finale
|
||||
```
|
||||
✅ Passed: 63
|
||||
⚠️ Warnings: 8 (upload/post_max_size, engine opzionali mancanti)
|
||||
❌ Failed: 1 (spazio disco 94.6% — server admin)
|
||||
```
|
||||
- Test CSRF: ✅ POST login → HTTP 302 (nessun 419)
|
||||
- Solo il disco (94.6%) è critico — non risolvibile via script.
|
||||
|
||||
### File modificati
|
||||
- `bootstrap/app.php` — rimosso middleware web duplicato
|
||||
- `.env` — aggiunte SESSION_SECURE_COOKIE, SESSION_SAME_SITE; APP_URL/APP_PROTOCOL/FORCE_HTTPS adeguati
|
||||
- `diagnose.php` — fix unlink warning, regex middleware multi-line, CSRF test manual-cookie, verbose debug
|
||||
|
||||
## 2026-06-19 — diagnose.php: storage false positivo + ForceHttps registration check
|
||||
|
||||
### Fix 1 — Storage permission false positive
|
||||
**Problema**: `diagnose.php` segnava 15 storage directory come ❌ NON SCRIVIBILE quando `php diagnose.php` veniva eseguito da CLI (utente `opencode`). Le directory sono di proprietà `www-data:www-data` con permessi `775`, quindi il web server **può** scrivere — il falso positivo generava confusione.
|
||||
|
||||
**Diagnosi**:
|
||||
- CLI esegue come `opencode` → `is_writable()` controlla i permessi "other" (5 = r-x) → `false`
|
||||
- Web server esegue come `www-data` → owner `rwx` → `true`
|
||||
- Questo è normale su server condivisi/Debian standard
|
||||
|
||||
**Fix** (`diagnose.php:659-660`):
|
||||
- Aggiunto `elseif ($owner === WEB_USER && in_array($perms, ['775', '777', '755', '770']))`
|
||||
- Se il proprietario è `www-data` e permessi sono `775`/`777`/`755`/`770`, mostra ⚠️ **warning** invece di ❌ **fail**, con messaggio "NON scrivibile da CLI ma WEB server sì"
|
||||
- Altrimenti mostra fail come prima
|
||||
|
||||
### Fix 2 — ForceHttps registration check
|
||||
**Problema**: Il controllo ForceHttps usava `str_contains($appContent, 'ForceHttps')` che matchava anche il semplice `use App\Http\Middleware\ForceHttps` (import), segnando ✅ "registrato" anche quando il middleware non era attivo in alcun gruppo.
|
||||
|
||||
**Stato attuale**: `ForceHttps::class` è **importato** ma **non registrato** in nessun middleware group (`$middleware->append()`, `->web()`, ecc.) in `bootstrap/app.php`.
|
||||
|
||||
**Fix** (`diagnose.php:871-879`):
|
||||
- Sostituito con `preg_match('/\$middleware\s*->\s*\w+\s*\(.*?ForceHttps::class/s', $appContent)`
|
||||
- 3 stati:
|
||||
1. ✅ `"registrato correttamente"` — se ForceHttps::class è argomento di una chiamata `$middleware->xxx()`
|
||||
2. ⚠️ `"IMPORTATO ma NON registrato"` — se la stringa ForceHttps esiste (use import) ma non in una chiamata
|
||||
3. ⚠️ `"NON presente"` — se ForceHttps non esiste nel file
|
||||
|
||||
### Verifica
|
||||
- `php -l diagnose.php`: No syntax errors detected
|
||||
- `php diagnose.php` su locale: ForceHttps ora segnala ⚠️ "IMPORTATO ma NON registrato" (corretto)
|
||||
|
||||
### File modificati
|
||||
- `diagnose.php` — storage false positive + ForceHttps registration check
|
||||
|
||||
## 2026-06-22 — build-dist.sh: genera post-deploy.sh per upgrade/install automatico
|
||||
|
||||
**Obiettivo**: Includere nell'archivio di distribuzione uno script `post-deploy.sh` che esegue controllo e setup dell'installazione sul server target, sia per fresh install che per upgrade.
|
||||
|
||||
### Cosa è stato fatto
|
||||
|
||||
**`build-dist.sh`** riscritto:
|
||||
- Prima del `tar`, genera `post-deploy.sh` (heredoc) e lo include nell'archivio
|
||||
- Dopo il `tar`, `post-deploy.sh` viene rimosso localmente (non sporca la working dir)
|
||||
- Istruzioni finali aggiornate: mostrano i tre comandi principali
|
||||
|
||||
**`post-deploy.sh`** (generato, non versionato):
|
||||
Tre modalità d'uso:
|
||||
|
||||
| Comando | Cosa fa |
|
||||
|---------|---------|
|
||||
| `bash post-deploy.sh check` | Diagnostica stato installazione: migration pendenti, permessi directory, symlink, cache. Nessuna modifica. |
|
||||
| `bash post-deploy.sh upgrade` | Check + upgrade esistente: crea directory, permessi, symlink, `migrate --force`, `key:generate` (se mancante), cache clear, verifica finale. Chiede conferma prima di eseguire. |
|
||||
| `bash post-deploy.sh install` | Fresh install completo: come upgrade + copia `.env.example → .env` con pausa per modifica utente. |
|
||||
| `bash post-deploy.sh upgrade --yes` | Batch mode (nessuna conferma richiesta) |
|
||||
|
||||
Fasi upgrade (8 step): directory → .gitignore → permessi → symlink → migrazioni → key → cache → diagnose
|
||||
Fasi install (9 step): come upgrade + setup .env con pausa interattiva
|
||||
|
||||
### Vantaggi
|
||||
- **Idempotente**: `migrate --force` esegue solo migration pendenti
|
||||
- **Sicuro**: modalità `check` non modifica nulla
|
||||
- **Interattivo**: chiede conferma prima di azioni distruttive
|
||||
- **Batch**: `--yes` per automazione CI/CD
|
||||
- **Compatibile**: integra `diagnose.php` già esistente (check + fix)
|
||||
- **Autopulente**: `post-deploy.sh` non resta nella working dir di build
|
||||
|
||||
### Verifica
|
||||
- `bash -n build-dist.sh`: syntax OK
|
||||
- `bash -n post-deploy.sh`: syntax OK
|
||||
- `bash post-deploy.sh check`: eseguito correttamente
|
||||
- `tar tzf glastree-*.tar.gz | grep post-deploy`: presente nell'archivio
|
||||
- `rm -f post-deploy.sh` dopo tar: pulizia OK
|
||||
|
||||
## 2026-06-22 — Estensione Gruppi: contatti propri + diocesi multiple
|
||||
|
||||
**Obiettivo**: Aggiungere contatti (email, telefono) ai gruppi e supportare assegnazione di più diocesi.
|
||||
|
||||
### Cosa è stato fatto
|
||||
|
||||
**Migration** (2 nuove tabelle):
|
||||
- `2026_06_22_000002_create_gruppo_contatti_table.php` — `gruppo_contatti` con: id, gruppo_id (FK cascade), tipo, valore, etichetta, is_primary, timestamps
|
||||
- `2026_06_22_000003_create_diocesi_gruppo_table.php` — pivot `diocesi_gruppo` con: id, gruppo_id (FK cascade), diocesi_id (FK cascade), unique(gruppo_id, diocesi_id)
|
||||
|
||||
**Model `GruppoContatto.php`** (nuovo):
|
||||
- `$table = 'gruppo_contatti'`, fillable: gruppo_id, tipo, valore, etichetta, is_primary
|
||||
- `gruppo()`: BelongsTo Gruppo
|
||||
|
||||
**Model `Gruppo.php`**:
|
||||
- `diocesi()`: cambiata da `BelongsTo` a `BelongsToMany` via `diocesi_gruppo`
|
||||
- `gruppoContatti()`: nuova `HasMany` relation
|
||||
- Accessor `getEmailPrimariaAttribute()`: primo contatto email (is_primary preferito)
|
||||
- Accessor `getTelefonoPrimarioAttribute()`: primo contatto telefono/cellulare (is_primary preferito)
|
||||
- `$appends`: già presente `email_primaria`, `telefono_primario` (invariato)
|
||||
- `$fillable`: `diocesi_id` mantenuto (nullable) per backward compat con import CSV
|
||||
|
||||
**Model `Diocesi.php`**:
|
||||
- `gruppi()`: cambiata da `HasMany` a `BelongsToMany` via `diocesi_gruppo`
|
||||
|
||||
**`GruppoController.php`**:
|
||||
- `store()`/`update()`: validazione cambiata da `diocesi_id` (nullable|exists) a `diocesi_ids` (nullable|array|exists)
|
||||
- `store()`/`update()`: nuova validazione `contatti` array con tipo (in:email,telefono,cellulare), valore, etichetta, is_primary
|
||||
- `store()`: crea contatti dopo create; sync diocesi
|
||||
- `update()`: cancella+ricrea contatti se presenti; sync diocesi (o detach se assenti)
|
||||
- `edit()`: eager-load `gruppoContatti`, passa `$selectedDiocesiIds`
|
||||
- `show()`: eager-load `gruppoContatti`
|
||||
- `index()`: eager-load `gruppoContatti`
|
||||
- `create()`: invariato (passa già `$diocesi`)
|
||||
|
||||
**`ReportController.php`**:
|
||||
- 4 occorrenze `$g->diocesi?->nome` → collection pattern `$g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-'`
|
||||
|
||||
**View `gruppi/create.blade.php`**:
|
||||
- Select diocesi: cambiato da single `select name="diocesi_id"` a multiple `select name="diocesi_ids[]" class="select2-multi"`
|
||||
- Aggiunta card "Contatti del Gruppo" con tabella inline (tipo select, valore input, etichetta, checkbox primario, elimina)
|
||||
- Select2 CSS/JS caricati via CDN
|
||||
|
||||
**View `gruppi/edit.blade.php`**:
|
||||
- Stessa modifica diocesi → Select2 multi con `$selectedDiocesiIds` pre-selezionati
|
||||
- Aggiunta card "Contatti del Gruppo" con righe pre-popolate da `$gruppo->gruppoContatti`
|
||||
- Select2 CSS/JS caricati via CDN
|
||||
|
||||
**View `gruppi/show.blade.php`**:
|
||||
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
|
||||
- Aggiunte righe Email/Telefono (da accessor) e "Altri Contatti" nella card info
|
||||
|
||||
**View `gruppi/index.blade.php`**:
|
||||
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
|
||||
- Telefono/Email: da hardcoded `-` a `$gruppo->telefono_primario` / `$gruppo->email_primaria`
|
||||
|
||||
**View `gruppi/partials/tree-item.blade.php`**:
|
||||
- Diocesi: da `$gruppo->diocesi?->nome` a collection implode
|
||||
|
||||
**View `individui/show.blade.php` + `individui/edit.blade.php`**:
|
||||
- Diocesi nei gruppi dell'individuo: da `$gruppo->diocesi?->nome` a collection implode
|
||||
|
||||
### Backward compatibility
|
||||
- Colonna `diocesi_id` su `gruppi` mantenuta (nullable) per import CSV
|
||||
- CSV import (`importStore()`) usa ancora `diocesi_id` → singola diocesi
|
||||
- `$fillable` include ancora `diocesi_id`
|
||||
- Nessuna modifica a `Individuo` o `Contatto` esistenti
|
||||
|
||||
### Verifica
|
||||
- `php -l` su tutti i file modificati: OK
|
||||
- Migrations eseguite: ✅
|
||||
- Relazioni verificate: `diocesi()` BelongsToMany, `gruppoContatti()` HasMany, `email_primaria`/`telefono_primario` accessors funzionanti
|
||||
|
||||
## 2026-06-22 — Fix produzione: 500 pagina gruppi (migration pending + autoloader stale)
|
||||
|
||||
**Problema**: Dopo deploy su server produzione (`192.168.222.177`):
|
||||
- Migration `diocesi_gruppo` non eseguita (PENDING)
|
||||
- `GruppoContatto.php` non nell'autoloader (`composer dump-autoload` non eseguito)
|
||||
- `storage/logs/laravel.log` non scrivibile da www-data
|
||||
- 3 migration pre-esistenti non marcate come "ran" (google_oauth_connections, add_auth_method, add_google_id) — tabelle/colonne già esistenti ma migration record mancanti → bloccavano `migrate --force`
|
||||
|
||||
**Fix**:
|
||||
1. `sudo chmod -R 775 storage bootstrap/cache` — permessi
|
||||
2. `sudo usermod -a -G www-data opencode` — utente CLI nel gruppo www-data
|
||||
3. Inseriti manualmente 3 migration record mancanti in `migrations` table (già eseguite in passato ma mai registrate)
|
||||
4. `php artisan migrate --force` — eseguita `2026_06_22_000003_create_diocesi_gruppo_table`
|
||||
5. `composer dump-autoload` — autoloader rigenerato (41464 classi)
|
||||
6. `php artisan view:clear && php artisan route:clear && php artisan config:clear`
|
||||
|
||||
**Risultato**:
|
||||
- `class_exists(App\Models\GruppoContatto)` → ✅
|
||||
- `gruppi` page HTTP 200 (after login redirect 302) — 500 risolto
|
||||
- Tutte le migration marked as Ran (batch 40-42)
|
||||
- Views/routes/config cache pulite
|
||||
|
||||
**Lezione**: `build-dist.sh` fa `composer install --no-scripts` che non rigenera autoloader ottimizzato per il target. `post-deploy.sh upgrade` dovrebbe includere `composer dump-autoload` (o almeno `composer install --no-dev --optimize-autoloader`).
|
||||
|
||||
## 2026-06-22 — Fix: Select2 search field visibile sotto le diocesi selezionate
|
||||
|
||||
**Problema**: In create/edit gruppi, il `<textarea class="select2-search__field">` creato da Select2 in modalità multiple era visibile sotto i tag delle diocesi selezionate, con altezza 65px (ereditata da Bootstrap form-control).
|
||||
|
||||
**Causa**:
|
||||
1. `allowClear: true` in Select2 multiple mode non serve (ogni tag ha già la X per rimuoverlo) e causa conflitti di rendering
|
||||
2. Bootstrap 4 applica `height: 65px` al textarea del search field
|
||||
3. Nessun CSS specifico per normalizzare il search field inline
|
||||
|
||||
**Fix**:
|
||||
1. Rimosso `allowClear: true` da entrambi i file (`create.blade.php`, `edit.blade.php`)
|
||||
2. Aggiunto CSS per normalizzare `height: 28px`, `border: none`, `background: transparent`, `width: auto` con `min-width: 30px`
|
||||
|
||||
**File modificati**:
|
||||
- `resources/views/gruppi/create.blade.php` — rimosso allowClear, aggiunto CSS search field
|
||||
- `resources/views/gruppi/edit.blade.php` — rimosso allowClear, aggiunto CSS search field
|
||||
|
||||
## 2026-06-23 — Fix: Select2 counter rimosso (utente vuole vedere tutti i nomi)
|
||||
|
||||
**Problema**: Il `templateSelection` con counter mostrava "5 diocesi selezionate" invece dei nomi.
|
||||
|
||||
**Fix**: Rimosso l'intero blocco `templateSelection` da entrambi create/edit. Ogni diocesi selezionata mostra ora il proprio nome.
|
||||
|
||||
**File modificati**:
|
||||
- `resources/views/gruppi/create.blade.php` — rimosso templateSelection
|
||||
- `resources/views/gruppi/edit.blade.php` — rimosso templateSelection
|
||||
|
||||
## 2026-06-23 — Fix: DiocesiSeeder + build-dist.sh per deploy
|
||||
|
||||
**Problema**: La tabella `diocesi` con 225 record importati da ODS non veniva popolata sul server target dopo deploy. Il seeder `DiocesiSeeder.php` aveva solo ~100 nomi obsoleti/inaccurati (es. "Mongolia", "Donegal", "Tirana").
|
||||
|
||||
**Fix**:
|
||||
1. **`DiocesiSeeder.php`** riscritto completamente:
|
||||
- 225 nomi corretti (Arcidiocesi/Diocesi/Sede/Patriarcato/Abbazia/Eparachia)
|
||||
- Encoding: 7 nomi con mojibake da ODS fixati via `where('id', ...)->update()` (Trinità, Cefalù, Città, Forlì, Mondovì, Nardò, Perugia-Città)
|
||||
- Apostrofo: "Val d Elsa" → "Val d'Elsa"
|
||||
- `declare(strict_types=1)` aggiunto
|
||||
- `firstOrCreate` per idempotenza
|
||||
2. **`build-dist.sh`** (`post-deploy.sh` generato):
|
||||
- Upgrade: aggiunto step `[6/10] Seed diocesi` dopo migrazioni (rinumerati da [1-5/9] → [1-10/10])
|
||||
- Install: aggiunto step `[9/11] Seed diocesi` dopo migrazioni (rinumerato da [1-8/10] → [1-11/11])
|
||||
- Usa `--force` per bypassare conferma in produzione
|
||||
|
||||
**File modificati**:
|
||||
- `database/seeders/DiocesiSeeder.php` — riscritto con 225 nomi + fix encoding
|
||||
- `build-dist.sh` — aggiunto step seed diocesi in upgrade/install
|
||||
|
||||
## 2026-06-23 — Fix 419: SESSION_DRIVER=database + diagnose CSRF su IP locale
|
||||
|
||||
**Problema**: Accesso via IP di rete locale (`http://192.168.222.174`) poteva causare 419 Page Expired. Sessione su file vulnerabile a permessi/LOCK del filesystem.
|
||||
|
||||
**Fix**:
|
||||
1. **`SESSION_DRIVER=database`**: cambiato da `file` a `database`. La sessione su DB non soffre di:
|
||||
- Permessi filesystem errati
|
||||
- Lock concorrente su file
|
||||
- Pulizia sessioni scadute lottery-based
|
||||
2. **Migration `2026_06_23_000001_create_sessions_table.php`**: con guard `Schema::hasTable` per idempotenza cross-DB
|
||||
3. **`diagnose.php`**: aggiunto test CSRF su IP locale automatico. Rileva `hostname -I` ed esegue POST login → verifica 419 su `http://<lan-ip>/login`
|
||||
4. **`.env` e `.env.example`**: `SESSION_DRIVER=file` → `SESSION_DRIVER=database`
|
||||
5. Vecchi file di sessione in `storage/framework/sessions/` eliminati
|
||||
|
||||
**Risultato**:
|
||||
- diagnose.php: `✅ HTTP: POST login → HTTP 302 (nessun 419)`
|
||||
- diagnose.php: `✅ LOCAL IP: POST login → HTTP 302 (nessun 419)`
|
||||
- Sessioni persistono in tabella `sessions` (DB) invece di file
|
||||
|
||||
**File modificati**:
|
||||
- `database/migrations/2026_06_23_000001_create_sessions_table.php` (nuovo)
|
||||
- `.env` — SESSION_DRIVER=database
|
||||
- `.env.example` — SESSION_DRIVER=database
|
||||
- `diagnose.php` — test CSRF su IP locale
|
||||
|
||||
## 2026-06-23 — Email compose: firma dopo corpo + Mailing list: mittente e firma
|
||||
|
||||
**Obiettivo**: Spostare la select firma dopo il corpo email nella pagina di composizione email. Aggiungere mittente predefinito (`sender_account_id`) alle mailing list (salvato in creazione/edit, usato come fallback all'invio).
|
||||
|
||||
### Cosa è stato fatto
|
||||
|
||||
**Migration** (`2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php`):
|
||||
- Aggiunta colonna `sender_account_id` (FK → sender_accounts, nullOnDelete) a `mailing_lists`
|
||||
|
||||
**Model `MailingList.php`**:
|
||||
- `sender_account_id` aggiunto a `$fillable`
|
||||
- `senderAccount()`: nuova relazione BelongsTo → `SenderAccount`
|
||||
|
||||
**Controller `MailingListController.php`**:
|
||||
- `create()`: passa `$senderAccounts` (SenderAccount::active()->get()) alla view
|
||||
- `edit()`: passa `$senderAccounts` alla view, eager-load `senderAccount`
|
||||
- `store()`: validazione `sender_account_id` nullable|exists, salvato in create
|
||||
- `update()`: validazione `sender_account_id` nullable|exists, salvato in update
|
||||
|
||||
**View `mailing-liste/create.blade.php`**:
|
||||
- Aggiunto select per `sender_account_id` dopo il select firma
|
||||
|
||||
**View `mailing-liste/edit.blade.php`**:
|
||||
- Aggiunto select per `sender_account_id` dopo il select firma, con `selected` se uguale a `$mailingList->sender_account_id`
|
||||
|
||||
**View `email/compose.blade.php`**:
|
||||
- Spostato blocco firma (prima del body) → dopo il textarea body (prima della sezione allegati)
|
||||
|
||||
**Controller `MailingController.php`**:
|
||||
- `invia()`: fallback a `$lista->senderAccount` se `mittente_id` non fornito esplicitamente (stesso pattern del firma_id fallback già esistente)
|
||||
- `invioElabora()`: fallback a `mittente_id`/`firma_id` dalla mailing list se non forniti esplicitamente (solo quando una singola lista è selezionata)
|
||||
|
||||
**File modificati**:
|
||||
- `database/migrations/2026_06_23_000002_add_sender_account_id_to_mailing_lists_table.php` (nuovo)
|
||||
- `app/Models/MailingList.php` — fillable + senderAccount relazione
|
||||
- `app/Http/Controllers/MailingListController.php` — sender_account in create/edit/store/update
|
||||
- `app/Http/Controllers/MailingController.php` — fallback mittente/firma da mailing list
|
||||
- `resources/views/email/compose.blade.php` — firma spostata dopo il body
|
||||
- `resources/views/mailing-liste/create.blade.php` — select mittente aggiunto
|
||||
- `resources/views/mailing-liste/edit.blade.php` — select mittente aggiunto
|
||||
|
||||
## 2026-06-23 — Fix document move: modal event listener + PHP empty-ids guard
|
||||
|
||||
**Problema**: Spostare documenti (singolo via icona cartella o mass move via toolbar) mostrava "successo" ma non spostava nulla. Il documento rimaneva nella cartella originale.
|
||||
|
||||
**Root cause**:
|
||||
1. **JS — Bootstrap 4 event compatibility**: `addEventListener('show.bs.modal', ...)` al mass move modal usava native DOM listener, ma Bootstrap 4.6.2 dispone `show.bs.modal` come evento jQuery. Il listener non veniva mai eseguito → hidden input `massMoveIds` mai popolato → `explode(',', '')` → `['']` → `empty([''])` è `false` → `Documento::whereIn('id', [''])` (0 rows affected) → falso successo.
|
||||
2. **PHP — No guardia su array vuoto dopo explode**: `massMove()`, `massDownload()`, `massDestroy()`, `massAssociate()`, `massTag()` in `DocumentoController.php` non filtravano valori vuoti/non-numerici da `$ids`.
|
||||
|
||||
**Fix**:
|
||||
- `resources/views/documenti/index.blade.php:1306`: `document.getElementById('massMoveModal')?.addEventListener('show.bs.modal', ...)` → `$('#massMoveModal').on('show.bs.modal', ...)` (jQuery event, coerente con tutti gli altri listener nello stesso file)
|
||||
- `app/Http/Controllers/DocumentoController.php`: aggiunto `array_values(array_filter($ids, fn($v) => is_numeric($v)))` in `massMove()`, `massDownload()`, `massDestroy()`, `massAssociate()`, `massTag()` — filtra stringhe vuote e non-numeriche prima di ogni `whereIn`
|
||||
- `massMove()`: aggiunto controllo esplicito `count($ids)` per validare singolo documento nel flusso per-documento
|
||||
|
||||
**File modificati**:
|
||||
- `resources/views/documenti/index.blade.php` — `addEventListener` → jQuery `.on()` a riga 1306
|
||||
- `app/Http/Controllers/DocumentoController.php` — `array_filter` guard in 5 mass-action metodi
|
||||
|
||||
**Verifica**:
|
||||
- PHP lint: OK su controller
|
||||
- JS brace balance: OK (diff=0)
|
||||
- Tutti gli altri listener modal nel file usano già jQuery `.on()` — il fix allinea `massMoveModal` al pattern esistente
|
||||
- Nessun test automatizzato presente nel progetto (no Pest, no PHPUnit config)
|
||||
|
||||
## 2026-06-23 — Test + Fix migration per compatibilità SQLite + massDownload unlink fix
|
||||
|
||||
### Tests scritti
|
||||
- **`tests/Unit/DocumentoLinkTest.php`**: 6 unit test per `Documento::isLink()` — copre tutti i valori `tipo` (link, upload, allegato, null), verifica `url` fillable, verifica link-doc senza file fields
|
||||
- **`tests/Feature/DocumentoLinkTest.php`**: 14 feature test per link-doc CRUD:
|
||||
- `store` con link: creazione base, validazione required url, validazione formato url, skip file, set tipo=link, visibilità individuo, visibilità gruppo
|
||||
- `update`: modifica URL, preserva URL se non inviato, validazione formato
|
||||
- `download`: redirect away all'URL esterno
|
||||
- `preview`: vista fallback con header X-Preview-Fallback
|
||||
- `destroy`: delete senza chiamate Storage
|
||||
- `massDownload`: skip link-doc con errore "Nessun file disponibile"
|
||||
- **Totale**: 20 test, 45 assertion, tutti ✅ passanti su SQLite `:memory:`
|
||||
|
||||
### Fix migration per compatibilità SQLite (test cross-DB)
|
||||
3 migration non compatibili con SQLite fixate per permettere test su `:memory:`:
|
||||
|
||||
1. **`2026_05_12_000002_change_ruolo_to_multi.php`**:
|
||||
- Aggiunto `Schema::hasColumn('gruppo_individuo', 'ruolo_id')` guard prima di `dropForeign`/`dropColumn`
|
||||
- Aggiunto `dropIndex(['ruolo_id'])` prima di `dropColumn` (SQLite richiede drop index esplicito)
|
||||
|
||||
2. **`2024_01_01_000008_create_documenti_table.php`**:
|
||||
- `$table->enum('tipologia', [...])` → `$table->string('tipologia', 50)` (app-level validation)
|
||||
- `$table->enum('visibilita', [...])` → `$table->string('visibilita', 50)` (app-level validation)
|
||||
- `$table->string('file_path')` → `$table->string('file_path')->nullable()` (necessario per link-docs)
|
||||
|
||||
3. **`2026_05_11_000005_add_email_attachment_to_documenti.php`**: reso no-op (colonna già string, app-level validation)
|
||||
4. **`2026_05_13_072203_add_programma_locandina_to_tipologia_enum.php`**: reso no-op (colonna già string, app-level validation)
|
||||
|
||||
### Fix massDownload unlink edge case
|
||||
- **`app/Http/Controllers/DocumentoController.php:710`**: Aggiunto `file_exists($zipPath)` guard prima di `@unlink($zipPath)` — preveniva `ErrorException` quando ZipArchive non creava file su alcuni filesystem (documenti tutti link → addedCount=0 → file zip non creato fisicamente → unlink falliva)
|
||||
|
||||
### Fix produzione: file_path NOT NULL su MySQL
|
||||
- **Problema**: La colonna `file_path` su MySQL è `NOT NULL` (migrazione originale `$table->string('file_path')`). Inserendo un link-doc (`tipo='link'`) senza `file_path`, MySQL rifiuta: `Field 'file_path' doesn't have a default value`.
|
||||
- **Fix**: Nuova migration `2026_06_23_085637_make_file_path_nullable_in_documenti.php` → `ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NULL` su MySQL
|
||||
- **Eseguita** localmente ✅ (80ms)
|
||||
|
||||
### build-dist.sh
|
||||
- Nessuna modifica necessaria: `php artisan migrate --force` è già presente in `post-deploy.sh` sia per upgrade (`[5/10]`) che per install (`[8/11]`)
|
||||
|
||||
### Prossimi Passi
|
||||
- ✅ Eseguire `php artisan migrate` su produzione per rendere `file_path` nullable
|
||||
- Verificare in ambiente di staging: creazione link-doc (tutte le visibilità), visualizzazione grid/list, preview fallback, download (redirect), modifica URL, eliminazione, mass-download con skip link, email/mailing con link nel corpo
|
||||
- Test edge case: URL malformato (validazione update), link senza url, link con cartella_id, link con visibilità individuo/gruppo
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Proxy Configuration — Glastree
|
||||
|
||||
## Nginx Proxy Manager (Custom Nginx Configuration)
|
||||
|
||||
Nella scheda **Advanced** del tuo proxy host:
|
||||
|
||||
```nginx
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `.env` (App Laravel)
|
||||
|
||||
```env
|
||||
APP_URL=https://glastree.soon.it
|
||||
APP_PROTOCOL=https
|
||||
FORCE_HTTPS=true
|
||||
TRUSTED_PROXIES=*
|
||||
```
|
||||
|
||||
Dopo qualsiasi modifica:
|
||||
|
||||
```bash
|
||||
php artisan config:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Google OAuth — Authorized Redirect URIs
|
||||
|
||||
```
|
||||
https://glastree.soon.it/auth/google/callback
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `bootstrap/app.php`
|
||||
|
||||
```php
|
||||
$middleware->trustProxies(at: '*');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schema richieste
|
||||
|
||||
```
|
||||
Cliente (HTTPS)
|
||||
↓
|
||||
Nginx Proxy Manager (termina TLS, inoltra HTTP + X-Forwarded-Proto: https)
|
||||
↓
|
||||
App Laravel (vede richieste come HTTPS grazie a trustProxies(at:"*"))
|
||||
```
|
||||
@@ -28,16 +28,17 @@ class BackupRunCommand extends Command
|
||||
{
|
||||
$this->info('Avvio backup...');
|
||||
|
||||
$options = [];
|
||||
if ($this->option('no-files')) {
|
||||
$this->backupService->saveConfig(['backup_include_files' => false]);
|
||||
$options['include_files'] = false;
|
||||
$this->warn('Files esclusi dal backup.');
|
||||
}
|
||||
if ($this->option('no-env')) {
|
||||
$this->backupService->saveConfig(['backup_include_env' => false]);
|
||||
$options['include_env'] = false;
|
||||
$this->warn('.env escluso dal backup.');
|
||||
}
|
||||
|
||||
$result = $this->backupService->run();
|
||||
$result = $this->backupService->run($options);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info(' Backup completato con successo!');
|
||||
@@ -45,6 +46,13 @@ class BackupRunCommand extends Command
|
||||
$this->line('Dimensione: ' . $result['size_formatted']);
|
||||
$this->line('Percorso: ' . storage_path('app/backups/' . $result['filename']));
|
||||
|
||||
if (!empty($result['steps'])) {
|
||||
$this->line('Contenuto:');
|
||||
foreach ($result['steps'] as $step) {
|
||||
$this->line(' - ' . $step);
|
||||
}
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum GoogleService: string
|
||||
{
|
||||
case Email = 'email';
|
||||
case Drive = 'drive';
|
||||
case Calendar = 'calendar';
|
||||
|
||||
public function scopes(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => ['https://mail.google.com/'],
|
||||
self::Drive => ['https://www.googleapis.com/auth/drive'],
|
||||
self::Calendar => ['https://www.googleapis.com/auth/calendar'],
|
||||
};
|
||||
}
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'Gmail',
|
||||
self::Drive => 'Google Drive',
|
||||
self::Calendar => 'Google Calendar',
|
||||
};
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'fas fa-envelope',
|
||||
self::Drive => 'fab fa-google-drive',
|
||||
self::Calendar => 'fas fa-calendar-alt',
|
||||
};
|
||||
}
|
||||
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Email => 'Invia e ricevi email tramite Gmail API (alternativa a SMTP/IMAP con password)',
|
||||
self::Drive => 'Accedi ai file su Google Drive per documenti e allegati',
|
||||
self::Calendar => 'Sincronizza eventi con Google Calendar',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,9 @@ class BackupController extends Controller
|
||||
$result = $this->backupService->run();
|
||||
|
||||
if ($result['success']) {
|
||||
$steps = !empty($result['steps']) ? ' — ' . implode(', ', $result['steps']) : '';
|
||||
return redirect()->route('admin.backup.index')
|
||||
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')');
|
||||
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')' . $steps);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.backup.index')
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EmailAttachment;
|
||||
use App\Models\EmailFolder;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Firma;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
class EmailSettingsController extends Controller
|
||||
{
|
||||
@@ -19,7 +24,9 @@ class EmailSettingsController extends Controller
|
||||
$this->authorizeWrite('settings');
|
||||
$settings = EmailSetting::first() ?? new EmailSetting();
|
||||
$senderAccounts = SenderAccount::orderBy('email_address')->get();
|
||||
return view('admin.email-settings.index', compact('settings', 'senderAccounts'));
|
||||
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
|
||||
$firme = $settings->exists ? $settings->firme : collect();
|
||||
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus', 'firme'));
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
@@ -38,11 +45,15 @@ class EmailSettingsController extends Controller
|
||||
'smtp_password' => 'nullable|string',
|
||||
'email_address' => 'required|email',
|
||||
'email_name' => 'nullable|string|max:255',
|
||||
'from_email' => 'nullable|email|max:255',
|
||||
'from_name' => 'nullable|string|max:255',
|
||||
'reply_to' => 'nullable|email',
|
||||
'sync_interval_minutes' => 'nullable|integer|min:1|max:60',
|
||||
'is_active' => 'boolean',
|
||||
'signature' => 'nullable|string',
|
||||
'signature_enabled' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($validated['imap_password'])) {
|
||||
@@ -61,29 +72,17 @@ class EmailSettingsController extends Controller
|
||||
$validated
|
||||
);
|
||||
|
||||
// Verifica che i dati siano stati salvati
|
||||
$verify = EmailSetting::find($saved->id);
|
||||
if ($verify &&
|
||||
|
||||
$allGood = $verify &&
|
||||
$verify->imap_host === $validated['imap_host'] &&
|
||||
$verify->email_address === $validated['email_address']) {
|
||||
$verify->email_address === $validated['email_address'];
|
||||
|
||||
$signatureMatch = !array_key_exists('signature', $validated) || $verify->signature === $validated['signature'];
|
||||
$signatureEnabledMatch = !array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled'];
|
||||
|
||||
if (!$signatureMatch) {
|
||||
\Illuminate\Support\Facades\Log::warning('Signature mismatch after save', [
|
||||
'expected' => $validated['signature'] ?? null,
|
||||
'actual' => $verify->signature,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$signatureEnabledMatch) {
|
||||
\Illuminate\Support\Facades\Log::warning('Signature enabled mismatch after save', [
|
||||
'expected' => $validated['signature_enabled'] ?? null,
|
||||
'actual' => $verify->signature_enabled,
|
||||
]);
|
||||
}
|
||||
$allGood = $allGood &&
|
||||
(!array_key_exists('signature', $validated) || $verify->signature === $validated['signature']) &&
|
||||
(!array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled']);
|
||||
|
||||
if ($allGood) {
|
||||
return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
|
||||
} else {
|
||||
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
|
||||
@@ -99,7 +98,6 @@ class EmailSettingsController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
|
||||
}
|
||||
|
||||
// Verifica che i dati siano salvati
|
||||
if (empty($settings->imap_host) || empty($settings->email_address)) {
|
||||
return response()->json(['success' => false, 'message' => 'Prima salva le impostazioni.']);
|
||||
}
|
||||
@@ -129,60 +127,27 @@ class EmailSettingsController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
|
||||
}
|
||||
|
||||
if (empty($settings->smtp_host)) {
|
||||
$settings->smtp_host = 'smtp.gmail.com';
|
||||
$settings->smtp_port = 587;
|
||||
$settings->smtp_encryption = 'tls';
|
||||
}
|
||||
|
||||
try {
|
||||
$smtpUsername = $settings->smtp_username;
|
||||
$smtpPassword = $settings->smtp_password;
|
||||
$mailer = $settings->getSmtpMailer();
|
||||
|
||||
if (empty($smtpUsername)) {
|
||||
$smtpUsername = $settings->email_address;
|
||||
}
|
||||
if (empty($smtpPassword)) {
|
||||
$smtpPassword = $settings->getDecryptedPassword();
|
||||
} else {
|
||||
$smtpPassword = Crypt::decryptString($smtpPassword);
|
||||
if ($mailer === null) {
|
||||
return response()->json(['success' => false, 'message' => 'Impossibile creare il mailer SMTP. Verifica la configurazione.']);
|
||||
}
|
||||
|
||||
$encryption = $settings->smtp_encryption ?? 'tls';
|
||||
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
|
||||
$smtpUser = $settings->smtp_username ?: $settings->email_address;
|
||||
$fromName = $settings->getEffectiveFromName();
|
||||
$replyToEmail = $settings->from_email ?: $smtpUser;
|
||||
|
||||
$dsn = sprintf(
|
||||
'%s://%s:%s@%s:%d?encryption=%s',
|
||||
$scheme,
|
||||
rawurlencode($smtpUsername),
|
||||
rawurlencode($smtpPassword),
|
||||
$settings->smtp_host,
|
||||
$settings->smtp_port ?? 587,
|
||||
$encryption
|
||||
);
|
||||
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
|
||||
$fromName = $settings->email_name ?? '';
|
||||
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
|
||||
$fromName = 'Glastree';
|
||||
} else {
|
||||
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
|
||||
$fromName = trim($fromName);
|
||||
if (empty($fromName) || strlen($fromName) > 50) {
|
||||
$fromName = 'Glastree';
|
||||
}
|
||||
}
|
||||
|
||||
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
|
||||
|
||||
$email = (new \Symfony\Component\Mime\Email())
|
||||
->from($fromAddress)
|
||||
$email = (new Email())
|
||||
->from(Address::create($smtpUser, $fromName))
|
||||
->to($request->email)
|
||||
->subject('Test Configurazione SMTP - Glastree')
|
||||
->text('Test email da Glastree - Configurazione SMTP funziona!');
|
||||
|
||||
if ($replyToEmail !== $smtpUser) {
|
||||
$email->replyTo(Address::create($replyToEmail, $fromName));
|
||||
}
|
||||
|
||||
$mailer->send($email);
|
||||
|
||||
\Illuminate\Support\Facades\Log::info('Test SMTP ok', ['to' => $request->email, 'from' => $settings->email_address]);
|
||||
@@ -227,6 +192,8 @@ class EmailSettingsController extends Controller
|
||||
'verify_email' => 'nullable|email|max:255',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'is_active' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($data['smtp_password'])) {
|
||||
@@ -257,6 +224,8 @@ class EmailSettingsController extends Controller
|
||||
'verify_email' => 'nullable|email|max:255',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'is_active' => 'boolean',
|
||||
'auth_method' => 'nullable|in:password,oauth',
|
||||
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
|
||||
]);
|
||||
|
||||
if (!empty($data['smtp_password'])) {
|
||||
@@ -326,4 +295,66 @@ class EmailSettingsController extends Controller
|
||||
return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function firmaStore(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'contenuto_html' => 'nullable|string',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
$settings = EmailSetting::getActive();
|
||||
if (!$settings) {
|
||||
return redirect('/impostazioni/email')->with('error', 'Account email non configurato.');
|
||||
}
|
||||
|
||||
$firma = $settings->firme()->create($data);
|
||||
|
||||
if ($data['is_default'] ?? false) {
|
||||
$settings->firme()->where('id', '!=', $firma->id)->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" creata.');
|
||||
}
|
||||
|
||||
public function firmaUpdate(Request $request, int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'contenuto_html' => 'nullable|string',
|
||||
'is_default' => 'boolean',
|
||||
]);
|
||||
|
||||
$firma = Firma::findOrFail($id);
|
||||
$firma->update($data);
|
||||
|
||||
if ($data['is_default'] ?? false) {
|
||||
Firma::where('email_setting_id', $firma->email_setting_id)
|
||||
->where('id', '!=', $firma->id)
|
||||
->update(['is_default' => false]);
|
||||
}
|
||||
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" aggiornata.');
|
||||
}
|
||||
|
||||
public function firmaSetDefault(int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$firma = Firma::findOrFail($id);
|
||||
Firma::where('email_setting_id', $firma->email_setting_id)->update(['is_default' => false]);
|
||||
$firma->update(['is_default' => true]);
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $firma->nome . '" impostata come predefinita.');
|
||||
}
|
||||
|
||||
public function firmaDestroy(int $id)
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
$firma = Firma::findOrFail($id);
|
||||
$nome = $firma->nome;
|
||||
$firma->delete();
|
||||
return redirect('/impostazioni/email#firme')->with('success', 'Firma "' . $nome . '" eliminata.');
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class CalendarioConnessioneController extends Controller
|
||||
$connessione->is_active = $request->boolean('is_active', true);
|
||||
|
||||
if (!empty($data['config'])) {
|
||||
$connessione->encryptAndSetConfig($data['config']);
|
||||
$connessione->encryptAndSetConfig($data['config'], $connessione->config ?? []);
|
||||
}
|
||||
|
||||
$connessione->save();
|
||||
|
||||
@@ -56,7 +56,7 @@ class DocumentoController extends Controller
|
||||
} elseif ($folderId) {
|
||||
$query->where('cartella_id', $folderId);
|
||||
} else {
|
||||
$query->whereNull('repository_id');
|
||||
$query->whereNull('repository_id')->whereNull('cartella_id');
|
||||
}
|
||||
|
||||
$documenti = $query->paginate(20);
|
||||
@@ -201,33 +201,43 @@ class DocumentoController extends Controller
|
||||
$tipologieValidi = TipologiaDocumento::opzioni();
|
||||
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
|
||||
|
||||
$data = $request->validate([
|
||||
$rules = [
|
||||
'nome_file' => 'required|string|max:255',
|
||||
'tipologia' => 'required|' . $tipologieRule,
|
||||
'visibilita' => 'nullable|string',
|
||||
'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing',
|
||||
'visibilita_target_id' => 'nullable|integer',
|
||||
'visibilita_target_type' => 'nullable|string',
|
||||
'note' => 'nullable|string',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
]);
|
||||
];
|
||||
|
||||
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
|
||||
if ($documento->isLink()) {
|
||||
$rules['url'] = 'nullable|url|max:2048';
|
||||
}
|
||||
|
||||
if ($request->contesto_tipo) {
|
||||
$data = $request->validate($rules);
|
||||
|
||||
$contestoTipo = $data['contesto_tipo'] ?? null;
|
||||
$data['visibilita'] = $contestoTipo ?: 'pubblico';
|
||||
|
||||
if ($contestoTipo) {
|
||||
$typeMap = [
|
||||
'individuo' => Individuo::class,
|
||||
'gruppo' => Gruppo::class,
|
||||
'evento' => Evento::class,
|
||||
'mailing' => \App\Models\MailingList::class,
|
||||
];
|
||||
$data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? null;
|
||||
$data['visibilita_target_type'] = $typeMap[$contestoTipo] ?? null;
|
||||
} else {
|
||||
$data['visibilita'] = 'pubblico';
|
||||
$data['visibilita_target_id'] = null;
|
||||
$data['visibilita_target_type'] = null;
|
||||
}
|
||||
|
||||
if ($documento->isLink()) {
|
||||
$data['url'] = $data['url'] ?? $documento->url;
|
||||
}
|
||||
|
||||
$documento->update($data);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
@@ -236,23 +246,70 @@ class DocumentoController extends Controller
|
||||
$documento->tags()->detach();
|
||||
}
|
||||
|
||||
return redirect('/documenti')->with('success', 'Documento aggiornato.');
|
||||
$redirect = $request->input('folder_id') ? '/documenti?folder_id=' . $request->input('folder_id') : '/documenti';
|
||||
return redirect($redirect)->with('success', 'Documento aggiornato.');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('documenti');
|
||||
$data = $request->validate([
|
||||
$tipologieValidi = TipologiaDocumento::opzioni();
|
||||
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
|
||||
|
||||
$isLink = $request->input('tipo_caricamento') === 'link';
|
||||
|
||||
$rules = [
|
||||
'nome_file' => 'required|string|max:255',
|
||||
'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro',
|
||||
'file' => 'required|file|max:10240',
|
||||
'tipologia' => 'required|' . $tipologieRule,
|
||||
'visibilita' => 'required|in:pubblico,individuo,gruppo',
|
||||
'visibilita_target_id' => 'nullable|integer',
|
||||
'visibilita_target_type' => 'nullable|string',
|
||||
'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id',
|
||||
'repository_id' => 'nullable|integer|exists:storage_repositories,id',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'tipo_caricamento' => 'required|in:file,link',
|
||||
'url' => 'required_if:tipo_caricamento,link|nullable|url|max:2048',
|
||||
];
|
||||
|
||||
if (!$isLink) {
|
||||
$rules['file'] = 'required|file|max:10240';
|
||||
}
|
||||
|
||||
$data = $request->validate($rules);
|
||||
|
||||
if ($isLink) {
|
||||
$linkVisibilitaTargetType = null;
|
||||
if ($data['visibilita'] === 'individuo' && $data['visibilita_target_id']) {
|
||||
$linkVisibilitaTargetType = Individuo::class;
|
||||
} elseif ($data['visibilita'] === 'gruppo' && $data['visibilita_target_id']) {
|
||||
$linkVisibilitaTargetType = Gruppo::class;
|
||||
}
|
||||
|
||||
$doc = Documento::create([
|
||||
'nome_file' => $data['nome_file'],
|
||||
'url' => $data['url'],
|
||||
'tipo' => 'link',
|
||||
'tipologia' => $data['tipologia'],
|
||||
'visibilita' => $data['visibilita'],
|
||||
'visibilita_target_id' => $data['visibilita_target_id'] ?? null,
|
||||
'visibilita_target_type' => $linkVisibilitaTargetType,
|
||||
'user_id' => auth()->id(),
|
||||
'cartella_id' => $data['cartella_id'] ?? null,
|
||||
'repository_id' => null,
|
||||
]);
|
||||
|
||||
if ($doc && $request->has('tags')) {
|
||||
$doc->tags()->sync($request->tags);
|
||||
}
|
||||
|
||||
$redirect = $request->_redirect ?? url()->previous();
|
||||
if ($request->filled('folder_id')) {
|
||||
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
|
||||
}
|
||||
return redirect($redirect)->with('success', 'Documento (link) creato.');
|
||||
}
|
||||
|
||||
$repositoryId = $data['repository_id'] ?? null;
|
||||
|
||||
if ($repositoryId) {
|
||||
@@ -284,7 +341,7 @@ class DocumentoController extends Controller
|
||||
$visibilitaTargetType = Gruppo::class;
|
||||
}
|
||||
|
||||
Documento::create([
|
||||
$doc = Documento::create([
|
||||
'nome_file' => $data['nome_file'],
|
||||
'file_path' => $filePath,
|
||||
'storage_disk' => $disk ?? null,
|
||||
@@ -300,7 +357,13 @@ class DocumentoController extends Controller
|
||||
'repository_id' => $repositoryId,
|
||||
]);
|
||||
|
||||
if ($doc && $request->has('tags')) {
|
||||
$doc->tags()->sync($request->tags);
|
||||
}
|
||||
$redirect = $request->_redirect ?? url()->previous();
|
||||
if ($request->filled('folder_id')) {
|
||||
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
|
||||
}
|
||||
return redirect($redirect)->with('success', 'Documento caricato.');
|
||||
}
|
||||
|
||||
@@ -309,6 +372,10 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->isLink()) {
|
||||
return redirect()->away($documento->url);
|
||||
}
|
||||
|
||||
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
|
||||
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
|
||||
|
||||
@@ -354,6 +421,12 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if ($documento->isLink()) {
|
||||
return response()->view('documenti.preview-fallback', [
|
||||
'documento' => $documento,
|
||||
])->header('X-Preview-Fallback', 'true');
|
||||
}
|
||||
|
||||
$previewableMimeTypes = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
||||
'application/pdf',
|
||||
@@ -404,6 +477,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeDelete('documenti');
|
||||
$documento = Documento::findOrFail($documento);
|
||||
|
||||
if (!$documento->isLink()) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
@@ -418,6 +492,7 @@ class DocumentoController extends Controller
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$documento->delete();
|
||||
|
||||
@@ -494,9 +569,10 @@ class DocumentoController extends Controller
|
||||
{
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return response()->json(['linked_count' => 0, 'links' => []]);
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
}
|
||||
|
||||
$documenti = Documento::whereIn('id', $ids)->get();
|
||||
@@ -572,6 +648,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeRead('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -590,8 +667,14 @@ class DocumentoController extends Controller
|
||||
return back()->with('error', 'Impossibile creare l\'archivio ZIP.');
|
||||
}
|
||||
|
||||
$skippedLinks = 0;
|
||||
$addedCount = 0;
|
||||
foreach ($documenti as $documento) {
|
||||
if ($documento->isLink()) {
|
||||
$skippedLinks++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$extension = pathinfo($documento->file_path, PATHINFO_EXTENSION);
|
||||
$filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file;
|
||||
|
||||
@@ -624,11 +707,23 @@ class DocumentoController extends Controller
|
||||
$zip->close();
|
||||
|
||||
if ($addedCount === 0) {
|
||||
unlink($zipPath);
|
||||
return back()->with('error', 'Nessun file disponibile per il download.');
|
||||
if (file_exists($zipPath)) {
|
||||
@unlink($zipPath);
|
||||
}
|
||||
$message = 'Nessun file disponibile per il download.';
|
||||
if ($skippedLinks > 0) {
|
||||
$message .= " ({$skippedLinks} link esterni saltati)";
|
||||
}
|
||||
return back()->with('error', $message);
|
||||
}
|
||||
|
||||
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true);
|
||||
$message = $addedCount . ' file scaricati.';
|
||||
if ($skippedLinks > 0) {
|
||||
$message .= " {$skippedLinks} link esterni saltati.";
|
||||
}
|
||||
|
||||
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true)
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
public function massDestroy(Request $request)
|
||||
@@ -636,6 +731,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeDelete('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -644,6 +740,7 @@ class DocumentoController extends Controller
|
||||
$documenti = Documento::whereIn('id', $ids)->get();
|
||||
|
||||
foreach ($documenti as $documento) {
|
||||
if (!$documento->isLink()) {
|
||||
if ($documento->repository_id) {
|
||||
$repo = StorageRepository::find($documento->repository_id);
|
||||
if ($repo) {
|
||||
@@ -658,6 +755,7 @@ class DocumentoController extends Controller
|
||||
Storage::disk($disk)->delete($documento->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
$documento->delete();
|
||||
}
|
||||
|
||||
@@ -731,6 +829,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeWrite('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
@@ -762,6 +861,7 @@ class DocumentoController extends Controller
|
||||
$this->authorizeWrite('documenti');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
$ids = array_values(array_filter($ids, fn($v) => is_numeric($v)));
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun documento selezionato.');
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\MailingList;
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Documento;
|
||||
use App\Models\Firma;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -96,6 +97,9 @@ class EmailController extends Controller
|
||||
|
||||
$documenti = Documento::orderBy('nome_file')->get();
|
||||
|
||||
$settings = EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
|
||||
$prefill = [];
|
||||
if ($request->reply_to) {
|
||||
$original = EmailMessage::find($request->reply_to);
|
||||
@@ -113,7 +117,7 @@ class EmailController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts'));
|
||||
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts', 'firme'));
|
||||
}
|
||||
|
||||
public function send(Request $request)
|
||||
@@ -128,6 +132,7 @@ class EmailController extends Controller
|
||||
$rules['to'] = 'required';
|
||||
}
|
||||
|
||||
$rules['firma_id'] = 'nullable|integer|exists:firme,id';
|
||||
$validated = $request->validate($rules);
|
||||
|
||||
$recipients = $this->resolveRecipients($request);
|
||||
@@ -157,12 +162,18 @@ class EmailController extends Controller
|
||||
|
||||
$imapSuccess = false;
|
||||
$imapError = null;
|
||||
$attachments = $this->processAttachments($request);
|
||||
$attachmentResult = $this->processAttachments($request);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$firmaId = $request->firma_id;
|
||||
|
||||
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
try {
|
||||
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
|
||||
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments);
|
||||
$bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
|
||||
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachmentPaths);
|
||||
$imapSuccess = true;
|
||||
} catch (\Exception $e) {
|
||||
$imapError = $e->getMessage();
|
||||
@@ -171,7 +182,7 @@ class EmailController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
|
||||
$bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
|
||||
$this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
|
||||
@@ -188,13 +199,20 @@ class EmailController extends Controller
|
||||
private function sendWithSender(Request $request, array $validated, array $recipients)
|
||||
{
|
||||
$sender = SenderAccount::findOrFail($request->mittente_id);
|
||||
$attachmentPaths = $this->resolveAttachmentPaths($request);
|
||||
$attachmentResult = $this->resolveAttachmentPaths($request);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$settings = EmailSetting::getActive();
|
||||
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
|
||||
$body = $this->appendSignature($body, $request->firma_id, $settings);
|
||||
|
||||
$ok = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($recipients as $recipient) {
|
||||
try {
|
||||
$sender->sendEmail($recipient, $validated['subject'], $validated['body'], $attachmentPaths);
|
||||
$sender->sendEmail($recipient, $validated['subject'], $body, $attachmentPaths);
|
||||
$ok++;
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = $recipient . ': ' . $e->getMessage();
|
||||
@@ -237,8 +255,8 @@ class EmailController extends Controller
|
||||
'body_text' => $request->body,
|
||||
'to_email' => $request->to,
|
||||
'is_draft' => true,
|
||||
'from_email' => $settings->email_address ?? '',
|
||||
'from_name' => $settings->email_name ?? '',
|
||||
'from_email' => $settings->getEffectiveFromAddress(),
|
||||
'from_name' => $settings->getEffectiveFromName(),
|
||||
]);
|
||||
|
||||
return response()->json(['success' => true, 'draft_id' => $message->id]);
|
||||
@@ -496,12 +514,20 @@ class EmailController extends Controller
|
||||
return array_filter(array_unique($recipients));
|
||||
}
|
||||
|
||||
private function appendSignature(string $body, ?string $signature, bool $enabled): string
|
||||
private function appendSignature(string $body, int|string|null $firmaId = null, ?EmailSetting $settings = null): string
|
||||
{
|
||||
if (empty($signature) || !$enabled) {
|
||||
return $body;
|
||||
if ($firmaId) {
|
||||
$firma = Firma::find($firmaId);
|
||||
if ($firma && $firma->contenuto_html) {
|
||||
return $body . "\n\n---\n" . $firma->contenuto_html;
|
||||
}
|
||||
return $body . "\n\n---\n" . $signature;
|
||||
}
|
||||
|
||||
if ($settings && $settings->signature && ($settings->signature_enabled ?? false)) {
|
||||
return $body . "\n\n---\n" . $settings->signature;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = [])
|
||||
@@ -527,18 +553,10 @@ class EmailController extends Controller
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
|
||||
$fromName = $settings->email_name ?? '';
|
||||
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
|
||||
$fromName = 'Glastree';
|
||||
} else {
|
||||
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
|
||||
$fromName = trim($fromName);
|
||||
if (empty($fromName)) {
|
||||
$fromName = 'Glastree';
|
||||
}
|
||||
}
|
||||
|
||||
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
|
||||
$fromAddress = \Symfony\Component\Mime\Address::create(
|
||||
$settings->getEffectiveFromAddress(),
|
||||
$settings->getEffectiveFromName()
|
||||
);
|
||||
|
||||
$email = (new \Symfony\Component\Mime\Email())
|
||||
->from($fromAddress)
|
||||
@@ -546,6 +564,11 @@ class EmailController extends Controller
|
||||
->subject($subject)
|
||||
->text($body);
|
||||
|
||||
$replyTo = $settings->getEffectiveReplyTo();
|
||||
if ($replyTo) {
|
||||
$email->replyTo($replyTo);
|
||||
}
|
||||
|
||||
if ($cc) {
|
||||
$email->cc($cc);
|
||||
}
|
||||
@@ -567,6 +590,7 @@ class EmailController extends Controller
|
||||
private function processAttachments(Request $request): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
|
||||
if ($request->hasFile('allegati')) {
|
||||
foreach ($request->file('allegati') as $file) {
|
||||
@@ -581,18 +605,21 @@ class EmailController extends Controller
|
||||
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
|
||||
$docs = Documento::whereIn('id', $ids)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
$paths[] = Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function resolveAttachmentPaths(Request $request): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
|
||||
if ($request->hasFile('allegati')) {
|
||||
foreach ($request->file('allegati') as $file) {
|
||||
@@ -607,26 +634,42 @@ class EmailController extends Controller
|
||||
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
|
||||
$docs = Documento::whereIn('id', $ids)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
|
||||
$paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function appendLinksToBody(string $body, array $linkDocs): string
|
||||
{
|
||||
if (empty($linkDocs)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$linksText = "\n\n---\nDocumenti collegati:\n";
|
||||
foreach ($linkDocs as $link) {
|
||||
$linksText .= "- {$link['nome']}: {$link['url']}\n";
|
||||
}
|
||||
|
||||
return $body . $linksText;
|
||||
}
|
||||
|
||||
private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null)
|
||||
{
|
||||
$sentFolder = EmailFolder::where('type', 'sent')->first();
|
||||
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false);
|
||||
$bodyWithSignature = $body;
|
||||
|
||||
$message = EmailMessage::create([
|
||||
'email_folder_id' => $sentFolder->id,
|
||||
'subject' => $subject,
|
||||
'body_text' => $bodyWithSignature,
|
||||
'from_email' => $settings->email_address,
|
||||
'from_name' => $settings->email_name,
|
||||
'from_email' => $settings->getEffectiveFromAddress(),
|
||||
'from_name' => $settings->getEffectiveFromName(),
|
||||
'to_email' => implode(', ', $recipients),
|
||||
'cc' => $cc,
|
||||
'is_sent' => true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Evento;
|
||||
@@ -630,4 +632,39 @@ class EventoController extends Controller
|
||||
|
||||
return redirect('/eventi')->with('success', $message);
|
||||
}
|
||||
|
||||
public function massTag(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('eventi');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessun evento selezionato.');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'tags' => 'required|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'mode' => 'required|in:assign,remove',
|
||||
]);
|
||||
|
||||
$tagIds = $data['tags'];
|
||||
$mode = $data['mode'];
|
||||
$count = 0;
|
||||
|
||||
Evento::whereIn('id', $ids)->chunk(100, function ($eventi) use ($tagIds, $mode, &$count) {
|
||||
foreach ($eventi as $evento) {
|
||||
if ($mode === 'assign') {
|
||||
$evento->tags()->syncWithoutDetaching($tagIds);
|
||||
} else {
|
||||
$evento->tags()->detach($tagIds);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||
return back()->with('success', "Tag $actionLabel per $count eventi.");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Evento;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Models\GoogleOAuthConnection;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GoogleOAuthController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GoogleOAuthService $googleOAuthService,
|
||||
) {}
|
||||
|
||||
public function redirect(string $service): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$serviceEnum = GoogleService::from($service);
|
||||
} catch (\ValueError $e) {
|
||||
return redirect()->back()->withErrors(['error' => 'Servizio Google non valido: ' . $service]);
|
||||
}
|
||||
|
||||
$authUrl = $this->googleOAuthService->getAuthUrl($service);
|
||||
|
||||
return redirect()->away($authUrl);
|
||||
}
|
||||
|
||||
public function callback(Request $request): RedirectResponse
|
||||
{
|
||||
$error = $request->input('error');
|
||||
if ($error) {
|
||||
$message = match ($error) {
|
||||
'access_denied' => 'Accesso negato. Autorizzazione non concessa.',
|
||||
default => 'Errore Google: ' . $error,
|
||||
};
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => $message]);
|
||||
}
|
||||
|
||||
$code = $request->input('code');
|
||||
if (!$code) {
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Nessun codice di autorizzazione ricevuto']);
|
||||
}
|
||||
|
||||
$state = $request->input('state', 'email');
|
||||
try {
|
||||
$service = GoogleService::from($state);
|
||||
} catch (\ValueError $e) {
|
||||
$service = GoogleService::Email;
|
||||
}
|
||||
|
||||
try {
|
||||
$connection = $this->googleOAuthService->handleCallback($code, $service);
|
||||
|
||||
$message = match ($service) {
|
||||
GoogleService::Email => 'Account email Gmail connesso con successo!',
|
||||
GoogleService::Drive => 'Account Google Drive connesso con successo!',
|
||||
GoogleService::Calendar => 'Account Google Calendar connesso con successo!',
|
||||
};
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->with('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth callback error', [
|
||||
'error' => $e->getMessage(),
|
||||
'service' => $service->value,
|
||||
]);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Errore durante la connessione: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function revoke(GoogleOAuthConnection $connection): RedirectResponse
|
||||
{
|
||||
if ($connection->user_id !== auth()->id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$service = GoogleService::from($connection->service);
|
||||
|
||||
$message = match ($service) {
|
||||
GoogleService::Email => 'Account email Gmail disconnesso.',
|
||||
GoogleService::Drive => 'Account Google Drive disconnesso.',
|
||||
GoogleService::Calendar => 'Account Google Calendar disconnesso.',
|
||||
};
|
||||
|
||||
$this->googleOAuthService->revoke($connection);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->with('success', $message);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth revoke error', [
|
||||
'error' => $e->getMessage(),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
return redirect()->route('impostazioni.index', ['tab' => 'google'])
|
||||
->withErrors(['error' => 'Errore durante la disconnessione: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function status(): \Illuminate\Http\JsonResponse
|
||||
{
|
||||
$status = $this->googleOAuthService->getConnectionStatus();
|
||||
|
||||
return response()->json($status);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GruppoController extends Controller
|
||||
@@ -36,6 +37,7 @@ class GruppoController extends Controller
|
||||
|
||||
$allColumnDefs = [
|
||||
['key' => 'nome', 'label' => 'Nome'],
|
||||
['key' => 'anno_fondazione', 'label' => 'Anno Fondazione'],
|
||||
['key' => 'descrizione', 'label' => 'Descrizione'],
|
||||
['key' => 'diocesi', 'label' => 'Diocesi'],
|
||||
['key' => 'livello', 'label' => 'Livello'],
|
||||
@@ -65,7 +67,7 @@ class GruppoController extends Controller
|
||||
$entityType = 'gruppi';
|
||||
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
|
||||
|
||||
$gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']);
|
||||
$gruppiQuery = Gruppo::with(['diocesi', 'gruppoContatti', 'parent', 'children', 'individui', 'avatar', 'tags']);
|
||||
|
||||
if (request()->filled('tag')) {
|
||||
$tagSlugs = (array) request()->input('tag');
|
||||
@@ -141,7 +143,7 @@ class GruppoController extends Controller
|
||||
public function create(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('gruppi');
|
||||
$diocesi = Diocesi::orderBy('nome')->get();
|
||||
$diocesi = Diocesi::orderBy('id')->get();
|
||||
$allGruppi = Gruppo::orderBy('nome')->get();
|
||||
$selectedParent = $request->query('parent_id') ? Gruppo::find($request->query('parent_id')) : null;
|
||||
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
|
||||
@@ -154,9 +156,11 @@ class GruppoController extends Controller
|
||||
$this->authorizeWrite('gruppi');
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
|
||||
'descrizione' => 'nullable|string',
|
||||
'parent_id' => 'nullable|exists:gruppi,id',
|
||||
'diocesi_id' => 'nullable|exists:diocesi,id',
|
||||
'diocesi_ids' => 'nullable|array',
|
||||
'diocesi_ids.*' => 'exists:diocesi,id',
|
||||
'responsabile_ids' => 'nullable|array',
|
||||
'responsabile_ids.*' => 'exists:individui,id',
|
||||
'indirizzo_incontro' => 'nullable|string|max:500',
|
||||
@@ -164,6 +168,11 @@ class GruppoController extends Controller
|
||||
'città_incontro' => 'nullable|string|max:255',
|
||||
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
||||
'mappa_posizione' => 'nullable|string',
|
||||
'contatti' => 'nullable|array',
|
||||
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
|
||||
'contatti.*.valore' => 'required|string|max:255',
|
||||
'contatti.*.etichetta' => 'nullable|string|max:255',
|
||||
'contatti.*.is_primary' => 'nullable|boolean',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
]);
|
||||
@@ -174,6 +183,21 @@ class GruppoController extends Controller
|
||||
|
||||
$gruppo = Gruppo::create($data);
|
||||
|
||||
if (!empty($data['diocesi_ids'])) {
|
||||
$gruppo->diocesi()->sync($data['diocesi_ids']);
|
||||
}
|
||||
|
||||
if (!empty($data['contatti'])) {
|
||||
foreach ($data['contatti'] as $contattoData) {
|
||||
$gruppo->gruppoContatti()->create([
|
||||
'tipo' => $contattoData['tipo'],
|
||||
'valore' => $contattoData['valore'],
|
||||
'etichetta' => $contattoData['etichetta'] ?? null,
|
||||
'is_primary' => $contattoData['is_primary'] ?? false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('tags')) {
|
||||
$gruppo->tags()->sync($request->tags);
|
||||
}
|
||||
@@ -195,7 +219,7 @@ class GruppoController extends Controller
|
||||
public function show($id)
|
||||
{
|
||||
$this->authorizeRead('gruppi');
|
||||
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
|
||||
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'gruppoContatti', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
|
||||
$q->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione');
|
||||
}, 'avatar', 'tags', 'eventi' => function ($q) {
|
||||
$q->where('is_incontro_gruppo', true);
|
||||
@@ -206,13 +230,14 @@ class GruppoController extends Controller
|
||||
public function edit($id)
|
||||
{
|
||||
$this->authorizeWrite('gruppi');
|
||||
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar', 'tags'])->findOrFail($id);
|
||||
$diocesi = Diocesi::orderBy('nome')->get();
|
||||
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'gruppoContatti', 'avatar', 'tags'])->findOrFail($id);
|
||||
$diocesi = Diocesi::orderBy('id')->get();
|
||||
$gruppi = Gruppo::where('id', '!=', $gruppo->id)->orderBy('nome')->get();
|
||||
$membri = $gruppo->individui()->withPivot('ruolo_ids', 'ruolo_nel_gruppo', 'data_adesione')->orderBy('cognome')->orderBy('nome')->get();
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
$selectedTags = $gruppo->tags->pluck('id')->toArray();
|
||||
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags'));
|
||||
$selectedDiocesiIds = $gruppo->diocesi()->pluck('diocesi.id')->toArray();
|
||||
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags', 'selectedDiocesiIds'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
@@ -221,9 +246,11 @@ class GruppoController extends Controller
|
||||
$gruppo = Gruppo::findOrFail($id);
|
||||
$data = $request->validate([
|
||||
'nome' => 'required|string|max:255',
|
||||
'anno_fondazione' => 'nullable|integer|min:1800|max:' . (date('Y') + 1),
|
||||
'descrizione' => 'nullable|string',
|
||||
'parent_id' => 'nullable|exists:gruppi,id',
|
||||
'diocesi_id' => 'nullable|exists:diocesi,id',
|
||||
'diocesi_ids' => 'nullable|array',
|
||||
'diocesi_ids.*' => 'exists:diocesi,id',
|
||||
'responsabile_ids' => 'nullable|array',
|
||||
'responsabile_ids.*' => 'exists:individui,id',
|
||||
'indirizzo_incontro' => 'nullable|string|max:500',
|
||||
@@ -231,6 +258,11 @@ class GruppoController extends Controller
|
||||
'città_incontro' => 'nullable|string|max:255',
|
||||
'sigla_provincia_incontro' => 'nullable|string|max:2',
|
||||
'mappa_posizione' => 'nullable|string',
|
||||
'contatti' => 'nullable|array',
|
||||
'contatti.*.tipo' => 'required|string|in:email,telefono,cellulare',
|
||||
'contatti.*.valore' => 'required|string|max:255',
|
||||
'contatti.*.etichetta' => 'nullable|string|max:255',
|
||||
'contatti.*.is_primary' => 'nullable|boolean',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
]);
|
||||
@@ -243,6 +275,24 @@ class GruppoController extends Controller
|
||||
|
||||
$gruppo->update($data);
|
||||
|
||||
if ($request->has('diocesi_ids')) {
|
||||
$gruppo->diocesi()->sync($data['diocesi_ids'] ?? []);
|
||||
} else {
|
||||
$gruppo->diocesi()->detach();
|
||||
}
|
||||
|
||||
if ($request->has('contatti')) {
|
||||
$gruppo->gruppoContatti()->delete();
|
||||
foreach ($data['contatti'] as $contattoData) {
|
||||
$gruppo->gruppoContatti()->create([
|
||||
'tipo' => $contattoData['tipo'],
|
||||
'valore' => $contattoData['valore'],
|
||||
'etichetta' => $contattoData['etichetta'] ?? null,
|
||||
'is_primary' => $contattoData['is_primary'] ?? false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->has('tags')) {
|
||||
$gruppo->tags()->sync($request->tags);
|
||||
} else {
|
||||
@@ -385,9 +435,12 @@ class GruppoController extends Controller
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->path(), 'r');
|
||||
$header = fgetcsv($handle, 1000, ',');
|
||||
$header = fgetcsv($handle, 0, ',');
|
||||
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
@@ -400,7 +453,9 @@ class GruppoController extends Controller
|
||||
$errors = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
|
||||
DB::beginTransaction();
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
if (count($row) < count($header)) {
|
||||
@@ -426,6 +481,7 @@ class GruppoController extends Controller
|
||||
try {
|
||||
Gruppo::create([
|
||||
'nome' => $nome,
|
||||
'anno_fondazione' => !empty($data['anno_fondazione']) ? (int) $data['anno_fondazione'] : null,
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'parent_id' => !empty($data['parent_id']) ? (int) $data['parent_id'] : null,
|
||||
'diocesi_id' => !empty($data['diocesi_id']) ? (int) $data['diocesi_id'] : null,
|
||||
@@ -444,6 +500,8 @@ class GruppoController extends Controller
|
||||
|
||||
fclose($handle);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$message = 'Importati ' . $imported . ' gruppi.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||
@@ -458,8 +516,8 @@ class GruppoController extends Controller
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$this->authorizeRead('gruppi');
|
||||
$headers = ['nome', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro'];
|
||||
$example = ['Gruppo Giovani', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
|
||||
$headers = ['nome', 'anno_fondazione', 'descrizione', 'parent_id', 'diocesi_id', 'indirizzo_incontro', 'cap_incontro', 'città_incontro', 'sigla_provincia_incontro'];
|
||||
$example = ['Gruppo Giovani', '2020', 'Giovani dai 18 ai 30 anni', '', '', 'Via Roma 10', '00100', 'Roma', 'RM'];
|
||||
|
||||
$csv = implode(',', $headers) . "\n" . implode(',', $example);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Ruolo;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Models\StorageRepository;
|
||||
use App\Models\Tag;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use App\Models\TipologiaDocumento;
|
||||
use App\Models\TipologiaEvento;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -34,8 +35,9 @@ class ImpostazioniController extends Controller
|
||||
$repositories = StorageRepository::orderBy('ordine')->get();
|
||||
$googleDriveNewToken = session('google_drive_new_token');
|
||||
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
|
||||
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
|
||||
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
|
||||
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni', 'googleStatus'));
|
||||
}
|
||||
|
||||
public function saveAppSettings(Request $request)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Individuo;
|
||||
@@ -11,6 +13,8 @@ use App\Models\Documento;
|
||||
use App\Models\Tag;
|
||||
use App\Models\VistaReport;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
@@ -148,7 +152,7 @@ public function create()
|
||||
{
|
||||
$this->authorizeWrite('individui');
|
||||
$comuni = Comune::orderBy('nome')->get();
|
||||
$diocesi = Diocesi::orderBy('nome')->get();
|
||||
$diocesi = Diocesi::orderBy('id')->get();
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
return view('individui.create', compact('comuni', 'diocesi', 'tags'));
|
||||
}
|
||||
@@ -497,16 +501,27 @@ public function emailList(Request $request)
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
set_time_limit(0);
|
||||
session_write_close();
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->path(), 'r');
|
||||
$header = fgetcsv($handle, 1000, ',');
|
||||
$header = fgetcsv($handle, 0, ',');
|
||||
|
||||
if ($header === false) {
|
||||
fclose($handle);
|
||||
return back()->with('error', 'Il file CSV non è valido.');
|
||||
}
|
||||
|
||||
$header = array_map('trim', $header);
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$errors = [];
|
||||
$rowNumber = 1;
|
||||
|
||||
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
|
||||
DB::beginTransaction();
|
||||
|
||||
while (($row = fgetcsv($handle, 0, ',')) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
if (count($row) < count($header)) {
|
||||
@@ -565,12 +580,15 @@ public function emailList(Request $request)
|
||||
|
||||
$imported++;
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Errore import individuo riga ' . $rowNumber . ': ' . $e->getMessage());
|
||||
$errors[] = 'Errore riga ' . $rowNumber . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
DB::commit();
|
||||
|
||||
$message = 'Importati ' . $imported . ' individui.';
|
||||
if ($skipped > 0) {
|
||||
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
|
||||
|
||||
@@ -10,6 +10,8 @@ use App\Models\MailingMessaggio;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Documento;
|
||||
use App\Models\EmailSetting;
|
||||
use App\Models\Firma;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -30,6 +32,8 @@ class MailingController extends Controller
|
||||
$liste = MailingList::all();
|
||||
$documenti = Documento::orderBy('nome_file')->get();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
$settings = EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
|
||||
$documentiSelezionati = collect();
|
||||
if ($request->has('documenti_selezionati')) {
|
||||
@@ -37,7 +41,7 @@ class MailingController extends Controller
|
||||
$documentiSelezionati = Documento::whereIn('id', $docIds)->get();
|
||||
}
|
||||
|
||||
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts'));
|
||||
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts', 'firme'));
|
||||
}
|
||||
|
||||
public function invia(Request $request)
|
||||
@@ -46,16 +50,22 @@ class MailingController extends Controller
|
||||
$data = $request->validate([
|
||||
'oggetto' => 'required|string|max:255',
|
||||
'corpo' => 'required',
|
||||
'lista_id' => 'nullable|exists:mailing_liste,id',
|
||||
'lista_id' => 'nullable|exists:mailing_lists,id',
|
||||
'destinatari_ids' => 'nullable|string',
|
||||
'documenti_selezionati' => 'nullable|string',
|
||||
'allegato' => 'nullable|file|max:10240',
|
||||
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
]);
|
||||
|
||||
$sender = null;
|
||||
if (!empty($data['mittente_id'])) {
|
||||
$sender = SenderAccount::findOrFail($data['mittente_id']);
|
||||
} elseif (!empty($data['lista_id'])) {
|
||||
$lista = MailingList::with('senderAccount')->find($data['lista_id']);
|
||||
if ($lista && $lista->senderAccount) {
|
||||
$sender = $lista->senderAccount;
|
||||
}
|
||||
}
|
||||
|
||||
$documentiAllegati = [];
|
||||
@@ -110,7 +120,23 @@ class MailingController extends Controller
|
||||
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
|
||||
}
|
||||
|
||||
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$firmaId = $data['firma_id'] ?? null;
|
||||
if (!$firmaId && !empty($data['lista_id'])) {
|
||||
$lista = MailingList::with('firma')->find($data['lista_id']);
|
||||
$firmaId = $lista?->firma?->id;
|
||||
}
|
||||
|
||||
$corpoConFirma = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
|
||||
if ($firmaId) {
|
||||
$firma = Firma::find($firmaId);
|
||||
if ($firma && $firma->contenuto_html) {
|
||||
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
|
||||
}
|
||||
}
|
||||
|
||||
$messaggio = MailingMessaggio::create([
|
||||
'mailing_list_id' => $data['lista_id'] ?? null,
|
||||
@@ -121,6 +147,7 @@ class MailingController extends Controller
|
||||
'totale_destinatari' => $emails->count(),
|
||||
'mittente_nome' => $sender?->email_name,
|
||||
'mittente_email' => $sender?->email_address,
|
||||
'firma_id' => $firmaId,
|
||||
]);
|
||||
|
||||
$ok = 0;
|
||||
@@ -130,9 +157,9 @@ class MailingController extends Controller
|
||||
foreach ($emails as $email) {
|
||||
try {
|
||||
if ($sender) {
|
||||
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
} else {
|
||||
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
}
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
@@ -181,11 +208,13 @@ class MailingController extends Controller
|
||||
public function invio(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$liste = MailingList::where('attiva', true)->orderBy('nome')->get();
|
||||
$liste = MailingList::with('firma')->where('attiva', true)->orderBy('nome')->get();
|
||||
$documenti = Documento::orderBy('nome_file')->get();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
$settings = EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
|
||||
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts'));
|
||||
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts', 'firme'));
|
||||
}
|
||||
|
||||
public function invioElabora(Request $request)
|
||||
@@ -193,18 +222,23 @@ class MailingController extends Controller
|
||||
$this->authorizeWrite('mailing');
|
||||
$data = $request->validate([
|
||||
'liste' => 'required|array',
|
||||
'liste.*' => 'exists:mailing_liste,id',
|
||||
'liste.*' => 'exists:mailing_lists,id',
|
||||
'oggetto' => 'required|string|max:255',
|
||||
'corpo' => 'required',
|
||||
'documenti_selezionati' => 'nullable|array',
|
||||
'documenti_selezionati.*' => 'exists:documenti,id',
|
||||
'allegato' => 'nullable|file|max:10240',
|
||||
'mittente_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
]);
|
||||
|
||||
$listeModels = MailingList::with('firma', 'senderAccount')->whereIn('id', $data['liste'])->get();
|
||||
|
||||
$sender = null;
|
||||
if (!empty($data['mittente_id'])) {
|
||||
$sender = SenderAccount::findOrFail($data['mittente_id']);
|
||||
} elseif ($listeModels->count() === 1) {
|
||||
$sender = $listeModels->first()->senderAccount;
|
||||
}
|
||||
|
||||
$documentiAllegati = [];
|
||||
@@ -252,9 +286,24 @@ class MailingController extends Controller
|
||||
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
|
||||
}
|
||||
|
||||
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
|
||||
$attachmentPaths = $attachmentResult['paths'];
|
||||
$linkDocs = $attachmentResult['links'];
|
||||
|
||||
$listeNomi = MailingList::whereIn('id', $data['liste'])->pluck('nome')->implode(', ');
|
||||
$firmaId = $data['firma_id'] ?? null;
|
||||
if (!$firmaId && $listeModels->count() === 1) {
|
||||
$firmaId = $listeModels->first()->firma_id;
|
||||
}
|
||||
|
||||
$corpoConFirma = $this->appendMailingLinksToBody($data['corpo'], $linkDocs);
|
||||
if ($firmaId) {
|
||||
$firma = Firma::find($firmaId);
|
||||
if ($firma && $firma->contenuto_html) {
|
||||
$corpoConFirma .= "\n\n---\n" . $firma->contenuto_html;
|
||||
}
|
||||
}
|
||||
|
||||
$listeNomi = $listeModels->pluck('nome')->implode(', ');
|
||||
|
||||
$messaggio = MailingMessaggio::create([
|
||||
'mailing_list_id' => null,
|
||||
@@ -265,6 +314,7 @@ class MailingController extends Controller
|
||||
'totale_destinatari' => $emails->count(),
|
||||
'mittente_nome' => $sender?->email_name,
|
||||
'mittente_email' => $sender?->email_address,
|
||||
'firma_id' => $firmaId,
|
||||
]);
|
||||
|
||||
$ok = 0;
|
||||
@@ -274,9 +324,9 @@ class MailingController extends Controller
|
||||
foreach ($emails as $email) {
|
||||
try {
|
||||
if ($sender) {
|
||||
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
} else {
|
||||
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
|
||||
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
|
||||
}
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
@@ -347,18 +397,10 @@ class MailingController extends Controller
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
|
||||
$fromName = $settings->email_name ?? '';
|
||||
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
|
||||
$fromName = 'Glastree';
|
||||
} else {
|
||||
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
|
||||
$fromName = trim($fromName);
|
||||
if (empty($fromName) || strlen($fromName) > 50) {
|
||||
$fromName = 'Glastree';
|
||||
}
|
||||
}
|
||||
|
||||
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
|
||||
$fromAddress = \Symfony\Component\Mime\Address::create(
|
||||
$settings->getEffectiveFromAddress(),
|
||||
$settings->getEffectiveFromName()
|
||||
);
|
||||
|
||||
$email = (new \Symfony\Component\Mime\Email())
|
||||
->from($fromAddress)
|
||||
@@ -366,6 +408,11 @@ class MailingController extends Controller
|
||||
->subject($subject)
|
||||
->text($body);
|
||||
|
||||
$replyTo = $settings->getEffectiveReplyTo();
|
||||
if ($replyTo) {
|
||||
$email->replyTo($replyTo);
|
||||
}
|
||||
|
||||
foreach ($attachmentPaths as $path) {
|
||||
$email->attachFromPath($path);
|
||||
}
|
||||
@@ -376,17 +423,34 @@ class MailingController extends Controller
|
||||
private function resolveMailingAttachmentPaths(array $documentIds): array
|
||||
{
|
||||
$paths = [];
|
||||
$links = [];
|
||||
if (empty($documentIds)) {
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
$docs = Documento::whereIn('id', $documentIds)->get();
|
||||
foreach ($docs as $doc) {
|
||||
if ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
if ($doc->isLink()) {
|
||||
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
|
||||
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
|
||||
$paths[] = Storage::path($doc->file_path);
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
return ['paths' => $paths, 'links' => $links];
|
||||
}
|
||||
|
||||
private function appendMailingLinksToBody(string $body, array $linkDocs): string
|
||||
{
|
||||
if (empty($linkDocs)) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
$linksText = "\n\n---\nDocumenti collegati:\n";
|
||||
foreach ($linkDocs as $link) {
|
||||
$linksText .= "- {$link['nome']}: {$link['url']}\n";
|
||||
}
|
||||
|
||||
return $body . $linksText;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\MailingList;
|
||||
use App\Models\SenderAccount;
|
||||
use App\Models\Tag;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -60,7 +61,10 @@ class MailingListController extends Controller
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
return view('mailing-liste.create', compact('tags'));
|
||||
$settings = \App\Models\EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
return view('mailing-liste.create', compact('tags', 'firme', 'senderAccounts'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
@@ -73,6 +77,8 @@ class MailingListController extends Controller
|
||||
'contatti_json' => 'nullable|string',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
]);
|
||||
|
||||
$data['user_id'] = auth()->id();
|
||||
@@ -83,6 +89,8 @@ class MailingListController extends Controller
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'attiva' => $data['attiva'],
|
||||
'user_id' => $data['user_id'],
|
||||
'firma_id' => $data['firma_id'] ?? null,
|
||||
'sender_account_id' => $data['sender_account_id'] ?? null,
|
||||
]);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
@@ -117,10 +125,13 @@ class MailingListController extends Controller
|
||||
public function edit($mailingList)
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailingList);
|
||||
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags', 'firma', 'senderAccount'])->findOrFail($mailingList);
|
||||
$tags = Tag::orderBy('name')->get();
|
||||
$selectedTags = $mailingList->tags->pluck('id')->toArray();
|
||||
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags'));
|
||||
$settings = \App\Models\EmailSetting::getActive();
|
||||
$firme = $settings?->firme ?? collect();
|
||||
$senderAccounts = SenderAccount::active()->get();
|
||||
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags', 'firme', 'senderAccounts'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $mailingList)
|
||||
@@ -135,6 +146,8 @@ class MailingListController extends Controller
|
||||
'contatti_json' => 'nullable|string',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'firma_id' => 'nullable|integer|exists:firme,id',
|
||||
'sender_account_id' => 'nullable|integer|exists:sender_accounts,id',
|
||||
]);
|
||||
|
||||
$data['attiva'] = $data['attiva'] ?? false;
|
||||
@@ -143,6 +156,8 @@ class MailingListController extends Controller
|
||||
'nome' => $data['nome'],
|
||||
'descrizione' => $data['descrizione'] ?? null,
|
||||
'attiva' => $data['attiva'],
|
||||
'firma_id' => $data['firma_id'] ?? null,
|
||||
'sender_account_id' => $data['sender_account_id'] ?? null,
|
||||
]);
|
||||
|
||||
if ($request->has('tags')) {
|
||||
@@ -186,6 +201,41 @@ class MailingListController extends Controller
|
||||
return redirect()->route('mailing-liste.index')->with('success', 'Lista eliminata.');
|
||||
}
|
||||
|
||||
public function massTag(Request $request)
|
||||
{
|
||||
$this->authorizeWrite('mailing');
|
||||
$idsInput = $request->input('ids', '');
|
||||
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
|
||||
|
||||
if (empty($ids)) {
|
||||
return back()->with('error', 'Nessuna mailing list selezionata.');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'tags' => 'required|array',
|
||||
'tags.*' => 'exists:tags,id',
|
||||
'mode' => 'required|in:assign,remove',
|
||||
]);
|
||||
|
||||
$tagIds = $data['tags'];
|
||||
$mode = $data['mode'];
|
||||
$count = 0;
|
||||
|
||||
MailingList::whereIn('id', $ids)->chunk(100, function ($lists) use ($tagIds, $mode, &$count) {
|
||||
foreach ($lists as $list) {
|
||||
if ($mode === 'assign') {
|
||||
$list->tags()->syncWithoutDetaching($tagIds);
|
||||
} else {
|
||||
$list->tags()->detach($tagIds);
|
||||
}
|
||||
$count++;
|
||||
}
|
||||
});
|
||||
|
||||
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
|
||||
return back()->with('success', "Tag $actionLabel per $count mailing list.");
|
||||
}
|
||||
|
||||
public function massElimina(Request $request)
|
||||
{
|
||||
$this->authorizeDelete('mailing');
|
||||
|
||||
@@ -515,7 +515,7 @@ class ReportController extends Controller
|
||||
$rows[] = [
|
||||
'livello' => 0,
|
||||
'nome' => $gruppo->nome,
|
||||
'diocesi' => $gruppo->diocesi?->nome ?? '-',
|
||||
'diocesi' => $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'membri' => $gruppo->individui()->count(),
|
||||
'padre' => '-',
|
||||
];
|
||||
@@ -538,7 +538,7 @@ class ReportController extends Controller
|
||||
$rows[] = [
|
||||
'livello' => $level,
|
||||
'nome' => str_repeat(' ', $level) . $child->nome,
|
||||
'diocesi' => $child->diocesi?->nome ?? '-',
|
||||
'diocesi' => $child->diocesi->count() > 0 ? $child->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'membri' => $child->individui()->count(),
|
||||
'padre' => $parent?->nome ?? '-',
|
||||
];
|
||||
@@ -554,7 +554,7 @@ class ReportController extends Controller
|
||||
$parent = $g->parent;
|
||||
return [
|
||||
'nome' => $g->full_path,
|
||||
'diocesi' => $g->diocesi?->nome ?? '-',
|
||||
'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'membri' => $g->individui_count,
|
||||
'padre' => $parent?->nome ?? '-',
|
||||
'responsabili' => $g->getResponsabili()->pluck('cognome')->implode(', ') ?: '-',
|
||||
@@ -895,7 +895,7 @@ class ReportController extends Controller
|
||||
return [
|
||||
'nome' => $g->nome,
|
||||
'descrizione' => $g->descrizione ?? '-',
|
||||
'diocesi' => $g->diocesi?->nome ?? '-',
|
||||
'diocesi' => $g->diocesi->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
|
||||
'livello' => $g->parent_id ? (count($g->getAncestors()) + 1) : 0,
|
||||
'membri' => $g->individui_count,
|
||||
'padre' => $parent?->nome ?? '-',
|
||||
|
||||
@@ -53,11 +53,13 @@ class CalendarioConnessione extends Model
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function encryptAndSetConfig(array $config): void
|
||||
public function encryptAndSetConfig(array $config, array $existingConfig = []): void
|
||||
{
|
||||
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
|
||||
if (isset($config[$field]) && $config[$field] !== '' && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
if (!empty($config[$field]) && !str_starts_with($config[$field], 'eyJpdiI')) {
|
||||
$config[$field] = Crypt::encryptString($config[$field]);
|
||||
} elseif (empty($config[$field]) && !empty($existingConfig[$field])) {
|
||||
$config[$field] = $existingConfig[$field];
|
||||
}
|
||||
}
|
||||
$this->config = $config;
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Diocesi extends Model
|
||||
{
|
||||
protected $table = 'diocesi';
|
||||
protected $fillable = ['nome', 'regione'];
|
||||
|
||||
public function gruppi(): HasMany
|
||||
public function gruppi(): BelongsToMany
|
||||
{
|
||||
return $this->hasMany(Gruppo::class);
|
||||
return $this->belongsToMany(Gruppo::class, 'diocesi_gruppo');
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTagsLight;
|
||||
@@ -16,9 +18,14 @@ class Documento extends Model
|
||||
'tenant_id', 'user_id', 'cartella_id', 'repository_id', 'storage_disk',
|
||||
'nome_file', 'file_path', 'tipo', 'tipologia',
|
||||
'visibilita', 'visibilita_target_id', 'visibilita_target_type',
|
||||
'mime_type', 'dimensione', 'note'
|
||||
'mime_type', 'dimensione', 'note', 'url'
|
||||
];
|
||||
|
||||
public function isLink(): bool
|
||||
{
|
||||
return $this->tipo === 'link';
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
|
||||
+152
-4
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
@@ -13,14 +15,17 @@ class EmailSetting extends Model
|
||||
protected $fillable = [
|
||||
'imap_host', 'imap_port', 'imap_encryption', 'imap_username', 'imap_password',
|
||||
'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_username', 'smtp_password',
|
||||
'email_address', 'email_name', 'reply_to', 'sync_interval_minutes',
|
||||
'last_sync_at', 'is_active', 'signature', 'signature_enabled'
|
||||
'email_address', 'email_name', 'from_email', 'from_name', 'reply_to',
|
||||
'sync_interval_minutes',
|
||||
'last_sync_at', 'is_active', 'signature', 'signature_enabled',
|
||||
'auth_method', 'google_oauth_connection_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'signature_enabled' => 'boolean',
|
||||
'last_sync_at' => 'datetime',
|
||||
'google_oauth_connection_id' => 'integer',
|
||||
];
|
||||
|
||||
public function folders(): HasMany
|
||||
@@ -28,8 +33,25 @@ class EmailSetting extends Model
|
||||
return $this->hasMany(EmailFolder::class)->orderBy('sort_order');
|
||||
}
|
||||
|
||||
public function googleOAuthConnection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GoogleOAuthConnection::class);
|
||||
}
|
||||
|
||||
public function getImapConfig(): array
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$token = $this->resolveOAuthToken();
|
||||
return [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port,
|
||||
'encryption' => $this->imap_encryption,
|
||||
'username' => $this->imap_username,
|
||||
'authentication' => 'oauth',
|
||||
'password' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port,
|
||||
@@ -41,6 +63,10 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->imap_password)) {
|
||||
return '';
|
||||
}
|
||||
@@ -53,6 +79,10 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedSmtpPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->smtp_password)) {
|
||||
return $this->getDecryptedPassword();
|
||||
}
|
||||
@@ -63,6 +93,22 @@ class EmailSetting extends Model
|
||||
}
|
||||
}
|
||||
|
||||
protected function resolveOAuthToken(): string
|
||||
{
|
||||
if (!$this->relationLoaded('googleOAuthConnection')) {
|
||||
$this->load('googleOAuthConnection');
|
||||
}
|
||||
$connection = $this->googleOAuthConnection;
|
||||
if (!$connection) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return app(GoogleOAuthService::class)->getAccessToken($connection);
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getSmtpConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -87,19 +133,87 @@ class EmailSetting extends Model
|
||||
default => 'none',
|
||||
};
|
||||
|
||||
return new \DirectoryTree\ImapEngine\Mailbox([
|
||||
$config = [
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port ?? 993,
|
||||
'encryption' => $encryption,
|
||||
'validate_cert' => false,
|
||||
'username' => $this->imap_username,
|
||||
'password' => $this->getDecryptedPassword(),
|
||||
]);
|
||||
];
|
||||
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$config['authentication'] = 'oauth';
|
||||
}
|
||||
|
||||
return new \DirectoryTree\ImapEngine\Mailbox($config);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->buildOAuthSmtpMailer();
|
||||
}
|
||||
|
||||
$dsn = $this->buildSmtpDsn();
|
||||
if (!$dsn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildOAuthSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
$token = $this->resolveOAuthToken();
|
||||
if (empty($token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$host = $this->smtp_host ?? 'smtp.gmail.com';
|
||||
$port = $this->smtp_port ?? 587;
|
||||
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
|
||||
$transport->setUsername($username);
|
||||
$transport->setPassword($token);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
public function buildSmtpDsn(): ?string
|
||||
{
|
||||
$password = $this->getDecryptedSmtpPassword();
|
||||
if (empty($password)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$encryption = $this->smtp_encryption ?? 'tls';
|
||||
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
|
||||
|
||||
return sprintf(
|
||||
'%s://%s:%s@%s:%d?encryption=%s',
|
||||
$scheme,
|
||||
rawurlencode($username),
|
||||
rawurlencode($password),
|
||||
$this->smtp_host ?? 'smtp.gmail.com',
|
||||
$this->smtp_port ?? 587,
|
||||
$encryption
|
||||
);
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
try {
|
||||
@@ -134,4 +248,38 @@ class EmailSetting extends Model
|
||||
{
|
||||
return $this->signature;
|
||||
}
|
||||
|
||||
public function getEffectiveFromAddress(): string
|
||||
{
|
||||
return $this->from_email ?: $this->email_address;
|
||||
}
|
||||
|
||||
public function getEffectiveFromName(): string
|
||||
{
|
||||
$name = $this->from_name ?: $this->email_name;
|
||||
if (filter_var($name, FILTER_VALIDATE_EMAIL)) {
|
||||
return 'Glastree';
|
||||
}
|
||||
$name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
|
||||
$name = trim($name);
|
||||
return empty($name) ? 'Glastree' : $name;
|
||||
}
|
||||
|
||||
public function getEffectiveReplyTo(): ?string
|
||||
{
|
||||
if ($this->from_email) {
|
||||
return $this->from_email;
|
||||
}
|
||||
return $this->reply_to ?: null;
|
||||
}
|
||||
|
||||
public function firme(): HasMany
|
||||
{
|
||||
return $this->hasMany(Firma::class);
|
||||
}
|
||||
|
||||
public function firmaPredefinita(): ?Firma
|
||||
{
|
||||
return $this->firme()->where('is_default', true)->first() ?? $this->firme()->first();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTagsLight;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Firma extends Model
|
||||
{
|
||||
protected $table = 'firme';
|
||||
|
||||
protected $fillable = [
|
||||
'email_setting_id',
|
||||
'nome',
|
||||
'contenuto_html',
|
||||
'is_default',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_default' => 'boolean',
|
||||
];
|
||||
|
||||
public function emailSetting(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EmailSetting::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class GoogleOAuthConnection extends Model
|
||||
{
|
||||
protected $table = 'google_oauth_connections';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'service',
|
||||
'email',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at && $this->expires_at->isPast();
|
||||
}
|
||||
|
||||
public function refresh(): void
|
||||
{
|
||||
if (!$this->refresh_token) {
|
||||
throw new \RuntimeException('Nessun refresh token disponibile per ' . $this->email);
|
||||
}
|
||||
app(GoogleOAuthService::class)->refreshToken($this);
|
||||
}
|
||||
|
||||
public function getValidAccessToken(): string
|
||||
{
|
||||
if ($this->isExpired() && $this->refresh_token) {
|
||||
$this->refresh();
|
||||
}
|
||||
return $this->access_token;
|
||||
}
|
||||
}
|
||||
+23
-3
@@ -15,15 +15,18 @@ class Gruppo extends Model
|
||||
protected $table = 'gruppi';
|
||||
protected $fillable = [
|
||||
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
|
||||
'nome', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
|
||||
'nome', 'anno_fondazione', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
|
||||
'città_incontro', 'sigla_provincia_incontro', 'mappa_posizione', 'metadata'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'responsabile_ids' => 'array',
|
||||
'anno_fondazione' => 'integer',
|
||||
];
|
||||
|
||||
protected $appends = ['email_primaria', 'telefono_primario'];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
@@ -39,9 +42,14 @@ class Gruppo extends Model
|
||||
return $this->hasMany(Gruppo::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function diocesi(): BelongsTo
|
||||
public function diocesi(): BelongsToMany
|
||||
{
|
||||
return $this->belongsTo(Diocesi::class);
|
||||
return $this->belongsToMany(Diocesi::class, 'diocesi_gruppo');
|
||||
}
|
||||
|
||||
public function gruppoContatti(): HasMany
|
||||
{
|
||||
return $this->hasMany(GruppoContatto::class, 'gruppo_id');
|
||||
}
|
||||
|
||||
public function individui(): BelongsToMany
|
||||
@@ -66,6 +74,18 @@ class Gruppo extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEmailPrimariaAttribute(): ?string
|
||||
{
|
||||
return $this->gruppoContatti()->where('tipo', 'email')->where('is_primary', true)->first()?->valore
|
||||
?? $this->gruppoContatti()->where('tipo', 'email')->first()?->valore;
|
||||
}
|
||||
|
||||
public function getTelefonoPrimarioAttribute(): ?string
|
||||
{
|
||||
return $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->where('is_primary', true)->first()?->valore
|
||||
?? $this->gruppoContatti()->whereIn('tipo', ['telefono', 'cellulare'])->first()?->valore;
|
||||
}
|
||||
|
||||
public function getResponsabili()
|
||||
{
|
||||
if (empty($this->responsabile_ids)) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class GruppoContatto extends Model
|
||||
{
|
||||
protected $table = 'gruppo_contatti';
|
||||
protected $fillable = ['gruppo_id', 'tipo', 'valore', 'etichetta', 'is_primary'];
|
||||
|
||||
protected $casts = [
|
||||
'is_primary' => 'boolean',
|
||||
];
|
||||
|
||||
public function gruppo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Gruppo::class);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class MailingList extends Model
|
||||
use HasTagsLight;
|
||||
|
||||
protected $table = 'mailing_lists';
|
||||
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
|
||||
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva', 'firma_id', 'sender_account_id'];
|
||||
|
||||
protected $casts = ['attiva' => 'boolean'];
|
||||
|
||||
@@ -36,6 +36,16 @@ class MailingList extends Model
|
||||
return $this->hasMany(MailingMessaggio::class);
|
||||
}
|
||||
|
||||
public function firma(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Firma::class);
|
||||
}
|
||||
|
||||
public function senderAccount(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SenderAccount::class);
|
||||
}
|
||||
|
||||
public function getIndividui()
|
||||
{
|
||||
return Individuo::whereHas('mailingContacts', function ($q) {
|
||||
|
||||
@@ -11,7 +11,7 @@ class MailingMessaggio extends Model
|
||||
protected $fillable = [
|
||||
'tenant_id', 'mailing_list_id', 'user_id', 'oggetto', 'corpo',
|
||||
'canale', 'stato', 'inviato_al', 'totale_destinatari', 'invii_ok', 'invii_falliti',
|
||||
'log_falliti', 'mittente_nome', 'mittente_email',
|
||||
'log_falliti', 'mittente_nome', 'mittente_email', 'firma_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class SenderAccount extends Model
|
||||
@@ -24,11 +26,14 @@ class SenderAccount extends Model
|
||||
'verify_email',
|
||||
'note',
|
||||
'is_active',
|
||||
'auth_method',
|
||||
'google_oauth_connection_id',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'smtp_port' => 'integer',
|
||||
'google_oauth_connection_id' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
@@ -36,8 +41,33 @@ class SenderAccount extends Model
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function googleOAuthConnection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(GoogleOAuthConnection::class);
|
||||
}
|
||||
|
||||
protected function resolveOAuthToken(): string
|
||||
{
|
||||
if (!$this->relationLoaded('googleOAuthConnection')) {
|
||||
$this->load('googleOAuthConnection');
|
||||
}
|
||||
$connection = $this->googleOAuthConnection;
|
||||
if (!$connection) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return app(GoogleOAuthService::class)->getAccessToken($connection);
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
if ($this->auth_method === 'oauth') {
|
||||
return $this->resolveOAuthToken();
|
||||
}
|
||||
|
||||
if (empty($this->smtp_password)) {
|
||||
return '';
|
||||
}
|
||||
@@ -121,11 +151,33 @@ class SenderAccount extends Model
|
||||
$email->attachFromPath($path);
|
||||
}
|
||||
|
||||
if ($this->auth_method === 'oauth') {
|
||||
$mailer = $this->buildOAuthMailer();
|
||||
} else {
|
||||
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
|
||||
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
$mailer->send($email);
|
||||
}
|
||||
|
||||
protected function buildOAuthMailer(): \Symfony\Component\Mailer\Mailer
|
||||
{
|
||||
$token = $this->resolveOAuthToken();
|
||||
$username = $this->smtp_username ?? $this->email_address;
|
||||
$host = $this->smtp_host ?? 'smtp.gmail.com';
|
||||
$port = $this->smtp_port ?? 587;
|
||||
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
|
||||
$transport->setUsername($username);
|
||||
$transport->setPassword($token);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return new \Symfony\Component\Mailer\Mailer($transport);
|
||||
}
|
||||
|
||||
public function sendReport(string $subject, string $body): bool
|
||||
{
|
||||
if (empty($this->verify_email)) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
+5
-4
@@ -197,12 +197,13 @@ class User extends Authenticatable implements CanResetPasswordContract
|
||||
|
||||
$resetUrl = url('/password/reset/' . $token);
|
||||
|
||||
$fromName = $settings->email_name ?? config('app.name');
|
||||
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
|
||||
$fromName = trim($fromName) ?: config('app.name');
|
||||
$fromAddress = new Address(
|
||||
$settings->getEffectiveFromAddress(),
|
||||
$settings->getEffectiveFromName()
|
||||
);
|
||||
|
||||
$email = (new Email())
|
||||
->from(new Address($settings->email_address, $fromName))
|
||||
->from($fromAddress)
|
||||
->to($this->email)
|
||||
->subject('Reset della password - ' . config('app.name'))
|
||||
->html(view('auth.emails.reset-password', [
|
||||
|
||||
@@ -60,7 +60,7 @@ class BackupService
|
||||
return $backups;
|
||||
}
|
||||
|
||||
public function run(): array
|
||||
public function run(array $options = []): array
|
||||
{
|
||||
$result = ['success' => true, 'message' => '', 'filename' => ''];
|
||||
|
||||
@@ -74,12 +74,28 @@ class BackupService
|
||||
$filename = $appName . '_' . $timestamp . '.zip';
|
||||
$filepath = $this->backupDir . '/' . $filename;
|
||||
|
||||
$includeFiles = $options['include_files'] ?? (bool) AppSetting::getSetting('backup_include_files', true);
|
||||
$includeEnv = $options['include_env'] ?? (bool) AppSetting::getSetting('backup_include_env', true);
|
||||
|
||||
$steps = [];
|
||||
$includedComponents = [];
|
||||
|
||||
$steps[] = $this->backupDatabase();
|
||||
$steps[] = $this->backupEnv();
|
||||
$steps[] = $this->backupFiles();
|
||||
$this->writeManifest($timestamp);
|
||||
$includedComponents[] = 'database';
|
||||
|
||||
$stepEnv = $this->backupEnv($includeEnv);
|
||||
$steps[] = $stepEnv;
|
||||
if ($includeEnv) {
|
||||
$includedComponents[] = 'env';
|
||||
}
|
||||
|
||||
$stepFiles = $this->backupFiles($includeFiles);
|
||||
$steps[] = $stepFiles;
|
||||
if ($includeFiles) {
|
||||
$includedComponents[] = 'files';
|
||||
}
|
||||
|
||||
$this->writeManifest($timestamp, $includedComponents);
|
||||
|
||||
$zipResult = $this->createZip($filepath);
|
||||
if (!$zipResult) {
|
||||
@@ -90,7 +106,7 @@ class BackupService
|
||||
|
||||
$size = file_exists($filepath) ? filesize($filepath) : 0;
|
||||
|
||||
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ")");
|
||||
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ')');
|
||||
|
||||
$this->cleanupOldBackups();
|
||||
|
||||
@@ -100,6 +116,7 @@ class BackupService
|
||||
'filename' => $filename,
|
||||
'size' => $size,
|
||||
'size_formatted' => $this->formatBytes($size),
|
||||
'steps' => $steps,
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
$this->cleanTemp();
|
||||
@@ -109,6 +126,7 @@ class BackupService
|
||||
'success' => false,
|
||||
'message' => 'Backup fallito: ' . $e->getMessage(),
|
||||
'filename' => '',
|
||||
'steps' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -181,9 +199,9 @@ class BackupService
|
||||
return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')';
|
||||
}
|
||||
|
||||
private function backupEnv(): string
|
||||
private function backupEnv(bool $includeEnv = true): string
|
||||
{
|
||||
if (!AppSetting::getSetting('backup_include_env', true)) {
|
||||
if (!$includeEnv) {
|
||||
return '.env (escluso)';
|
||||
}
|
||||
|
||||
@@ -200,9 +218,9 @@ class BackupService
|
||||
return '.env copiato';
|
||||
}
|
||||
|
||||
private function backupFiles(): string
|
||||
private function backupFiles(bool $includeFiles = true): string
|
||||
{
|
||||
if (!AppSetting::getSetting('backup_include_files', true)) {
|
||||
if (!$includeFiles) {
|
||||
return 'files (esclusi)';
|
||||
}
|
||||
|
||||
@@ -220,7 +238,7 @@ class BackupService
|
||||
return 'files copiati';
|
||||
}
|
||||
|
||||
private function writeManifest(string $timestamp): void
|
||||
private function writeManifest(string $timestamp, array $includedComponents = ['database']): void
|
||||
{
|
||||
$manifest = [
|
||||
'created_at' => $timestamp,
|
||||
@@ -230,6 +248,7 @@ class BackupService
|
||||
'db_name' => config('database.connections.mysql.database'),
|
||||
'php_version' => PHP_VERSION,
|
||||
'laravel_version' => app()->version(),
|
||||
'included_components' => $includedComponents,
|
||||
];
|
||||
|
||||
File::put($this->tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\GoogleOAuthConnection;
|
||||
use App\Enums\GoogleService;
|
||||
use Google_Client;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleOAuthService
|
||||
{
|
||||
private const TOKEN_CACHE_TTL = 300;
|
||||
private const MAX_RETRIES = 2;
|
||||
|
||||
public function buildClient(): Google_Client
|
||||
{
|
||||
$client = new Google_Client();
|
||||
$client->setClientId(config('services.google.client_id'));
|
||||
$client->setClientSecret(config('services.google.client_secret'));
|
||||
$client->setRedirectUri(route('google-oauth.callback'));
|
||||
$client->setAccessType('offline');
|
||||
$client->setPrompt('consent');
|
||||
$client->setIncludeGrantedScopes(true);
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
public function getAuthUrl(string $service): string
|
||||
{
|
||||
$serviceEnum = GoogleService::from($service);
|
||||
$client = $this->buildClient();
|
||||
$client->setScopes($serviceEnum->scopes());
|
||||
$client->setState($service);
|
||||
|
||||
return $client->createAuthUrl();
|
||||
}
|
||||
|
||||
public function handleCallback(string $authorizationCode, GoogleService $service): GoogleOAuthConnection
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$client->setScopes($service->scopes());
|
||||
$client->fetchAccessTokenWithAuthCode($authorizationCode);
|
||||
|
||||
$tokenData = $client->getAccessToken();
|
||||
|
||||
if (isset($tokenData['error'])) {
|
||||
throw new \RuntimeException('Google OAuth error: ' . ($tokenData['error_description'] ?? $tokenData['error']));
|
||||
}
|
||||
|
||||
$token = $tokenData['access_token'];
|
||||
$refreshToken = $tokenData['refresh_token'] ?? null;
|
||||
$expiresAt = now()->addSeconds($tokenData['expires_in']);
|
||||
|
||||
$email = $this->getEmailFromToken($token);
|
||||
|
||||
$connection = GoogleOAuthConnection::updateOrCreate([
|
||||
'user_id' => auth()->id(),
|
||||
'service' => $service->value,
|
||||
'email' => $email,
|
||||
], [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_at' => $expiresAt,
|
||||
]);
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
public function getAccessToken(GoogleOAuthConnection $connection): string
|
||||
{
|
||||
$cacheKey = "google_oauth_token_{$connection->id}";
|
||||
|
||||
return Cache::remember($cacheKey, self::TOKEN_CACHE_TTL, function () use ($connection) {
|
||||
if ($connection->isExpired()) {
|
||||
$this->refreshToken($connection);
|
||||
}
|
||||
return $connection->access_token;
|
||||
});
|
||||
}
|
||||
|
||||
public function refreshToken(GoogleOAuthConnection $connection): void
|
||||
{
|
||||
if (!$connection->refresh_token) {
|
||||
throw new \RuntimeException('Impossibile rinnovare: nessun refresh token');
|
||||
}
|
||||
|
||||
$client = $this->buildClient();
|
||||
$client->fetchAccessTokenWithRefreshToken($connection->refresh_token);
|
||||
|
||||
$tokenData = $client->getAccessToken();
|
||||
|
||||
if (isset($tokenData['error'])) {
|
||||
$connection->delete();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
|
||||
throw new \RuntimeException(
|
||||
'Refresh token scaduto o revocato per ' . $connection->email
|
||||
);
|
||||
}
|
||||
|
||||
$connection->access_token = $tokenData['access_token'];
|
||||
$connection->expires_at = now()->addSeconds($tokenData['expires_in']);
|
||||
|
||||
if (isset($tokenData['refresh_token'])) {
|
||||
$connection->refresh_token = $tokenData['refresh_token'];
|
||||
}
|
||||
|
||||
$connection->save();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
}
|
||||
|
||||
public function revoke(GoogleOAuthConnection $connection): void
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$client->revokeToken($connection->access_token);
|
||||
|
||||
$connection->delete();
|
||||
Cache::forget("google_oauth_token_{$connection->id}");
|
||||
}
|
||||
|
||||
public function createXoAuth2SmtpTransport(
|
||||
string $email,
|
||||
string $accessToken
|
||||
): \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport {
|
||||
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
|
||||
'smtp.gmail.com',
|
||||
587,
|
||||
true,
|
||||
);
|
||||
|
||||
$transport->setUsername($email);
|
||||
$transport->setPassword($accessToken);
|
||||
$transport->setAuthenticators([
|
||||
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
|
||||
]);
|
||||
|
||||
return $transport;
|
||||
}
|
||||
|
||||
private function getEmailFromToken(string $accessToken): string
|
||||
{
|
||||
$client = $this->buildClient();
|
||||
$payload = $client->verifyIdToken($accessToken);
|
||||
|
||||
if ($payload && isset($payload['email'])) {
|
||||
return $payload['email'];
|
||||
}
|
||||
|
||||
$http = new \GuzzleHttp\Client();
|
||||
$response = $http->get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', [
|
||||
'headers' => ['Authorization' => "Bearer $accessToken"],
|
||||
]);
|
||||
$data = json_decode((string) $response->getBody(), true);
|
||||
|
||||
return $data['email'];
|
||||
}
|
||||
|
||||
public function getConnectionStatus(): array
|
||||
{
|
||||
$connections = GoogleOAuthConnection::where('user_id', auth()->id())->get();
|
||||
$status = [];
|
||||
|
||||
foreach (GoogleService::cases() as $service) {
|
||||
$connection = $connections->firstWhere('service', $service->value);
|
||||
$status[$service->value] = [
|
||||
'connected' => $connection !== null,
|
||||
'connection' => $connection,
|
||||
'email' => $connection?->email,
|
||||
'expired' => $connection?->isExpired() ?? false,
|
||||
];
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -19,12 +19,9 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
'permission' => CheckPermission::class,
|
||||
]);
|
||||
$middleware->trustProxies(at: '*');
|
||||
$middleware->web(append: [
|
||||
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
]);
|
||||
// Web middleware defaults are already applied by the framework.
|
||||
// Do NOT append EncryptCookies, StartSession, etc. here — they
|
||||
// would run twice and corrupt session cookies, causing 419 errors.
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
+227
-21
@@ -20,6 +20,223 @@ echo "==> Pulizia cache..."
|
||||
rm -rf bootstrap/cache/*.php storage/framework/cache/data/* storage/framework/sessions/* storage/framework/views/* storage/logs/* 2>/dev/null || true
|
||||
echo " OK"
|
||||
|
||||
echo "==> Generazione post-deploy.sh..."
|
||||
cat > post-deploy.sh << 'POSTDEPLOY'
|
||||
#!/bin/bash
|
||||
# post-deploy.sh — Generated by build-dist.sh
|
||||
# Usage: bash post-deploy.sh {check|upgrade|install} [--yes]
|
||||
set -u
|
||||
APP_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ARTISAN="php ${APP_DIR}/artisan"
|
||||
DIAGNOSE="php ${APP_DIR}/diagnose.php"
|
||||
RED='\033[31m'; GREEN='\033[32m'; YELLOW='\033[33m'
|
||||
CYAN='\033[36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||
|
||||
MODE="${1:-check}"
|
||||
BATCH=false; [ "${2:-}" = "--yes" ] && BATCH=true
|
||||
|
||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||
echo -e "${CYAN}${BOLD} Glastree Post-Deploy — ${MODE}${NC}"
|
||||
echo -e "${CYAN}${BOLD}============================================${NC}"
|
||||
echo ""
|
||||
|
||||
IS_FRESH=false; [ ! -f "${APP_DIR}/.env" ] && IS_FRESH=true
|
||||
|
||||
log() { echo -e " ${BOLD}$1${NC}"; }
|
||||
ok() { echo -e " ${GREEN}✅ $1${NC}"; }
|
||||
warn() { echo -e " ${YELLOW}⚠ $1${NC}"; }
|
||||
fail() { echo -e " ${RED}❌ $1${NC}"; }
|
||||
|
||||
check_artisan() {
|
||||
if [ ! -f "${APP_DIR}/artisan" ]; then
|
||||
fail "artisan non trovato. Esegui da root dell'applicazione."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local step="$1" desc="$2"; shift 2
|
||||
log "${step} ${desc}"
|
||||
if "$@" 2>&1 | sed 's/^/ /'; then
|
||||
ok "${desc}"
|
||||
return 0
|
||||
else
|
||||
local rc=$?
|
||||
fail "${desc} — exit code ${rc}"
|
||||
return ${rc}
|
||||
fi
|
||||
}
|
||||
|
||||
run_diagnose() {
|
||||
if ${DIAGNOSE} 2>&1 | tail -5; then
|
||||
ok "Diagnostica OK"
|
||||
else
|
||||
warn "Diagnostica ha rilevato problemi (non bloccante)"
|
||||
fi
|
||||
}
|
||||
|
||||
run_check() {
|
||||
echo -e "${YELLOW}═══════════ CONTROLLO ═══════════${NC}" ; echo ""
|
||||
|
||||
echo -e "${BOLD}[0] Diagnostica sistema${NC}"
|
||||
run_diagnose ; echo ""
|
||||
|
||||
echo -e "${BOLD}[1] Ambiente${NC}"
|
||||
if $IS_FRESH; then warn ".env non trovato — fresh install necessario"
|
||||
else ok ".env presente"
|
||||
fi ; echo ""
|
||||
|
||||
echo -e "${BOLD}[2] Migration pendenti${NC}"
|
||||
if [ -f "${APP_DIR}/artisan" ]; then
|
||||
$ARTISAN migrate:status 2>&1 | grep -E '(Pending|Ran)' | head -5 || true
|
||||
PENDING=$(php -r "echo trim(shell_exec('${ARTISAN} migrate:status 2>&1 | grep -c Pending || true'));" 2>/dev/null || echo "0")
|
||||
if [ "${PENDING:-0}" -gt 0 ] 2>/dev/null; then
|
||||
warn "${PENDING} migration da eseguire"
|
||||
else
|
||||
ok "Nessuna migration pendente"
|
||||
fi
|
||||
else
|
||||
warn "artisan non disponibile — skip migration check"
|
||||
fi ; echo ""
|
||||
|
||||
echo -e "${BOLD}[3] Storage & Permessi${NC}"
|
||||
for d in storage/framework/cache/data storage/framework/sessions \
|
||||
storage/framework/views storage/logs bootstrap/cache \
|
||||
storage/app/public storage/app/backups; do
|
||||
p="${APP_DIR}/${d}"
|
||||
if [ -d "$p" ]; then
|
||||
[ -w "$p" ] && ok "${d}" || warn "${d} — non scrivibile"
|
||||
else
|
||||
warn "${d} — mancante"
|
||||
fi
|
||||
done ; echo ""
|
||||
|
||||
echo -e "${BOLD}[4] Symlink storage${NC}"
|
||||
if [ -L "${APP_DIR}/public/storage" ]; then
|
||||
ok "public/storage → $(readlink "${APP_DIR}/public/storage")"
|
||||
else
|
||||
warn "public/storage non presente"
|
||||
fi ; echo ""
|
||||
|
||||
echo -e "${BOLD}[5] Cache${NC}"
|
||||
for d in bootstrap/cache storage/framework/cache/data \
|
||||
storage/framework/sessions storage/framework/views; do
|
||||
c=$(find "${APP_DIR}/${d}" -type f 2>/dev/null | wc -l)
|
||||
[ "$c" -gt 0 ] && warn "${d}: ${c} file" || ok "${d}: vuoto"
|
||||
done
|
||||
echo "" ; echo -e "${YELLOW}═══════════ FINE CONTROLLO ═══════════${NC}"
|
||||
}
|
||||
|
||||
run_upgrade() {
|
||||
echo -e "${YELLOW}═══════════ UPGRADE ═══════════${NC}" ; check_artisan
|
||||
|
||||
run_step "[1/9]" "Directory storage" mkdir -p \
|
||||
"${APP_DIR}/storage/framework/cache/data" \
|
||||
"${APP_DIR}/storage/framework/sessions" \
|
||||
"${APP_DIR}/storage/framework/views" \
|
||||
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
|
||||
"${APP_DIR}/storage/app/public" \
|
||||
"${APP_DIR}/storage/app/public/logos" \
|
||||
"${APP_DIR}/storage/app/public/documenti/eventi" \
|
||||
"${APP_DIR}/storage/app/backups" \
|
||||
"${APP_DIR}/storage/app/documenti" \
|
||||
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
|
||||
|
||||
run_step "[2/9]" ".gitignore storage" bash -c \
|
||||
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
|
||||
|
||||
run_step "[3/9]" "Permessi" chmod -R 775 \
|
||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||
|
||||
run_step "[4/9]" "Symlink storage" bash -c \
|
||||
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
|
||||
|
||||
run_step "[5/10]" "Migrazioni DB" $ARTISAN migrate --force || true
|
||||
|
||||
run_step "[6/10]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
|
||||
|
||||
run_step "[7/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
|
||||
|
||||
run_step "[8/10]" "Key generate" $ARTISAN key:generate --force || true
|
||||
|
||||
run_step "[9/10]" "Pulizia cache" bash -c \
|
||||
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
|
||||
|
||||
run_step "[10/10]" "Verifica finale" $DIAGNOSE || true
|
||||
|
||||
echo "" ; echo -e "${GREEN}${BOLD}✅ Upgrade completato.${NC}"
|
||||
}
|
||||
|
||||
run_install() {
|
||||
echo -e "${YELLOW}═══════════ INSTALL (fresh) ═══════════${NC}" ; check_artisan
|
||||
|
||||
run_step "[1/10]" "Directory storage" mkdir -p \
|
||||
"${APP_DIR}/storage/framework/cache/data" \
|
||||
"${APP_DIR}/storage/framework/sessions" \
|
||||
"${APP_DIR}/storage/framework/views" \
|
||||
"${APP_DIR}/storage/logs" "${APP_DIR}/bootstrap/cache" \
|
||||
"${APP_DIR}/storage/app/public" \
|
||||
"${APP_DIR}/storage/app/public/logos" \
|
||||
"${APP_DIR}/storage/app/public/documenti/eventi" \
|
||||
"${APP_DIR}/storage/app/backups" \
|
||||
"${APP_DIR}/storage/app/documenti" \
|
||||
"${APP_DIR}/storage/app/private/avatars/gruppi" || true
|
||||
|
||||
run_step "[2/10]" ".gitignore storage" bash -c \
|
||||
"echo '*'>'${APP_DIR}/storage/app/public/.gitignore'; echo '!.gitignore'>>'${APP_DIR}/storage/app/public/.gitignore'" || true
|
||||
|
||||
run_step "[3/10]" "Permessi" chmod -R 775 \
|
||||
"${APP_DIR}/storage" "${APP_DIR}/bootstrap/cache" || true
|
||||
|
||||
run_step "[4/10]" "File .env" cp "${APP_DIR}/.env.example" "${APP_DIR}/.env" || true
|
||||
warn "MODIFICA .env con i tuoi dati DB/URL prima di proseguire"
|
||||
if ! $BATCH; then read -p " Fatto? (Invio per continuare) " _; fi
|
||||
|
||||
run_step "[5/10]" "Symlink storage" bash -c \
|
||||
"rm -f '${APP_DIR}/public/storage'; ln -sf '../storage/app/public' '${APP_DIR}/public/storage'" || true
|
||||
|
||||
run_step "[6/10]" "Dump autoload" composer dump-autoload --no-dev --optimize || true
|
||||
|
||||
run_step "[7/10]" "Key generate" $ARTISAN key:generate --force || true
|
||||
|
||||
run_step "[8/11]" "Migrazioni DB" $ARTISAN migrate --force || true
|
||||
|
||||
run_step "[9/11]" "Seed diocesi" $ARTISAN db:seed --class=DiocesiSeeder --force || true
|
||||
|
||||
run_step "[10/11]" "Pulizia cache" bash -c \
|
||||
"$ARTISAN config:clear && $ARTISAN route:clear && $ARTISAN view:clear" || true
|
||||
|
||||
run_step "[11/11]" "Verifica finale" $DIAGNOSE || true
|
||||
|
||||
echo "" ; echo -e "${GREEN}${BOLD}✅ Installazione completata.${NC}"
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
check) run_check ;;
|
||||
upgrade)
|
||||
run_check ; echo ""
|
||||
if $BATCH; then run_upgrade
|
||||
else
|
||||
read -p "Procedere con l'upgrade? (s/N) " c
|
||||
[ "$c" = "s" ] || [ "$c" = "S" ] && run_upgrade || echo "Annullato."
|
||||
fi
|
||||
;;
|
||||
install) run_install ;;
|
||||
*)
|
||||
echo "Uso: bash post-deploy.sh {check|upgrade|install} [--yes]"
|
||||
echo " check — diagnostica (nessuna modifica)"
|
||||
echo " upgrade — aggiorna installazione esistente"
|
||||
echo " install — fresh install"
|
||||
echo " --yes — batch (nessuna conferma)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
POSTDEPLOY
|
||||
|
||||
chmod +x post-deploy.sh
|
||||
|
||||
echo " OK"
|
||||
|
||||
echo "==> Creazione archivio: ${ARCHIVE}"
|
||||
|
||||
tar czf "${ARCHIVE}" \
|
||||
@@ -60,6 +277,8 @@ tar czf "${ARCHIVE}" \
|
||||
--warning=no-file-changed \
|
||||
.
|
||||
|
||||
rm -f post-deploy.sh
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Archivio creato: ${ARCHIVE}"
|
||||
@@ -70,27 +289,14 @@ echo "Per estrarre sul server di destinazione:"
|
||||
echo " tar xzf ${ARCHIVE}"
|
||||
echo ""
|
||||
echo "Poi esegui:"
|
||||
echo " # 1. Crea directory necessarie (se non esistono già)"
|
||||
echo ' mkdir -p storage/framework/cache/data storage/framework/sessions storage/framework/views storage/logs bootstrap/cache storage/app/public storage/app/public/logos storage/app/public/documenti/eventi storage/app/backups storage/app/documenti storage/app/private/avatars/gruppi'
|
||||
echo " # Upgrade di un'installazione esistente:"
|
||||
echo " bash post-deploy.sh upgrade"
|
||||
echo ""
|
||||
echo " # 2. Crea file .gitignore in storage/app/public (serve a Laravel)"
|
||||
echo ' echo "*" > storage/app/public/.gitignore'
|
||||
echo ' echo "!.gitignore" >> storage/app/public/.gitignore'
|
||||
echo " # Fresh install:"
|
||||
echo " bash post-deploy.sh install"
|
||||
echo ""
|
||||
echo " # 3. Permessi (web server = www-data)"
|
||||
echo ' chmod -R 775 storage bootstrap/cache'
|
||||
echo ' chown -R www-data:www-data storage bootstrap/cache'
|
||||
echo " # Solo controllo (nessuna modifica):"
|
||||
echo " bash post-deploy.sh check"
|
||||
echo ""
|
||||
echo " # 4. Configura ambiente"
|
||||
echo " cp .env.example .env # modifica DB, APP_URL, etc."
|
||||
echo " php artisan key:generate"
|
||||
echo ""
|
||||
echo " # 5. Pulisci cache e ricrea symlink storage (relativo per portabilità)"
|
||||
echo ' rm -f public/storage'
|
||||
echo ' ln -sf ../storage/app/public public/storage'
|
||||
echo " php artisan config:clear"
|
||||
echo " php artisan route:clear"
|
||||
echo " php artisan view:clear"
|
||||
echo ""
|
||||
echo " # 6. Avvia installazione MySQL"
|
||||
echo " php install.php"
|
||||
echo " # Batch mode (nessuna conferma):"
|
||||
echo " bash post-deploy.sh upgrade --yes"
|
||||
|
||||
@@ -35,4 +35,10 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
'redirect' => env('GOOGLE_REDIRECT_URI', config('app.url') . '/auth/google/callback'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -8,6 +8,10 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('tenants')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('tenants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
|
||||
@@ -8,13 +8,16 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('diocesi')) {
|
||||
Schema::create('diocesi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
$table->string('regione')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('comuni')) {
|
||||
Schema::create('comuni', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
@@ -29,6 +32,7 @@ return new class extends Migration
|
||||
$table->index('sigla_provincia');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('individui')) {
|
||||
Schema::create('individui', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
@@ -30,6 +31,7 @@ return new class extends Migration
|
||||
$table->index('cognome');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('contatti')) {
|
||||
Schema::create('contatti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
|
||||
@@ -19,6 +20,7 @@ return new class extends Migration
|
||||
$table->index('individuo_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('gruppi')) {
|
||||
Schema::create('gruppi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
@@ -27,6 +28,7 @@ return new class extends Migration
|
||||
$table->index('parent_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('gruppo_individuo')) {
|
||||
Schema::create('gruppo_individuo', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||
@@ -18,6 +19,7 @@ return new class extends Migration
|
||||
$table->unique(['gruppo_id', 'individuo_id']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('eventi')) {
|
||||
Schema::create('eventi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
@@ -23,14 +24,18 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
$table->index('tenant_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('eventi_gruppi')) {
|
||||
Schema::create('eventi_gruppi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('eventi_responsabili')) {
|
||||
Schema::create('eventi_responsabili', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
||||
@@ -38,6 +43,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,14 +8,15 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('documenti')) {
|
||||
Schema::create('documenti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
$table->string('nome_file');
|
||||
$table->string('file_path');
|
||||
$table->string('file_path')->nullable();
|
||||
$table->string('tipo')->nullable();
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento');
|
||||
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo');
|
||||
$table->string('tipologia', 50)->default('documento');
|
||||
$table->string('visibilita', 50)->default('gruppo');
|
||||
$table->unsignedBigInteger('visibilita_target_id')->nullable();
|
||||
$table->string('visibilita_target_type')->nullable();
|
||||
$table->string('mime_type')->nullable();
|
||||
@@ -25,6 +26,7 @@ return new class extends Migration
|
||||
$table->index('tenant_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('mailing_lists')) {
|
||||
Schema::create('mailing_lists', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
@@ -17,7 +18,9 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
$table->index('tenant_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('mailing_contacts')) {
|
||||
Schema::create('mailing_contacts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('mailing_list_id')->constrained('mailing_lists')->onDelete('cascade');
|
||||
@@ -28,7 +31,9 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
$table->unique(['mailing_list_id', 'individuo_id']);
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('mailing_messaggi')) {
|
||||
Schema::create('mailing_messaggi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
@@ -46,6 +51,7 @@ return new class extends Migration
|
||||
$table->index('tenant_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('notifiche')) {
|
||||
Schema::create('notifiche', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
@@ -21,6 +22,7 @@ return new class extends Migration
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('users')) {
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
|
||||
@@ -21,13 +22,17 @@ return new class extends Migration
|
||||
$table->index('tenant_id');
|
||||
$table->index('email');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('password_reset_tokens')) {
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('sessions')) {
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
@@ -37,6 +42,7 @@ return new class extends Migration
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,18 +8,23 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('cache')) {
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('cache_locks')) {
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('jobs')) {
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
@@ -29,7 +34,9 @@ return new class extends Migration
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('job_batches')) {
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
@@ -42,7 +49,9 @@ return new class extends Migration
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('failed_jobs')) {
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
@@ -53,6 +62,7 @@ return new class extends Migration
|
||||
$table->timestamp('failed_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('eventi_documenti')) {
|
||||
Schema::create('eventi_documenti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
|
||||
@@ -15,6 +16,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('viste_report')) {
|
||||
Schema::create('viste_report', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
@@ -22,6 +23,7 @@ return new class extends Migration
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('tipologie_documenti')) {
|
||||
Schema::create('tipologie_documenti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
@@ -29,6 +30,7 @@ return new class extends Migration
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ return new class extends Migration
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('activity_logs')) {
|
||||
Schema::create('activity_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||
@@ -28,7 +29,9 @@ return new class extends Migration
|
||||
$table->index(['created_at']);
|
||||
$table->index(['action']);
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable('role_presets')) {
|
||||
Schema::create('role_presets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 50)->unique();
|
||||
@@ -37,6 +40,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ return new class extends Migration
|
||||
/**
|
||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||
*/
|
||||
if (!Schema::hasTable($tableNames['permissions'])) {
|
||||
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
|
||||
$table->id(); // permission id
|
||||
$table->string('name');
|
||||
@@ -31,10 +32,12 @@ return new class extends Migration
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
|
||||
*/
|
||||
if (!Schema::hasTable($tableNames['roles'])) {
|
||||
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
|
||||
$table->id(); // role id
|
||||
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||
@@ -50,7 +53,9 @@ return new class extends Migration
|
||||
$table->unique(['name', 'guard_name']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable($tableNames['model_has_permissions'])) {
|
||||
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
|
||||
@@ -73,7 +78,9 @@ return new class extends Migration
|
||||
'model_has_permissions_permission_model_type_primary');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable($tableNames['model_has_roles'])) {
|
||||
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
|
||||
@@ -96,7 +103,9 @@ return new class extends Migration
|
||||
'model_has_roles_role_model_type_primary');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasTable($tableNames['role_has_permissions'])) {
|
||||
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||
$table->unsignedBigInteger($pivotPermission);
|
||||
$table->unsignedBigInteger($pivotRole);
|
||||
@@ -113,6 +122,7 @@ return new class extends Migration
|
||||
|
||||
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||
});
|
||||
}
|
||||
|
||||
app('cache')
|
||||
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('email_settings')) {
|
||||
Schema::create('email_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('imap_host')->nullable();
|
||||
@@ -29,6 +30,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('email_folders')) {
|
||||
Schema::create('email_folders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
@@ -20,6 +21,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('email_messages')) {
|
||||
Schema::create('email_messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade');
|
||||
@@ -38,6 +39,7 @@ return new class extends Migration
|
||||
$table->index('received_at');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro', 'email_attachment'])->default('documento')->change();
|
||||
});
|
||||
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
|
||||
// Application-level validation handles allowed values.
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento')->change();
|
||||
});
|
||||
// No-op: column is string, not enum.
|
||||
}
|
||||
};
|
||||
@@ -7,14 +7,15 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('tipologie_documenti')->insert([
|
||||
'nome' => 'email_attachment',
|
||||
DB::table('tipologie_documenti')->updateOrInsert(
|
||||
['nome' => 'email_attachment'],
|
||||
[
|
||||
'descrizione' => 'Allegato email',
|
||||
'ordine' => 100,
|
||||
'attiva' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('email_attachments')) {
|
||||
Schema::create('email_attachments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade');
|
||||
@@ -20,6 +21,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('ruoli')) {
|
||||
Schema::create('ruoli', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
@@ -37,6 +38,7 @@ return new class extends Migration
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
|
||||
@@ -8,10 +8,13 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
$table->dropIndex(['ruolo_id']);
|
||||
$table->dropForeign(['ruolo_id']);
|
||||
$table->dropColumn('ruolo_id');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) {
|
||||
Schema::table('gruppo_individuo', function (Blueprint $table) {
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro', 'programma', 'locandina') DEFAULT 'documento'");
|
||||
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
|
||||
// Application-level validation handles allowed values.
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro') DEFAULT 'documento'");
|
||||
// No-op: column is string, not enum.
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('report_custom')) {
|
||||
Schema::create('report_custom', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
@@ -18,6 +19,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('tipologie_eventi')) {
|
||||
Schema::create('tipologie_eventi', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome', 50)->unique();
|
||||
@@ -30,6 +31,7 @@ return new class extends Migration
|
||||
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('sender_accounts')) {
|
||||
Schema::create('sender_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email_address')->unique();
|
||||
@@ -26,6 +27,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('documenti_cartelle')) {
|
||||
Schema::create('documenti_cartelle', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
|
||||
@@ -15,6 +16,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('storage_repositories')) {
|
||||
Schema::create('storage_repositories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
@@ -18,6 +19,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('calendario_connessioni')) {
|
||||
Schema::create('calendario_connessioni', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nome');
|
||||
@@ -26,6 +27,7 @@ return new class extends Migration
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('google_oauth_connections')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('google_oauth_connections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('service'); // email, drive, calendar
|
||||
$table->string('email');
|
||||
$table->text('access_token');
|
||||
$table->text('refresh_token')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'service', 'email']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('google_oauth_connections');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('email_settings', 'auth_method')) {
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->string('auth_method', 20)->default('password');
|
||||
$table->foreignId('google_oauth_connection_id')
|
||||
->nullable()
|
||||
->constrained('google_oauth_connections')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('sender_accounts', 'auth_method')) {
|
||||
Schema::table('sender_accounts', function (Blueprint $table) {
|
||||
$table->string('auth_method', 20)->default('password');
|
||||
$table->foreignId('google_oauth_connection_id')
|
||||
->nullable()
|
||||
->constrained('google_oauth_connections')
|
||||
->nullOnDelete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('email_settings', 'google_oauth_connection_id')) {
|
||||
$table->dropForeign(['google_oauth_connection_id']);
|
||||
}
|
||||
if (Schema::hasColumn('email_settings', 'auth_method')) {
|
||||
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('sender_accounts', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('sender_accounts', 'google_oauth_connection_id')) {
|
||||
$table->dropForeign(['google_oauth_connection_id']);
|
||||
}
|
||||
if (Schema::hasColumn('sender_accounts', 'auth_method')) {
|
||||
$table->dropColumn(['auth_method', 'google_oauth_connection_id']);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('email_settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('email_settings', 'from_email')) {
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->string('from_email')->nullable()->after('email_name');
|
||||
});
|
||||
}
|
||||
|
||||
if (!Schema::hasColumn('email_settings', 'from_name')) {
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->string('from_name')->nullable()->after('from_email');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->dropColumn(['from_email', 'from_name']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('gruppo_contatti')) {
|
||||
Schema::create('gruppo_contatti', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||
$table->string('tipo');
|
||||
$table->string('valore');
|
||||
$table->string('etichetta')->nullable();
|
||||
$table->boolean('is_primary')->default(false);
|
||||
$table->timestamps();
|
||||
$table->index('gruppo_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('gruppo_contatti');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('diocesi_gruppo')) {
|
||||
Schema::create('diocesi_gruppo', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
|
||||
$table->foreignId('diocesi_id')->constrained('diocesi')->onDelete('cascade');
|
||||
$table->unique(['gruppo_id', 'diocesi_id']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('diocesi_gruppo');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('firme', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('email_setting_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('nome', 255);
|
||||
$table->longText('contenuto_html')->nullable();
|
||||
$table->boolean('is_default')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('firme');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->foreignId('firma_id')->nullable()->constrained('firme')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->dropForeign(['firma_id']);
|
||||
$table->dropColumn('firma_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('mailing_messaggi', function (Blueprint $table) {
|
||||
$table->foreignId('firma_id')->nullable()->constrained('firme')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mailing_messaggi', function (Blueprint $table) {
|
||||
$table->dropForeign(['firma_id']);
|
||||
$table->dropColumn('firma_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasTable('sessions')) {
|
||||
Schema::create('sessions', function (Blueprint $table): void {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->foreignId('sender_account_id')->nullable()->constrained('sender_accounts')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('mailing_lists', function (Blueprint $table) {
|
||||
$table->dropForeign(['sender_account_id']);
|
||||
$table->dropColumn('sender_account_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (!Schema::hasColumn('gruppi', 'anno_fondazione')) {
|
||||
Schema::table('gruppi', function (Blueprint $table) {
|
||||
$table->year('anno_fondazione')->nullable()->after('nome');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('gruppi', 'anno_fondazione')) {
|
||||
Schema::table('gruppi', function (Blueprint $table) {
|
||||
$table->dropColumn('anno_fondazione');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->text('url')->nullable()->after('note');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('documenti', function (Blueprint $table) {
|
||||
$table->dropColumn('url');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasColumn('documenti', 'file_path') && DB::getDriverName() === 'mysql') {
|
||||
DB::statement('ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NULL');
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasColumn('documenti', 'file_path') && DB::getDriverName() === 'mysql') {
|
||||
DB::statement('ALTER TABLE documenti MODIFY COLUMN file_path VARCHAR(255) NOT NULL');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -68,7 +68,7 @@ class ComuniSeeder extends Seeder
|
||||
];
|
||||
|
||||
foreach ($comuni as $c) {
|
||||
Comune::create($c);
|
||||
Comune::firstOrCreate(['codice_istat' => $c['codice_istat']], $c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,10 @@ class DatabaseSeeder extends Seeder
|
||||
DockerBaseDataSeeder::class,
|
||||
]);
|
||||
|
||||
$admin = User::create([
|
||||
$admin = User::firstOrCreate(
|
||||
['email' => 'admin@glastree.local'],
|
||||
[
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@glastree.local',
|
||||
'password' => bcrypt('password'),
|
||||
'is_admin' => true,
|
||||
'status' => 'active',
|
||||
@@ -34,9 +35,12 @@ class DatabaseSeeder extends Seeder
|
||||
'mailing' => 2,
|
||||
'viste' => 2,
|
||||
],
|
||||
]);
|
||||
]
|
||||
);
|
||||
|
||||
if (!$admin->role_preset_id) {
|
||||
$admin->role_preset_id = 1;
|
||||
$admin->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
+243
-138
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
@@ -9,147 +11,250 @@ class DiocesiSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$fixes = [
|
||||
50 => 'Abbazia territoriale della Santissima Trinità di Cava de Tirreni',
|
||||
51 => 'Diocesi di Cefalù',
|
||||
58 => 'Diocesi di Città di Castello',
|
||||
79 => 'Diocesi di Forlì-Bertinoro',
|
||||
121 => 'Diocesi di Mondovì',
|
||||
128 => 'Diocesi di Nardò-Gallipoli',
|
||||
148 => 'Arcidiocesi di Perugia-Città della Pieve',
|
||||
185 => "Arcidiocesi di Siena-Colle di Val d'Elsa-Montalcino",
|
||||
];
|
||||
foreach ($fixes as $id => $correctNome) {
|
||||
Diocesi::where('id', $id)->update(['nome' => $correctNome]);
|
||||
}
|
||||
|
||||
$diocesi = [
|
||||
['nome' => 'Acqui Terme', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Afragola', 'regione' => 'Campania'],
|
||||
['nome' => 'Agrigento', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Alba', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Alessandria', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Ancona', 'regione' => 'Marche'],
|
||||
['nome' => 'Aosta', 'regione' => 'Valle d\'Aosta'],
|
||||
['nome' => 'Aquila', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'Arezzo', 'regione' => 'Toscana'],
|
||||
['nome' => 'Ascoli Piceno', 'regione' => 'Marche'],
|
||||
['nome' => 'Assisi', 'regione' => 'Umbria'],
|
||||
['nome' => 'Asti', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Avellino', 'regione' => 'Campania'],
|
||||
['nome' => 'Bari', 'regione' => 'Puglia'],
|
||||
['nome' => 'Barletta', 'regione' => 'Puglia'],
|
||||
['nome' => 'Belluno', 'regione' => 'Veneto'],
|
||||
['nome' => 'Benevento', 'regione' => 'Campania'],
|
||||
['nome' => 'Bergamo', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Biella', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Bologna', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Bolzano', 'regione' => 'Trentino-Alto Adige'],
|
||||
['nome' => 'Brescia', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Brindisi', 'regione' => 'Puglia'],
|
||||
['nome' => 'Cagliari', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Caltanissetta', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Campobasso', 'regione' => 'Molise'],
|
||||
['nome' => 'Caserta', 'regione' => 'Campania'],
|
||||
['nome' => 'Cassano allo Ionio', 'regione' => 'Calabria'],
|
||||
['nome' => 'Castelvetrano', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Catania', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Catanzaro', 'regione' => 'Calabria'],
|
||||
['nome' => 'Cefalù', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Cerro al Lambro', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Cesena', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Chieti', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'Civitavecchia', 'regione' => 'Lazio'],
|
||||
['nome' => 'Cosenza', 'regione' => 'Calabria'],
|
||||
['nome' => 'Cremona', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Crotone', 'regione' => 'Calabria'],
|
||||
['nome' => 'Cuneo', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Donegal', 'regione' => 'Irlanda'],
|
||||
['nome' => 'Enna', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Fano', 'regione' => 'Marche'],
|
||||
['nome' => 'Fermo', 'regione' => 'Marche'],
|
||||
['nome' => 'Ferrara', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Firenze', 'regione' => 'Toscana'],
|
||||
['nome' => 'Foggia', 'regione' => 'Puglia'],
|
||||
['nome' => 'Foligno', 'regione' => 'Umbria'],
|
||||
['nome' => 'Forlì', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Frosinone', 'regione' => 'Lazio'],
|
||||
['nome' => 'Genova', 'regione' => 'Liguria'],
|
||||
['nome' => 'Gorizia', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Grosseto', 'regione' => 'Toscana'],
|
||||
['nome' => 'Gubbio', 'regione' => 'Umbria'],
|
||||
['nome' => 'Imola', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Isernia', 'regione' => 'Molise'],
|
||||
['nome' => 'L\'Aquila', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'La Spezia', 'regione' => 'Liguria'],
|
||||
['nome' => 'Lamezia Terme', 'regione' => 'Calabria'],
|
||||
['nome' => 'Latina', 'regione' => 'Lazio'],
|
||||
['nome' => 'Lecce', 'regione' => 'Puglia'],
|
||||
['nome' => 'Lecco', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Livorno', 'regione' => 'Toscana'],
|
||||
['nome' => 'Lodi', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Lucca', 'regione' => 'Toscana'],
|
||||
['nome' => 'Lunedì', 'regione' => 'Marche'],
|
||||
['nome' => 'Macerata', 'regione' => 'Marche'],
|
||||
['nome' => 'Mantova', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Massa', 'regione' => 'Toscana'],
|
||||
['nome' => 'Matera', 'regione' => 'Basilicata'],
|
||||
['nome' => 'Messina', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Milano', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Modena', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Monaco', 'regione' => 'Germania'],
|
||||
['nome' => 'Mongolia', 'regione' => 'Asia'],
|
||||
['nome' => 'Monreale', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Montefeltro', 'regione' => 'Marche'],
|
||||
['nome' => 'Montepulciano', 'regione' => 'Toscana'],
|
||||
['nome' => 'Monza', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Napoli', 'regione' => 'Campania'],
|
||||
['nome' => 'Novara', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Nuoro', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Oristano', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Otranto', 'regione' => 'Puglia'],
|
||||
['nome' => 'Padova', 'regione' => 'Veneto'],
|
||||
['nome' => 'Palermo', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Parma', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Pavia', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Perugia', 'regione' => 'Umbria'],
|
||||
['nome' => 'Pesaro', 'regione' => 'Marche'],
|
||||
['nome' => 'Piacenza', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Pisa', 'regione' => 'Toscana'],
|
||||
['nome' => 'Pistoia', 'regione' => 'Toscana'],
|
||||
['nome' => 'Pordenone', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Potenza', 'regione' => 'Basilicata'],
|
||||
['nome' => 'Prato', 'regione' => 'Toscana'],
|
||||
['nome' => 'Ragusa', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Ravenna', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Reggio Calabria', 'regione' => 'Calabria'],
|
||||
['nome' => 'Reggio Emilia', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Rieti', 'regione' => 'Lazio'],
|
||||
['nome' => 'Rimini', 'regione' => 'Emilia-Romagna'],
|
||||
['nome' => 'Roma', 'regione' => 'Lazio'],
|
||||
['nome' => 'Rosario', 'regione' => 'Argentina'],
|
||||
['nome' => 'Rovigo', 'regione' => 'Veneto'],
|
||||
['nome' => 'Salerno', 'regione' => 'Campania'],
|
||||
['nome' => 'San Severo', 'regione' => 'Puglia'],
|
||||
['nome' => 'Sassari', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Savona', 'regione' => 'Liguria'],
|
||||
['nome' => 'Siena', 'regione' => 'Toscana'],
|
||||
['nome' => 'Siracusa', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Sondrio', 'regione' => 'Lombardia'],
|
||||
['nome' => 'Spezia', 'regione' => 'Liguria'],
|
||||
['nome' => 'Spoleto', 'regione' => 'Umbria'],
|
||||
['nome' => 'Syracuse', 'regione' => 'USA'],
|
||||
['nome' => 'Taranto', 'regione' => 'Puglia'],
|
||||
['nome' => 'Tempio Ampurias', 'regione' => 'Sardegna'],
|
||||
['nome' => 'Teramo', 'regione' => 'Abruzzo'],
|
||||
['nome' => 'Terni', 'regione' => 'Umbria'],
|
||||
['nome' => 'Tirana', 'regione' => 'Albania'],
|
||||
['nome' => 'Torino', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Tortona', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Trani', 'regione' => 'Puglia'],
|
||||
['nome' => 'Trapani', 'regione' => 'Sicilia'],
|
||||
['nome' => 'Trento', 'regione' => 'Trentino-Alto Adige'],
|
||||
['nome' => 'Treviso', 'regione' => 'Veneto'],
|
||||
['nome' => 'Trieste', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Udine', 'regione' => 'Friuli-Venezia Giulia'],
|
||||
['nome' => 'Urbino', 'regione' => 'Marche'],
|
||||
['nome' => 'Vaticano', 'regione' => 'Lazio'],
|
||||
['nome' => 'Vercelli', 'regione' => 'Piemonte'],
|
||||
['nome' => 'Verona', 'regione' => 'Veneto'],
|
||||
['nome' => 'Vibo Valentia', 'regione' => 'Calabria'],
|
||||
['nome' => 'Vicenza', 'regione' => 'Veneto'],
|
||||
['nome' => 'Viterbo', 'regione' => 'Lazio'],
|
||||
['nome' => 'Vittorio Veneto', 'regione' => 'Veneto'],
|
||||
'Arcidiocesi di Acerenza',
|
||||
'Diocesi di Acerra',
|
||||
'Diocesi di Acireale',
|
||||
'Diocesi di Acqui',
|
||||
'Diocesi di Adria-Rovigo',
|
||||
'Arcidiocesi di Agrigento',
|
||||
'Diocesi di Alba',
|
||||
'Sede suburbicaria di Albano',
|
||||
'Diocesi di Albenga-Imperia',
|
||||
'Diocesi di Ales-Terralba',
|
||||
'Diocesi di Alessandria',
|
||||
'Diocesi di Alghero-Bosa',
|
||||
'Diocesi di Alife-Caiazzo',
|
||||
'Diocesi di Altamura-Gravina-Acquaviva delle Fonti',
|
||||
'Arcidiocesi di Amalfi-Cava dei Tirreni',
|
||||
'Diocesi di Anagni-Alatri',
|
||||
'Arcidiocesi di Ancona-Osimo',
|
||||
'Diocesi di Andria',
|
||||
'Diocesi di Aosta',
|
||||
'Diocesi di Arezzo-Cortona-Sansepolcro',
|
||||
'Diocesi di Ariano Irpino-Lacedonia',
|
||||
'Diocesi di Ascoli Piceno',
|
||||
'Diocesi di Assisi-Nocera Umbra-Gualdo Tadino',
|
||||
'Diocesi di Asti',
|
||||
'Diocesi di Avellino',
|
||||
'Diocesi di Aversa',
|
||||
'Diocesi di Avezzano',
|
||||
'Arcidiocesi di Bari-Bitonto',
|
||||
'Diocesi di Belluno-Feltre',
|
||||
'Arcidiocesi di Benevento',
|
||||
'Diocesi di Bergamo',
|
||||
'Diocesi di Biella',
|
||||
'Arcidiocesi di Bologna',
|
||||
'Diocesi di Bolzano-Bressanone',
|
||||
'Diocesi di Brescia',
|
||||
'Arcidiocesi di Brindisi-Ostuni',
|
||||
'Arcidiocesi di Cagliari',
|
||||
'Diocesi di Caltagirone',
|
||||
'Diocesi di Caltanissetta',
|
||||
'Arcidiocesi di Camerino-San Severino Marche',
|
||||
'Arcidiocesi di Campobasso-Boiano',
|
||||
'Arcidiocesi di Capua',
|
||||
'Diocesi di Carpi',
|
||||
'Diocesi di Casale Monferrato',
|
||||
'Diocesi di Caserta',
|
||||
'Diocesi di Cassano allo Ionio',
|
||||
'Diocesi di Castellaneta',
|
||||
'Arcidiocesi di Catania',
|
||||
'Arcidiocesi di Catanzaro-Squillace',
|
||||
'Abbazia territoriale della Santissima Trinità di Cava de Tirreni',
|
||||
'Diocesi di Cefalù',
|
||||
'Diocesi di Cerignola-Ascoli Satriano',
|
||||
'Diocesi di Cerreto Sannita-Telese-Sant\'Agata de Goti',
|
||||
'Diocesi di Cesena-Sarsina',
|
||||
'Diocesi di Chiavari',
|
||||
'Arcidiocesi di Chieti-Vasto',
|
||||
'Diocesi di Chioggia',
|
||||
'Diocesi di Città di Castello',
|
||||
'Diocesi di Civita Castellana',
|
||||
'Diocesi di Civitavecchia-Tarquinia',
|
||||
'Diocesi di Como',
|
||||
'Diocesi di Concordia-Pordenone',
|
||||
'Diocesi di Conversano-Monopoli',
|
||||
'Arcidiocesi di Cosenza-Bisignano',
|
||||
'Diocesi di Crema',
|
||||
'Diocesi di Cremona',
|
||||
'Arcidiocesi di Crotone-Santa Severina',
|
||||
'Diocesi di Cuneo',
|
||||
'Diocesi di Fabriano-Matelica',
|
||||
'Diocesi di Faenza-Modigliana',
|
||||
'Diocesi di Fano-Fossombrone-Cagli-Pergola',
|
||||
'Arcidiocesi di Fermo',
|
||||
'Arcidiocesi di Ferrara-Comacchio',
|
||||
'Diocesi di Fidenza',
|
||||
'Diocesi di Fiesole',
|
||||
'Arcidiocesi di Firenze',
|
||||
'Arcidiocesi di Foggia-Bovino',
|
||||
'Diocesi di Foligno',
|
||||
'Diocesi di Forlì-Bertinoro',
|
||||
'Diocesi di Fossano',
|
||||
'Sede suburbicaria di Frascati',
|
||||
'Diocesi di Frosinone-Veroli-Ferentino',
|
||||
'Arcidiocesi di Gaeta',
|
||||
'Arcidiocesi di Genova',
|
||||
'Arcidiocesi di Gorizia',
|
||||
'Diocesi di Grosseto',
|
||||
'Diocesi di Gubbio',
|
||||
'Diocesi di Iglesias',
|
||||
'Diocesi di Imola',
|
||||
'Diocesi di Ischia',
|
||||
'Diocesi di Isernia-Venafro',
|
||||
'Diocesi di Ivrea',
|
||||
'Diocesi di Jesi',
|
||||
'Diocesi della Spezia-Sarzana-Brugnato',
|
||||
'Diocesi di Lamezia Terme',
|
||||
'Arcidiocesi di Lanciano-Ortona',
|
||||
'Diocesi di Lanusei',
|
||||
'Arcidiocesi dell\'Aquila',
|
||||
'Diocesi di Latina-Terracina-Sezze-Priverno',
|
||||
'Arcidiocesi di Lecce',
|
||||
'Diocesi di Livorno',
|
||||
'Diocesi di Locri-Gerace',
|
||||
'Diocesi di Lodi',
|
||||
'Prelatura territoriale di Loreto',
|
||||
'Arcidiocesi di Lucca',
|
||||
'Diocesi di Lucera-Troia',
|
||||
'Eparchia di Lungro',
|
||||
'Diocesi di Macerata-Tolentino-Recanati-Cingoli-Treia',
|
||||
'Arcidiocesi di Manfredonia-Vieste-San Giovanni Rotondo',
|
||||
'Diocesi di Mantova',
|
||||
'Diocesi di Massa Carrara-Pontremoli',
|
||||
'Diocesi di Massa Marittima-Piombino',
|
||||
'Arcidiocesi di Matera-Irsina',
|
||||
'Diocesi di Mazara del Vallo',
|
||||
'Diocesi di Melfi-Rapolla-Venosa',
|
||||
'Arcidiocesi di Messina-Lipari-Santa Lucia del Mela',
|
||||
'Arcidiocesi di Milano',
|
||||
'Diocesi di Mileto-Nicotera-Tropea',
|
||||
'Arcidiocesi di Modena-Nonantola',
|
||||
'Diocesi di Molfetta-Ruvo-Giovinazzo-Terlizzi',
|
||||
'Diocesi di Mondovì',
|
||||
'Arcidiocesi di Monreale',
|
||||
'Abbazia territoriale di Montecassino',
|
||||
'Abbazia territoriale di Monte Oliveto Maggiore',
|
||||
'Diocesi di Montepulciano-Chiusi-Pienza',
|
||||
'Abbazia territoriale di Montevergine',
|
||||
'Arcidiocesi di Napoli',
|
||||
'Diocesi di Nardò-Gallipoli',
|
||||
'Diocesi di Nicosia',
|
||||
'Diocesi di Nocera Inferiore-Sarno',
|
||||
'Diocesi di Nola',
|
||||
'Diocesi di Noto',
|
||||
'Diocesi di Novara',
|
||||
'Diocesi di Nuoro',
|
||||
'Diocesi di Oppido Mamertina-Palmi',
|
||||
'Diocesi di Oria',
|
||||
'Arcidiocesi di Oristano',
|
||||
'Diocesi di Orvieto-Todi',
|
||||
'Sede suburbicaria di Ostia',
|
||||
'Arcidiocesi di Otranto',
|
||||
'Diocesi di Ozieri',
|
||||
'Diocesi di Padova',
|
||||
'Arcidiocesi di Palermo',
|
||||
'Sede suburbicaria di Palestrina',
|
||||
'Diocesi di Parma',
|
||||
'Diocesi di Patti',
|
||||
'Diocesi di Pavia',
|
||||
'Arcidiocesi di Perugia-Città della Pieve',
|
||||
'Arcidiocesi di Pesaro',
|
||||
'Arcidiocesi di Pescara-Penne',
|
||||
'Diocesi di Pescia',
|
||||
'Eparchia di Piana degli Albanesi',
|
||||
'Diocesi di Piazza Armerina',
|
||||
'Diocesi di Pinerolo',
|
||||
'Arcidiocesi di Pisa',
|
||||
'Diocesi di Pistoia',
|
||||
'Diocesi di Pitigliano-Sovana-Orbetello',
|
||||
'Prelatura territoriale di Pompei',
|
||||
'Sede suburbicaria di Porto-Santa Rufina',
|
||||
'Arcidiocesi di Potenza-Muro Lucano-Marsico Nuovo',
|
||||
'Diocesi di Pozzuoli',
|
||||
'Diocesi di Prato',
|
||||
'Diocesi di Ragusa',
|
||||
'Arcidiocesi di Ravenna-Cervia',
|
||||
'Arcidiocesi di Reggio Calabria-Bova',
|
||||
'Diocesi di Reggio Emilia-Guastalla',
|
||||
'Diocesi di Rieti',
|
||||
'Diocesi di Rimini',
|
||||
'Diocesi di Roma',
|
||||
'Arcidiocesi di Rossano-Cariati',
|
||||
'Sede suburbicaria di Sabina-Poggio Mirteto',
|
||||
'Arcidiocesi di Salerno-Campagna-Acerno',
|
||||
'Diocesi di Saluzzo',
|
||||
'Diocesi di San Benedetto del Tronto-Ripatransone-Montalto',
|
||||
'Diocesi di San Marco Argentano-Scalea',
|
||||
'Diocesi di San Marino-Montefeltro',
|
||||
'Diocesi di San Miniato',
|
||||
'Diocesi di San Severo',
|
||||
'Abbazia territoriale di Santa Maria di Grottaferrata',
|
||||
'Arcidiocesi di Sant\'Angelo dei Lombardi-Conza-Nusco-Bisaccia',
|
||||
'Arcidiocesi di Sassari',
|
||||
'Diocesi di Savona-Noli',
|
||||
'Diocesi di Senigallia',
|
||||
'Diocesi di Sessa Aurunca',
|
||||
'Arcidiocesi di Siena-Colle di Val d\'Elsa-Montalcino',
|
||||
'Arcidiocesi di Siracusa',
|
||||
'Diocesi di Sora-Cassino-Aquino-Pontecorvo',
|
||||
'Arcidiocesi di Sorrento-Castellammare di Stabia',
|
||||
'Arcidiocesi di Spoleto-Norcia',
|
||||
'Abbazia territoriale di Subiaco',
|
||||
'Diocesi di Sulmona-Valva',
|
||||
'Diocesi di Susa',
|
||||
'Arcidiocesi di Taranto',
|
||||
'Diocesi di Teano-Calvi',
|
||||
'Diocesi di Teggiano-Policastro',
|
||||
'Diocesi di Tempio-Ampurias',
|
||||
'Diocesi di Teramo-Atri',
|
||||
'Diocesi di Termoli-Larino',
|
||||
'Diocesi di Terni-Narni-Amelia',
|
||||
'Diocesi di Tivoli',
|
||||
'Arcidiocesi di Torino',
|
||||
'Diocesi di Tortona',
|
||||
'Arcidiocesi di Trani-Barletta-Bisceglie-Nazareth',
|
||||
'Diocesi di Trapani',
|
||||
'Arcidiocesi di Trento',
|
||||
'Diocesi di Treviso',
|
||||
'Diocesi di Tricarico',
|
||||
'Diocesi di Trieste',
|
||||
'Diocesi di Trivento',
|
||||
'Diocesi di Tursi-Lagonegro',
|
||||
'Arcidiocesi di Udine',
|
||||
'Diocesi di Ugento-Santa Maria di Leuca',
|
||||
'Arcidiocesi di Urbino-Urbania-Sant\'Angelo in Vado',
|
||||
'Diocesi di Vallo della Lucania',
|
||||
'Sede suburbicaria di Velletri-Segni',
|
||||
'Patriarcato di Venezia',
|
||||
'Diocesi di Ventimiglia-Sanremo',
|
||||
'Arcidiocesi di Vercelli',
|
||||
'Diocesi di Verona',
|
||||
'Diocesi di Vicenza',
|
||||
'Diocesi di Vigevano',
|
||||
'Diocesi di Viterbo',
|
||||
'Diocesi di Vittorio Veneto',
|
||||
'Diocesi di Volterra',
|
||||
'Diocesi di Piacenza-Bobbio',
|
||||
];
|
||||
|
||||
foreach ($diocesi as $d) {
|
||||
Diocesi::create($d);
|
||||
foreach ($diocesi as $nome) {
|
||||
Diocesi::firstOrCreate(['nome' => $nome]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1162
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
../laravel-vite-plugin/bin/clean.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../concurrently/dist/bin/concurrently.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../concurrently/dist/bin/concurrently.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../jiti/lib/jiti-cli.mjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../nanoid/bin/nanoid.cjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../rolldown/bin/cli.mjs
|
||||
+1
@@ -0,0 +1 @@
|
||||
../tree-kill/cli.js
|
||||
+1
@@ -0,0 +1 @@
|
||||
../vite/bin/vite.js
|
||||
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user