Compare commits

5 Commits

Author SHA1 Message Date
inext 7302146a77 Merge branch 'dev'
merge dev
2026-06-10 14:04:40 +02:00
inext c959f72c66 Merge branch 'dev'
da dev version 2
2026-06-09 11:46:09 +02:00
inext 708cadfac2 Merge branch 'dev'
-
2026-06-09 11:27:06 +02:00
inext a35b0056e1 finale 2.0.0.0 2026-06-09 11:26:32 +02:00
inext b7842bf2e0 finale 2.0.0 2026-06-09 11:22:56 +02:00
5887 changed files with 1251 additions and 684401 deletions
+5 -15
View File
@@ -2,18 +2,10 @@ APP_NAME=Glastree
APP_ENV=local
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
APP_DEBUG=true
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_URL=
APP_PROTOCOL=https
FORCE_HTTPS=true
TRUSTED_PROXIES=*
APP_LOCALE=it
APP_FALLBACK_LOCALE=it
@@ -38,11 +30,9 @@ DB_DATABASE=glastree
DB_USERNAME=glastree
DB_PASSWORD=glastree
SESSION_DRIVER=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
SESSION_PATH=/
SESSION_DOMAIN=null
+3 -9
View File
@@ -1,12 +1,8 @@
APP_NAME="Glastree"
APP_ENV=production
APP_ENV=local
APP_KEY=
APP_DEBUG=false
APP_DEBUG=true
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
@@ -28,11 +24,9 @@ DB_DATABASE=glastree
DB_USERNAME=glastree
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_SECURE_COOKIE=false
SESSION_SAME_SITE=lax
SESSION_PATH=/
SESSION_DOMAIN=null
-1
View File
@@ -1 +0,0 @@
{"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
View File
@@ -1,27 +0,0 @@
# 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';"
```
+5 -796
View File
@@ -3,59 +3,6 @@
## 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
@@ -344,7 +291,8 @@ if (!empty($contatto['individuo_id'])) { create }
```
## Prossimi Passi
- [DONE] Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
- [DONE] ... (existing items)
- 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`
@@ -352,10 +300,9 @@ 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 `"`
- [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)
- [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
## 2026-06-10 — Per-User Column Views (colonne visibili, larghezze, ordine)
@@ -433,741 +380,3 @@ 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 (`&times;``<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 `&times;` 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
-59
View File
@@ -1,59 +0,0 @@
# 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:"*"))
```
+3 -11
View File
@@ -28,17 +28,16 @@ class BackupRunCommand extends Command
{
$this->info('Avvio backup...');
$options = [];
if ($this->option('no-files')) {
$options['include_files'] = false;
$this->backupService->saveConfig(['backup_include_files' => false]);
$this->warn('Files esclusi dal backup.');
}
if ($this->option('no-env')) {
$options['include_env'] = false;
$this->backupService->saveConfig(['backup_include_env' => false]);
$this->warn('.env escluso dal backup.');
}
$result = $this->backupService->run($options);
$result = $this->backupService->run();
if ($result['success']) {
$this->info(' Backup completato con successo!');
@@ -46,13 +45,6 @@ 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;
}
-48
View File
@@ -1,48 +0,0 @@
<?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,9 +34,8 @@ 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'] ?? '') . ')' . $steps);
->with('success', 'Backup completato: ' . ($result['filename'] ?? '') . ' (' . ($result['size_formatted'] ?? '') . ')');
}
return redirect()->route('admin.backup.index')
@@ -2,20 +2,15 @@
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
{
@@ -24,9 +19,7 @@ class EmailSettingsController extends Controller
$this->authorizeWrite('settings');
$settings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get();
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
$firme = $settings->exists ? $settings->firme : collect();
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus', 'firme'));
return view('admin.email-settings.index', compact('settings', 'senderAccounts'));
}
public function save(Request $request)
@@ -45,15 +38,11 @@ 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'])) {
@@ -72,17 +61,29 @@ class EmailSettingsController extends Controller
$validated
);
// Verifica che i dati siano stati salvati
$verify = EmailSetting::find($saved->id);
$allGood = $verify &&
if ($verify &&
$verify->imap_host === $validated['imap_host'] &&
$verify->email_address === $validated['email_address'];
$verify->email_address === $validated['email_address']) {
$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']);
$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,
]);
}
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.');
@@ -98,6 +99,7 @@ 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.']);
}
@@ -127,27 +129,60 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
}
try {
$mailer = $settings->getSmtpMailer();
if (empty($settings->smtp_host)) {
$settings->smtp_host = 'smtp.gmail.com';
$settings->smtp_port = 587;
$settings->smtp_encryption = 'tls';
}
if ($mailer === null) {
return response()->json(['success' => false, 'message' => 'Impossibile creare il mailer SMTP. Verifica la configurazione.']);
try {
$smtpUsername = $settings->smtp_username;
$smtpPassword = $settings->smtp_password;
if (empty($smtpUsername)) {
$smtpUsername = $settings->email_address;
}
if (empty($smtpPassword)) {
$smtpPassword = $settings->getDecryptedPassword();
} else {
$smtpPassword = Crypt::decryptString($smtpPassword);
}
$smtpUser = $settings->smtp_username ?: $settings->email_address;
$fromName = $settings->getEffectiveFromName();
$replyToEmail = $settings->from_email ?: $smtpUser;
$encryption = $settings->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
$dsn = sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($smtpUsername),
rawurlencode($smtpPassword),
$settings->smtp_host,
$settings->smtp_port ?? 587,
$encryption
);
$email = (new Email())
->from(Address::create($smtpUser, $fromName))
$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)
->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]);
@@ -192,8 +227,6 @@ 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'])) {
@@ -224,8 +257,6 @@ 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'])) {
@@ -295,66 +326,4 @@ 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->config ?? []);
$connessione->encryptAndSetConfig($data['config']);
}
$connessione->save();
+34 -134
View File
@@ -56,7 +56,7 @@ class DocumentoController extends Controller
} elseif ($folderId) {
$query->where('cartella_id', $folderId);
} else {
$query->whereNull('repository_id')->whereNull('cartella_id');
$query->whereNull('repository_id');
}
$documenti = $query->paginate(20);
@@ -201,43 +201,33 @@ class DocumentoController extends Controller
$tipologieValidi = TipologiaDocumento::opzioni();
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
$rules = [
$data = $request->validate([
'nome_file' => 'required|string|max:255',
'tipologia' => 'required|' . $tipologieRule,
'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing',
'visibilita' => 'nullable|string',
'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string',
'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
];
]);
if ($documento->isLink()) {
$rules['url'] = 'nullable|url|max:2048';
}
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
$data = $request->validate($rules);
$contestoTipo = $data['contesto_tipo'] ?? null;
$data['visibilita'] = $contestoTipo ?: 'pubblico';
if ($contestoTipo) {
if ($request->contesto_tipo) {
$typeMap = [
'individuo' => Individuo::class,
'gruppo' => Gruppo::class,
'evento' => Evento::class,
'mailing' => \App\Models\MailingList::class,
];
$data['visibilita_target_type'] = $typeMap[$contestoTipo] ?? null;
$data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? 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')) {
@@ -246,69 +236,22 @@ class DocumentoController extends Controller
$documento->tags()->detach();
}
$redirect = $request->input('folder_id') ? '/documenti?folder_id=' . $request->input('folder_id') : '/documenti';
return redirect($redirect)->with('success', 'Documento aggiornato.');
return redirect('/documenti')->with('success', 'Documento aggiornato.');
}
public function store(Request $request)
{
$this->authorizeWrite('documenti');
$tipologieValidi = TipologiaDocumento::opzioni();
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
$isLink = $request->input('tipo_caricamento') === 'link';
$rules = [
$data = $request->validate([
'nome_file' => 'required|string|max:255',
'tipologia' => 'required|' . $tipologieRule,
'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro',
'file' => 'required|file|max:10240',
'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;
@@ -341,7 +284,7 @@ class DocumentoController extends Controller
$visibilitaTargetType = Gruppo::class;
}
$doc = Documento::create([
Documento::create([
'nome_file' => $data['nome_file'],
'file_path' => $filePath,
'storage_disk' => $disk ?? null,
@@ -357,13 +300,7 @@ 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.');
}
@@ -372,10 +309,6 @@ 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;
@@ -421,12 +354,6 @@ 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',
@@ -477,8 +404,7 @@ class DocumentoController extends Controller
$this->authorizeDelete('documenti');
$documento = Documento::findOrFail($documento);
if (!$documento->isLink()) {
if ($documento->repository_id) {
if ($documento->repository_id) {
$repo = StorageRepository::find($documento->repository_id);
if ($repo) {
$filesystem = $this->repoService->buildFilesystem($repo);
@@ -486,11 +412,10 @@ class DocumentoController extends Controller
$filesystem->delete($documento->file_path);
}
}
} else {
$disk = $this->resolveStorageDisk($documento);
if (Storage::disk($disk)->exists($documento->file_path)) {
Storage::disk($disk)->delete($documento->file_path);
}
} else {
$disk = $this->resolveStorageDisk($documento);
if (Storage::disk($disk)->exists($documento->file_path)) {
Storage::disk($disk)->delete($documento->file_path);
}
}
@@ -569,10 +494,9 @@ 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 back()->with('error', 'Nessun documento selezionato.');
return response()->json(['linked_count' => 0, 'links' => []]);
}
$documenti = Documento::whereIn('id', $ids)->get();
@@ -648,7 +572,6 @@ 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.');
@@ -667,14 +590,8 @@ 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;
@@ -707,23 +624,11 @@ class DocumentoController extends Controller
$zip->close();
if ($addedCount === 0) {
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);
unlink($zipPath);
return back()->with('error', 'Nessun file disponibile per il download.');
}
$message = $addedCount . ' file scaricati.';
if ($skippedLinks > 0) {
$message .= " {$skippedLinks} link esterni saltati.";
}
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true)
->with('success', $message);
return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true);
}
public function massDestroy(Request $request)
@@ -731,7 +636,6 @@ 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.');
@@ -740,21 +644,19 @@ 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) {
$filesystem = $this->repoService->buildFilesystem($repo);
if ($filesystem && $filesystem->fileExists($documento->file_path)) {
$filesystem->delete($documento->file_path);
}
}
} else {
$disk = $this->resolveStorageDisk($documento);
if (Storage::disk($disk)->exists($documento->file_path)) {
Storage::disk($disk)->delete($documento->file_path);
if ($documento->repository_id) {
$repo = StorageRepository::find($documento->repository_id);
if ($repo) {
$filesystem = $this->repoService->buildFilesystem($repo);
if ($filesystem && $filesystem->fileExists($documento->file_path)) {
$filesystem->delete($documento->file_path);
}
}
} else {
$disk = $this->resolveStorageDisk($documento);
if (Storage::disk($disk)->exists($documento->file_path)) {
Storage::disk($disk)->delete($documento->file_path);
}
}
$documento->delete();
}
@@ -829,7 +731,6 @@ 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.');
@@ -861,7 +762,6 @@ 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.');
+33 -76
View File
@@ -11,7 +11,6 @@ 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;
@@ -97,9 +96,6 @@ 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);
@@ -117,7 +113,7 @@ class EmailController extends Controller
}
}
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts', 'firme'));
return view('email.compose', compact('folders', 'mailingLists', 'gruppi', 'individui', 'documenti', 'prefill', 'senderAccounts'));
}
public function send(Request $request)
@@ -132,7 +128,6 @@ class EmailController extends Controller
$rules['to'] = 'required';
}
$rules['firma_id'] = 'nullable|integer|exists:firme,id';
$validated = $request->validate($rules);
$recipients = $this->resolveRecipients($request);
@@ -162,18 +157,12 @@ class EmailController extends Controller
$imapSuccess = false;
$imapError = null;
$attachmentResult = $this->processAttachments($request);
$attachmentPaths = $attachmentResult['paths'];
$linkDocs = $attachmentResult['links'];
$firmaId = $request->firma_id;
$body = $this->appendLinksToBody($validated['body'], $linkDocs);
$attachments = $this->processAttachments($request);
foreach ($recipients as $recipient) {
try {
$bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachmentPaths);
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$this->sendViaImap($settings, $recipient, $validated['subject'], $bodyWithSig, $request->cc, $attachments);
$imapSuccess = true;
} catch (\Exception $e) {
$imapError = $e->getMessage();
@@ -182,7 +171,7 @@ class EmailController extends Controller
}
try {
$bodyWithSig = $this->appendSignature($body, $firmaId, $settings);
$bodyWithSig = $this->appendSignature($validated['body'], $settings->getSignature(), $settings->signature_enabled ?? false);
$this->storeSentMessage($settings, $recipients, $validated['subject'], $bodyWithSig, $request->cc);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Store sent message error', ['error' => $e->getMessage()]);
@@ -199,20 +188,13 @@ class EmailController extends Controller
private function sendWithSender(Request $request, array $validated, array $recipients)
{
$sender = SenderAccount::findOrFail($request->mittente_id);
$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);
$attachmentPaths = $this->resolveAttachmentPaths($request);
$ok = 0;
$errors = [];
foreach ($recipients as $recipient) {
try {
$sender->sendEmail($recipient, $validated['subject'], $body, $attachmentPaths);
$sender->sendEmail($recipient, $validated['subject'], $validated['body'], $attachmentPaths);
$ok++;
} catch (\Exception $e) {
$errors[] = $recipient . ': ' . $e->getMessage();
@@ -255,8 +237,8 @@ class EmailController extends Controller
'body_text' => $request->body,
'to_email' => $request->to,
'is_draft' => true,
'from_email' => $settings->getEffectiveFromAddress(),
'from_name' => $settings->getEffectiveFromName(),
'from_email' => $settings->email_address ?? '',
'from_name' => $settings->email_name ?? '',
]);
return response()->json(['success' => true, 'draft_id' => $message->id]);
@@ -514,20 +496,12 @@ class EmailController extends Controller
return array_filter(array_unique($recipients));
}
private function appendSignature(string $body, int|string|null $firmaId = null, ?EmailSetting $settings = null): string
private function appendSignature(string $body, ?string $signature, bool $enabled): string
{
if ($firmaId) {
$firma = Firma::find($firmaId);
if ($firma && $firma->contenuto_html) {
return $body . "\n\n---\n" . $firma->contenuto_html;
}
if (empty($signature) || !$enabled) {
return $body;
}
if ($settings && $settings->signature && ($settings->signature_enabled ?? false)) {
return $body . "\n\n---\n" . $settings->signature;
}
return $body;
return $body . "\n\n---\n" . $signature;
}
private function sendViaImap($settings, $to, $subject, $body, $cc = null, array $attachmentPaths = [])
@@ -552,23 +526,26 @@ class EmailController extends Controller
try {
$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->getEffectiveFromAddress(),
$settings->getEffectiveFromName()
);
$fromAddress = \Symfony\Component\Mime\Address::create($settings->email_address, $fromName);
$email = (new \Symfony\Component\Mime\Email())
->from($fromAddress)
->to($to)
->subject($subject)
->text($body);
$replyTo = $settings->getEffectiveReplyTo();
if ($replyTo) {
$email->replyTo($replyTo);
}
if ($cc) {
$email->cc($cc);
}
@@ -590,7 +567,6 @@ class EmailController extends Controller
private function processAttachments(Request $request): array
{
$paths = [];
$links = [];
if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) {
@@ -605,21 +581,18 @@ class EmailController extends Controller
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
$docs = Documento::whereIn('id', $ids)->get();
foreach ($docs as $doc) {
if ($doc->isLink()) {
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
if ($doc->file_path && Storage::exists($doc->file_path)) {
$paths[] = Storage::path($doc->file_path);
}
}
}
return ['paths' => $paths, 'links' => $links];
return $paths;
}
private function resolveAttachmentPaths(Request $request): array
{
$paths = [];
$links = [];
if ($request->hasFile('allegati')) {
foreach ($request->file('allegati') as $file) {
@@ -634,42 +607,26 @@ class EmailController extends Controller
$ids = is_array($documentIds) ? $documentIds : explode(',', $documentIds);
$docs = Documento::whereIn('id', $ids)->get();
foreach ($docs as $doc) {
if ($doc->isLink()) {
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
} elseif ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
if ($doc->file_path && \Illuminate\Support\Facades\Storage::exists($doc->file_path)) {
$paths[] = \Illuminate\Support\Facades\Storage::path($doc->file_path);
}
}
}
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;
return $paths;
}
private function storeSentMessage($settings, $recipients, $subject, $body, $cc = null)
{
$sentFolder = EmailFolder::where('type', 'sent')->first();
$bodyWithSignature = $body;
$bodyWithSignature = $this->appendSignature($body, $settings->getSignature(), $settings->signature_enabled ?? false);
$message = EmailMessage::create([
'email_folder_id' => $sentFolder->id,
'subject' => $subject,
'body_text' => $bodyWithSignature,
'from_email' => $settings->getEffectiveFromAddress(),
'from_name' => $settings->getEffectiveFromName(),
'from_email' => $settings->email_address,
'from_name' => $settings->email_name,
'to_email' => implode(', ', $recipients),
'cc' => $cc,
'is_sent' => true,
-37
View File
@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Evento;
@@ -632,39 +630,4 @@ 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,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Evento;
@@ -1,117 +0,0 @@
<?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);
}
}
+12 -70
View File
@@ -11,7 +11,6 @@ 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
@@ -37,7 +36,6 @@ 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'],
@@ -67,7 +65,7 @@ class GruppoController extends Controller
$entityType = 'gruppi';
$columnWidths = $vista && $vista->colonne_larghezze ? $vista->colonne_larghezze : [];
$gruppiQuery = Gruppo::with(['diocesi', 'gruppoContatti', 'parent', 'children', 'individui', 'avatar', 'tags']);
$gruppiQuery = Gruppo::with(['diocesi', 'parent', 'children', 'individui', 'avatar', 'tags']);
if (request()->filled('tag')) {
$tagSlugs = (array) request()->input('tag');
@@ -143,7 +141,7 @@ class GruppoController extends Controller
public function create(Request $request)
{
$this->authorizeWrite('gruppi');
$diocesi = Diocesi::orderBy('id')->get();
$diocesi = Diocesi::orderBy('nome')->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();
@@ -156,11 +154,9 @@ 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_ids' => 'nullable|array',
'diocesi_ids.*' => 'exists:diocesi,id',
'diocesi_id' => 'nullable|exists:diocesi,id',
'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500',
@@ -168,11 +164,6 @@ 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',
]);
@@ -183,21 +174,6 @@ 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);
}
@@ -219,7 +195,7 @@ class GruppoController extends Controller
public function show($id)
{
$this->authorizeRead('gruppi');
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', 'gruppoContatti', 'individui.contatti', 'individui.documenti', 'individui' => function ($q) {
$gruppo = Gruppo::with(['parent', 'children', 'diocesi', '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);
@@ -230,14 +206,13 @@ class GruppoController extends Controller
public function edit($id)
{
$this->authorizeWrite('gruppi');
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'gruppoContatti', 'avatar', 'tags'])->findOrFail($id);
$diocesi = Diocesi::orderBy('id')->get();
$gruppo = Gruppo::with(['individui.contatti', 'individui.documenti', 'avatar', 'tags'])->findOrFail($id);
$diocesi = Diocesi::orderBy('nome')->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();
$selectedDiocesiIds = $gruppo->diocesi()->pluck('diocesi.id')->toArray();
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags', 'selectedDiocesiIds'));
return view('gruppi.edit', compact('gruppo', 'diocesi', 'gruppi', 'membri', 'tags', 'selectedTags'));
}
public function update(Request $request, $id)
@@ -246,11 +221,9 @@ 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_ids' => 'nullable|array',
'diocesi_ids.*' => 'exists:diocesi,id',
'diocesi_id' => 'nullable|exists:diocesi,id',
'responsabile_ids' => 'nullable|array',
'responsabile_ids.*' => 'exists:individui,id',
'indirizzo_incontro' => 'nullable|string|max:500',
@@ -258,11 +231,6 @@ 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',
]);
@@ -275,24 +243,6 @@ 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 {
@@ -435,12 +385,9 @@ 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, 0, ',');
$header = fgetcsv($handle, 1000, ',');
if ($header === false) {
fclose($handle);
@@ -453,9 +400,7 @@ class GruppoController extends Controller
$errors = [];
$rowNumber = 1;
DB::beginTransaction();
while (($row = fgetcsv($handle, 0, ',')) !== false) {
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
$rowNumber++;
if (count($row) < count($header)) {
@@ -481,7 +426,6 @@ 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,
@@ -500,8 +444,6 @@ class GruppoController extends Controller
fclose($handle);
DB::commit();
$message = 'Importati ' . $imported . ' gruppi.';
if ($skipped > 0) {
$message .= ' Saltate ' . $skipped . ' righe (vuote o incomplete).';
@@ -516,8 +458,8 @@ class GruppoController extends Controller
public function downloadTemplate()
{
$this->authorizeRead('gruppi');
$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'];
$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'];
$csv = implode(',', $headers) . "\n" . implode(',', $example);
@@ -10,7 +10,6 @@ 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;
@@ -35,9 +34,8 @@ 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', 'googleStatus'));
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
}
public function saveAppSettings(Request $request)
+12 -30
View File
@@ -1,23 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Models\Contatto;
use App\Models\Documento;
use App\Models\Gruppo;
use App\Models\Comune;
use App\Models\Diocesi;
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;
use App\Models\Gruppo;
use App\Models\Comune;
use App\Models\Diocesi;
use App\Models\Tag;
use App\Models\VistaReport;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpFoundation\StreamedResponse;
class IndividuoController extends Controller
{
@@ -152,7 +148,7 @@ public function create()
{
$this->authorizeWrite('individui');
$comuni = Comune::orderBy('nome')->get();
$diocesi = Diocesi::orderBy('id')->get();
$diocesi = Diocesi::orderBy('nome')->get();
$tags = Tag::orderBy('name')->get();
return view('individui.create', compact('comuni', 'diocesi', 'tags'));
}
@@ -501,27 +497,16 @@ 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, 0, ',');
$header = fgetcsv($handle, 1000, ',');
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;
DB::beginTransaction();
while (($row = fgetcsv($handle, 0, ',')) !== false) {
while (($row = fgetcsv($handle, 1000, ',')) !== false) {
$rowNumber++;
if (count($row) < count($header)) {
@@ -580,15 +565,12 @@ 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).';
+27 -91
View File
@@ -10,8 +10,6 @@ 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;
@@ -32,8 +30,6 @@ 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')) {
@@ -41,7 +37,7 @@ class MailingController extends Controller
$documentiSelezionati = Documento::whereIn('id', $docIds)->get();
}
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts', 'firme'));
return view('mailing.nuovo', compact('individui', 'liste', 'documenti', 'documentiSelezionati', 'senderAccounts'));
}
public function invia(Request $request)
@@ -50,22 +46,16 @@ class MailingController extends Controller
$data = $request->validate([
'oggetto' => 'required|string|max:255',
'corpo' => 'required',
'lista_id' => 'nullable|exists:mailing_lists,id',
'lista_id' => 'nullable|exists:mailing_liste,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 = [];
@@ -120,23 +110,7 @@ class MailingController extends Controller
return back()->with('error', 'Nessun destinatario email valido trovato.')->withInput();
}
$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;
}
}
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
$messaggio = MailingMessaggio::create([
'mailing_list_id' => $data['lista_id'] ?? null,
@@ -147,7 +121,6 @@ class MailingController extends Controller
'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address,
'firma_id' => $firmaId,
]);
$ok = 0;
@@ -157,9 +130,9 @@ class MailingController extends Controller
foreach ($emails as $email) {
try {
if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
} else {
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
}
$ok++;
} catch (\Throwable $e) {
@@ -208,13 +181,11 @@ class MailingController extends Controller
public function invio(Request $request)
{
$this->authorizeWrite('mailing');
$liste = MailingList::with('firma')->where('attiva', true)->orderBy('nome')->get();
$liste = MailingList::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', 'firme'));
return view('mailing.invio', compact('liste', 'documenti', 'senderAccounts'));
}
public function invioElabora(Request $request)
@@ -222,23 +193,18 @@ class MailingController extends Controller
$this->authorizeWrite('mailing');
$data = $request->validate([
'liste' => 'required|array',
'liste.*' => 'exists:mailing_lists,id',
'liste.*' => 'exists:mailing_liste,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 = [];
@@ -286,24 +252,9 @@ class MailingController extends Controller
return back()->with('error', 'Nessun destinatario email valido trovato nelle liste selezionate.');
}
$attachmentResult = $this->resolveMailingAttachmentPaths($documentiAllegati);
$attachmentPaths = $attachmentResult['paths'];
$linkDocs = $attachmentResult['links'];
$attachmentPaths = $this->resolveMailingAttachmentPaths($documentiAllegati);
$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(', ');
$listeNomi = MailingList::whereIn('id', $data['liste'])->pluck('nome')->implode(', ');
$messaggio = MailingMessaggio::create([
'mailing_list_id' => null,
@@ -314,7 +265,6 @@ class MailingController extends Controller
'totale_destinatari' => $emails->count(),
'mittente_nome' => $sender?->email_name,
'mittente_email' => $sender?->email_address,
'firma_id' => $firmaId,
]);
$ok = 0;
@@ -324,9 +274,9 @@ class MailingController extends Controller
foreach ($emails as $email) {
try {
if ($sender) {
$sender->sendEmail($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
$sender->sendEmail($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
} else {
$this->sendViaSystem($email, $data['oggetto'], $corpoConFirma, $attachmentPaths);
$this->sendViaSystem($email, $data['oggetto'], $data['corpo'], $attachmentPaths);
}
$ok++;
} catch (\Throwable $e) {
@@ -397,10 +347,18 @@ class MailingController extends Controller
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromAddress = \Symfony\Component\Mime\Address::create(
$settings->getEffectiveFromAddress(),
$settings->getEffectiveFromName()
);
$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)
@@ -408,11 +366,6 @@ class MailingController extends Controller
->subject($subject)
->text($body);
$replyTo = $settings->getEffectiveReplyTo();
if ($replyTo) {
$email->replyTo($replyTo);
}
foreach ($attachmentPaths as $path) {
$email->attachFromPath($path);
}
@@ -423,34 +376,17 @@ class MailingController extends Controller
private function resolveMailingAttachmentPaths(array $documentIds): array
{
$paths = [];
$links = [];
if (empty($documentIds)) {
return ['paths' => $paths, 'links' => $links];
return $paths;
}
$docs = Documento::whereIn('id', $documentIds)->get();
foreach ($docs as $doc) {
if ($doc->isLink()) {
$links[] = ['nome' => $doc->nome_file, 'url' => $doc->url];
} elseif ($doc->file_path && Storage::exists($doc->file_path)) {
if ($doc->file_path && Storage::exists($doc->file_path)) {
$paths[] = Storage::path($doc->file_path);
}
}
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;
return $paths;
}
}
+3 -53
View File
@@ -3,7 +3,6 @@
namespace App\Http\Controllers;
use App\Models\MailingList;
use App\Models\SenderAccount;
use App\Models\Tag;
use Illuminate\Http\Request;
@@ -61,10 +60,7 @@ class MailingListController extends Controller
{
$this->authorizeWrite('mailing');
$tags = Tag::orderBy('name')->get();
$settings = \App\Models\EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$senderAccounts = SenderAccount::active()->get();
return view('mailing-liste.create', compact('tags', 'firme', 'senderAccounts'));
return view('mailing-liste.create', compact('tags'));
}
public function store(Request $request)
@@ -77,8 +73,6 @@ 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();
@@ -89,8 +83,6 @@ 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')) {
@@ -125,13 +117,10 @@ class MailingListController extends Controller
public function edit($mailingList)
{
$this->authorizeWrite('mailing');
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags', 'firma', 'senderAccount'])->findOrFail($mailingList);
$mailingList = MailingList::with(['contatti.individuo.contatti', 'tags'])->findOrFail($mailingList);
$tags = Tag::orderBy('name')->get();
$selectedTags = $mailingList->tags->pluck('id')->toArray();
$settings = \App\Models\EmailSetting::getActive();
$firme = $settings?->firme ?? collect();
$senderAccounts = SenderAccount::active()->get();
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags', 'firme', 'senderAccounts'));
return view('mailing-liste.edit', compact('mailingList', 'tags', 'selectedTags'));
}
public function update(Request $request, $mailingList)
@@ -146,8 +135,6 @@ 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;
@@ -156,8 +143,6 @@ 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')) {
@@ -201,41 +186,6 @@ 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');
+4 -4
View File
@@ -515,7 +515,7 @@ class ReportController extends Controller
$rows[] = [
'livello' => 0,
'nome' => $gruppo->nome,
'diocesi' => $gruppo->diocesi->count() > 0 ? $gruppo->diocesi->pluck('nome')->implode(', ') : '-',
'diocesi' => $gruppo->diocesi?->nome ?? '-',
'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->count() > 0 ? $child->diocesi->pluck('nome')->implode(', ') : '-',
'diocesi' => $child->diocesi?->nome ?? '-',
'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->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
'diocesi' => $g->diocesi?->nome ?? '-',
'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->count() > 0 ? $g->diocesi->pluck('nome')->implode(', ') : '-',
'diocesi' => $g->diocesi?->nome ?? '-',
'livello' => $g->parent_id ? (count($g->getAncestors()) + 1) : 0,
'membri' => $g->individui_count,
'padre' => $parent?->nome ?? '-',
+2 -4
View File
@@ -53,13 +53,11 @@ class CalendarioConnessione extends Model
return $config;
}
public function encryptAndSetConfig(array $config, array $existingConfig = []): void
public function encryptAndSetConfig(array $config): void
{
foreach (['username', 'password', 'client_secret', 'refresh_token', 'api_key'] as $field) {
if (!empty($config[$field]) && !str_starts_with($config[$field], 'eyJpdiI')) {
if (isset($config[$field]) && $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 -3
View File
@@ -3,15 +3,15 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Diocesi extends Model
{
protected $table = 'diocesi';
protected $fillable = ['nome', 'regione'];
public function gruppi(): BelongsToMany
public function gruppi(): HasMany
{
return $this->belongsToMany(Gruppo::class, 'diocesi_gruppo');
return $this->hasMany(Gruppo::class);
}
}
+1 -8
View File
@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Traits\HasTagsLight;
@@ -18,14 +16,9 @@ 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', 'url'
'mime_type', 'dimensione', 'note'
];
public function isLink(): bool
{
return $this->tipo === 'link';
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
+8 -156
View File
@@ -2,9 +2,7 @@
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;
@@ -15,17 +13,14 @@ 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', 'from_email', 'from_name', 'reply_to',
'sync_interval_minutes',
'last_sync_at', 'is_active', 'signature', 'signature_enabled',
'auth_method', 'google_oauth_connection_id',
'email_address', 'email_name', 'reply_to', 'sync_interval_minutes',
'last_sync_at', 'is_active', 'signature', 'signature_enabled'
];
protected $casts = [
'is_active' => 'boolean',
'signature_enabled' => 'boolean',
'last_sync_at' => 'datetime',
'google_oauth_connection_id' => 'integer',
];
public function folders(): HasMany
@@ -33,25 +28,8 @@ 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,
@@ -63,10 +41,6 @@ class EmailSetting extends Model
public function getDecryptedPassword(): string
{
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->imap_password)) {
return '';
}
@@ -79,10 +53,6 @@ class EmailSetting extends Model
public function getDecryptedSmtpPassword(): string
{
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->smtp_password)) {
return $this->getDecryptedPassword();
}
@@ -93,22 +63,6 @@ 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 [
@@ -133,101 +87,33 @@ class EmailSetting extends Model
default => 'none',
};
$config = [
return new \DirectoryTree\ImapEngine\Mailbox([
'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 {
$mailbox = $this->getImapClient();
if (!$mailbox) {
return ['success' => false, 'message' => 'Configurazione IMAP non valida'];
}
$mailbox->connect();
$mailbox->disconnect();
return [
'success' => true,
'success' => true,
'message' => 'Connessione IMAP riuscita! Server: ' . $this->imap_host
];
} catch (\Exception $e) {
@@ -248,38 +134,4 @@ 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();
}
}
}
-2
View File
@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Traits\HasTagsLight;
-29
View File
@@ -1,29 +0,0 @@
<?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);
}
}
-53
View File
@@ -1,53 +0,0 @@
<?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;
}
}
+3 -23
View File
@@ -15,18 +15,15 @@ class Gruppo extends Model
protected $table = 'gruppi';
protected $fillable = [
'tenant_id', 'parent_id', 'diocesi_id', 'responsabile_ids',
'nome', 'anno_fondazione', 'descrizione', 'indirizzo_incontro', 'cap_incontro',
'nome', '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);
@@ -42,14 +39,9 @@ class Gruppo extends Model
return $this->hasMany(Gruppo::class, 'parent_id');
}
public function diocesi(): BelongsToMany
public function diocesi(): BelongsTo
{
return $this->belongsToMany(Diocesi::class, 'diocesi_gruppo');
}
public function gruppoContatti(): HasMany
{
return $this->hasMany(GruppoContatto::class, 'gruppo_id');
return $this->belongsTo(Diocesi::class);
}
public function individui(): BelongsToMany
@@ -74,18 +66,6 @@ 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)) {
-23
View File
@@ -1,23 +0,0 @@
<?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);
}
}
+1 -11
View File
@@ -12,7 +12,7 @@ class MailingList extends Model
use HasTagsLight;
protected $table = 'mailing_lists';
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva', 'firma_id', 'sender_account_id'];
protected $fillable = ['tenant_id', 'user_id', 'nome', 'descrizione', 'attiva'];
protected $casts = ['attiva' => 'boolean'];
@@ -36,16 +36,6 @@ 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) {
+1 -1
View File
@@ -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', 'firma_id',
'log_falliti', 'mittente_nome', 'mittente_email',
];
protected $casts = [
+2 -54
View File
@@ -4,10 +4,8 @@ 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
@@ -26,14 +24,11 @@ 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
@@ -41,33 +36,8 @@ 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 '';
}
@@ -151,33 +121,11 @@ 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);
}
$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)) {
-2
View File
@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
+4 -5
View File
@@ -197,13 +197,12 @@ class User extends Authenticatable implements CanResetPasswordContract
$resetUrl = url('/password/reset/' . $token);
$fromAddress = new Address(
$settings->getEffectiveFromAddress(),
$settings->getEffectiveFromName()
);
$fromName = $settings->email_name ?? config('app.name');
$fromName = preg_replace('/[^\p{L}\p{N}\s]/u', '', $fromName);
$fromName = trim($fromName) ?: config('app.name');
$email = (new Email())
->from($fromAddress)
->from(new Address($settings->email_address, $fromName))
->to($this->email)
->subject('Reset della password - ' . config('app.name'))
->html(view('auth.emails.reset-password', [
+10 -29
View File
@@ -60,7 +60,7 @@ class BackupService
return $backups;
}
public function run(array $options = []): array
public function run(): array
{
$result = ['success' => true, 'message' => '', 'filename' => ''];
@@ -74,28 +74,12 @@ 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();
$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);
$steps[] = $this->backupEnv();
$steps[] = $this->backupFiles();
$this->writeManifest($timestamp);
$zipResult = $this->createZip($filepath);
if (!$zipResult) {
@@ -106,7 +90,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();
@@ -116,7 +100,6 @@ class BackupService
'filename' => $filename,
'size' => $size,
'size_formatted' => $this->formatBytes($size),
'steps' => $steps,
];
} catch (\Exception $e) {
$this->cleanTemp();
@@ -126,7 +109,6 @@ class BackupService
'success' => false,
'message' => 'Backup fallito: ' . $e->getMessage(),
'filename' => '',
'steps' => [],
];
}
}
@@ -199,9 +181,9 @@ class BackupService
return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')';
}
private function backupEnv(bool $includeEnv = true): string
private function backupEnv(): string
{
if (!$includeEnv) {
if (!AppSetting::getSetting('backup_include_env', true)) {
return '.env (escluso)';
}
@@ -218,9 +200,9 @@ class BackupService
return '.env copiato';
}
private function backupFiles(bool $includeFiles = true): string
private function backupFiles(): string
{
if (!$includeFiles) {
if (!AppSetting::getSetting('backup_include_files', true)) {
return 'files (esclusi)';
}
@@ -238,7 +220,7 @@ class BackupService
return 'files copiati';
}
private function writeManifest(string $timestamp, array $includedComponents = ['database']): void
private function writeManifest(string $timestamp): void
{
$manifest = [
'created_at' => $timestamp,
@@ -248,7 +230,6 @@ 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));
-178
View File
@@ -1,178 +0,0 @@
<?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;
}
}
+6 -3
View File
@@ -19,9 +19,12 @@ return Application::configure(basePath: dirname(__DIR__))
'permission' => CheckPermission::class,
]);
$middleware->trustProxies(at: '*');
// 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.
$middleware->web(append: [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
+21 -227
View File
@@ -20,223 +20,6 @@ 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}" \
@@ -277,8 +60,6 @@ tar czf "${ARCHIVE}" \
--warning=no-file-changed \
.
rm -f post-deploy.sh
echo ""
echo "============================================"
echo " Archivio creato: ${ARCHIVE}"
@@ -289,14 +70,27 @@ echo "Per estrarre sul server di destinazione:"
echo " tar xzf ${ARCHIVE}"
echo ""
echo "Poi esegui:"
echo " # Upgrade di un'installazione esistente:"
echo " bash post-deploy.sh upgrade"
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 ""
echo " # Fresh install:"
echo " bash post-deploy.sh install"
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 ""
echo " # Solo controllo (nessuna modifica):"
echo " bash post-deploy.sh check"
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 ""
echo " # Batch mode (nessuna conferma):"
echo " bash post-deploy.sh upgrade --yes"
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"
-6
View File
@@ -35,10 +35,4 @@ 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,10 +8,6 @@ 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,30 +8,26 @@ 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();
});
}
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');
$table->string('codice_istat', 10)->nullable();
$table->string('cap')->nullable();
$table->string('sigla_provincia', 2)->nullable();
$table->string('regione')->nullable();
$table->float('latitudine')->nullable();
$table->float('longitudine')->nullable();
$table->timestamps();
$table->index('nome');
$table->index('sigla_provincia');
});
}
Schema::create('comuni', function (Blueprint $table) {
$table->id();
$table->string('nome');
$table->string('codice_istat', 10)->nullable();
$table->string('cap')->nullable();
$table->string('sigla_provincia', 2)->nullable();
$table->string('regione')->nullable();
$table->float('latitudine')->nullable();
$table->float('longitudine')->nullable();
$table->timestamps();
$table->index('nome');
$table->index('sigla_provincia');
});
}
public function down(): void
@@ -8,29 +8,27 @@ 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');
$table->string('codice_id', 5)->unique();
$table->string('cognome');
$table->string('nome');
$table->date('data_nascita')->nullable();
$table->string('indirizzo')->nullable();
$table->string('cap', 10)->nullable();
$table->string('città')->nullable();
$table->string('sigla_provincia', 2)->nullable();
$table->enum('genere', ['M', 'F'])->nullable();
$table->enum('tipo_documento', ['carta_identita', 'patente'])->nullable();
$table->date('scadenza_documento')->nullable();
$table->text('note')->nullable();
$table->string('avatar_path')->nullable();
$table->timestamps();
$table->index('tenant_id');
$table->index('codice_id');
$table->index('cognome');
});
}
Schema::create('individui', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('codice_id', 5)->unique();
$table->string('cognome');
$table->string('nome');
$table->date('data_nascita')->nullable();
$table->string('indirizzo')->nullable();
$table->string('cap', 10)->nullable();
$table->string('città')->nullable();
$table->string('sigla_provincia', 2)->nullable();
$table->enum('genere', ['M', 'F'])->nullable();
$table->enum('tipo_documento', ['carta_identita', 'patente'])->nullable();
$table->date('scadenza_documento')->nullable();
$table->text('note')->nullable();
$table->string('avatar_path')->nullable();
$table->timestamps();
$table->index('tenant_id');
$table->index('codice_id');
$table->index('cognome');
});
}
public function down(): void
@@ -8,18 +8,16 @@ 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');
$table->enum('tipo', ['telefono', 'cellulare', 'email', 'fax', 'web', 'telegram', 'whatsapp', 'altro']);
$table->string('valore');
$table->string('etichetta')->nullable();
$table->boolean('is_primary')->default(false);
$table->timestamps();
$table->index('individuo_id');
});
}
Schema::create('contatti', function (Blueprint $table) {
$table->id();
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->enum('tipo', ['telefono', 'cellulare', 'email', 'fax', 'web', 'telegram', 'whatsapp', 'altro']);
$table->string('valore');
$table->string('etichetta')->nullable();
$table->boolean('is_primary')->default(false);
$table->timestamps();
$table->index('individuo_id');
});
}
public function down(): void
@@ -8,26 +8,24 @@ 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');
$table->foreignId('parent_id')->nullable()->constrained('gruppi')->onDelete('set null');
$table->foreignId('diocesi_id')->nullable()->constrained('diocesi')->onDelete('set null');
$table->foreignId('responsabile_id')->nullable()->constrained('individui')->onDelete('set null');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->string('indirizzo_incontro')->nullable();
$table->string('cap_incontro', 10)->nullable();
$table->string('città_incontro')->nullable();
$table->string('sigla_provincia_incontro', 2)->nullable();
$table->string('mappa_posizione')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index('tenant_id');
$table->index('parent_id');
});
}
Schema::create('gruppi', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->foreignId('parent_id')->nullable()->constrained('gruppi')->onDelete('set null');
$table->foreignId('diocesi_id')->nullable()->constrained('diocesi')->onDelete('set null');
$table->foreignId('responsabile_id')->nullable()->constrained('individui')->onDelete('set null');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->string('indirizzo_incontro')->nullable();
$table->string('cap_incontro', 10)->nullable();
$table->string('città_incontro')->nullable();
$table->string('sigla_provincia_incontro', 2)->nullable();
$table->string('mappa_posizione')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->index('tenant_id');
$table->index('parent_id');
});
}
public function down(): void
@@ -8,17 +8,15 @@ 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');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->string('ruolo_nel_gruppo')->nullable();
$table->date('data_adesione')->nullable();
$table->timestamps();
$table->unique(['gruppo_id', 'individuo_id']);
});
}
Schema::create('gruppo_individuo', function (Blueprint $table) {
$table->id();
$table->foreignId('gruppo_id')->constrained('gruppi')->onDelete('cascade');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->string('ruolo_nel_gruppo')->nullable();
$table->date('data_adesione')->nullable();
$table->timestamps();
$table->unique(['gruppo_id', 'individuo_id']);
});
}
public function down(): void
@@ -8,41 +8,35 @@ 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');
$table->string('nome_evento');
$table->string('tipo_evento')->nullable();
$table->enum('tipo_recorrenza', ['singolo', 'ricorrente'])->default('singolo');
$table->unsignedTinyInteger('giorno_settimana')->nullable();
$table->time('ora_inizio')->nullable();
$table->date('data_specifica')->nullable();
$table->unsignedInteger('durata_minuti')->nullable();
$table->text('descrizione')->nullable();
$table->text('note')->nullable();
$table->timestamps();
$table->index('tenant_id');
});
}
Schema::create('eventi', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('nome_evento');
$table->string('tipo_evento')->nullable();
$table->enum('tipo_recorrenza', ['singolo', 'ricorrente'])->default('singolo');
$table->unsignedTinyInteger('giorno_settimana')->nullable();
$table->time('ora_inizio')->nullable();
$table->date('data_specifica')->nullable();
$table->unsignedInteger('durata_minuti')->nullable();
$table->text('descrizione')->nullable();
$table->text('note')->nullable();
$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();
});
}
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');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->timestamps();
});
}
Schema::create('eventi_responsabili', function (Blueprint $table) {
$table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->timestamps();
});
}
public function down(): void
@@ -8,24 +8,22 @@ 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')->nullable();
$table->string('tipo')->nullable();
$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();
$table->unsignedBigInteger('dimensione')->nullable();
$table->text('note')->nullable();
$table->timestamps();
$table->index('tenant_id');
});
}
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('tipo')->nullable();
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento');
$table->enum('visibilita', ['pubblico', 'gruppo', 'individuo', 'associazione', 'federazione'])->default('gruppo');
$table->unsignedBigInteger('visibilita_target_id')->nullable();
$table->string('visibilita_target_type')->nullable();
$table->string('mime_type')->nullable();
$table->unsignedBigInteger('dimensione')->nullable();
$table->text('note')->nullable();
$table->timestamps();
$table->index('tenant_id');
});
}
public function down(): void
@@ -8,49 +8,43 @@ 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');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->boolean('attiva')->default(true);
$table->timestamps();
$table->index('tenant_id');
});
}
Schema::create('mailing_lists', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->boolean('attiva')->default(true);
$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');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->boolean('opt_in')->default(true);
$table->dateTime('opt_in_data')->nullable();
$table->dateTime('opt_out_data')->nullable();
$table->timestamps();
$table->unique(['mailing_list_id', 'individuo_id']);
});
}
Schema::create('mailing_contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('mailing_list_id')->constrained('mailing_lists')->onDelete('cascade');
$table->foreignId('individuo_id')->constrained('individui')->onDelete('cascade');
$table->boolean('opt_in')->default(true);
$table->dateTime('opt_in_data')->nullable();
$table->dateTime('opt_out_data')->nullable();
$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');
$table->foreignId('mailing_list_id')->nullable()->constrained('mailing_lists')->onDelete('set null');
$table->unsignedBigInteger('user_id')->nullable();
$table->string('oggetto');
$table->text('corpo')->nullable();
$table->enum('canale', ['email', 'telegram'])->default('email');
$table->enum('stato', ['bozza', 'in_coda', 'inviato', 'fallito'])->default('bozza');
$table->timestamp('inviato_al')->nullable();
$table->integer('totale_destinatari')->default(0);
$table->integer('invii_ok')->default(0);
$table->integer('invii_falliti')->default(0);
$table->timestamps();
$table->index('tenant_id');
});
}
Schema::create('mailing_messaggi', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->foreignId('mailing_list_id')->nullable()->constrained('mailing_lists')->onDelete('set null');
$table->unsignedBigInteger('user_id')->nullable();
$table->string('oggetto');
$table->text('corpo')->nullable();
$table->enum('canale', ['email', 'telegram'])->default('email');
$table->enum('stato', ['bozza', 'in_coda', 'inviato', 'fallito'])->default('bozza');
$table->timestamp('inviato_al')->nullable();
$table->integer('totale_destinatari')->default(0);
$table->integer('invii_ok')->default(0);
$table->integer('invii_falliti')->default(0);
$table->timestamps();
$table->index('tenant_id');
});
}
public function down(): void
@@ -8,20 +8,18 @@ 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();
$table->string('tipo');
$table->string('titolo');
$table->text('messaggio');
$table->string('link')->nullable();
$table->boolean('is_read')->default(false);
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index('user_id');
});
}
Schema::create('notifiche', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->nullable();
$table->string('tipo');
$table->string('titolo');
$table->text('messaggio');
$table->string('link')->nullable();
$table->boolean('is_read')->default(false);
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index('user_id');
});
}
public function down(): void
@@ -8,40 +8,34 @@ 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');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('is_admin')->default(false);
$table->rememberToken();
$table->timestamps();
$table->index('tenant_id');
$table->index('email');
});
}
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->foreignId('tenant_id')->nullable()->constrained('tenants')->onDelete('set null');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->boolean('is_admin')->default(false);
$table->rememberToken();
$table->timestamps();
$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();
});
}
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();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
Schema::create('sessions', function (Blueprint $table) {
$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
@@ -8,60 +8,50 @@ 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');
});
}
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');
});
}
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();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$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');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
}
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$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();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at');
});
}
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at');
});
}
public function down(): void
@@ -8,14 +8,12 @@ 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');
$table->foreignId('documento_id')->constrained('documenti')->onDelete('cascade');
$table->timestamps();
});
}
Schema::create('eventi_documenti', function (Blueprint $table) {
$table->id();
$table->foreignId('evento_id')->constrained('eventi')->onDelete('cascade');
$table->foreignId('documento_id')->constrained('documenti')->onDelete('cascade');
$table->timestamps();
});
}
public function down(): void
@@ -8,21 +8,19 @@ 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');
$table->string('nome');
$table->string('tipo'); // individui, gruppi, documenti, eventi
$table->json('colonne_visibili')->nullable();
$table->json('colonne_ordinamento')->nullable(); // [['colonna', 'direzione']]
$table->json('filtri')->nullable(); // [['colonna', 'operatore', 'valore']]
$table->string('ricerca')->nullable();
$table->timestamps();
$table->index('tipo');
$table->index('user_id');
});
}
Schema::create('viste_report', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->string('nome');
$table->string('tipo'); // individui, gruppi, documenti, eventi
$table->json('colonne_visibili')->nullable();
$table->json('colonne_ordinamento')->nullable(); // [['colonna', 'direzione']]
$table->json('filtri')->nullable(); // [['colonna', 'operatore', 'valore']]
$table->string('ricerca')->nullable();
$table->timestamps();
$table->index('tipo');
$table->index('user_id');
});
}
public function down(): void
@@ -8,27 +8,25 @@ 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();
$table->string('descrizione', 255)->nullable();
$table->integer('ordine')->default(0);
$table->boolean('attiva')->default(true);
$table->timestamps();
});
Schema::create('tipologie_documenti', function (Blueprint $table) {
$table->id();
$table->string('nome', 50)->unique();
$table->string('descrizione', 255)->nullable();
$table->integer('ordine')->default(0);
$table->boolean('attiva')->default(true);
$table->timestamps();
});
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
foreach ($defaultTipologie as $i => $nome) {
DB::table('tipologie_documenti')->insert([
'nome' => $nome,
'descrizione' => ucfirst($nome),
'ordine' => $i,
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
$defaultTipologie = ['avatar', 'galleria', 'documento', 'statuto', 'altro'];
foreach ($defaultTipologie as $i => $nome) {
DB::table('tipologie_documenti')->insert([
'nome' => $nome,
'descrizione' => ucfirst($nome),
'ordine' => $i,
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
@@ -14,32 +14,28 @@ 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');
$table->string('action');
$table->string('module');
$table->string('description')->nullable();
$table->json('details')->nullable();
$table->string('ip_address', 45)->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['user_id', 'module']);
$table->index(['created_at']);
$table->index(['action']);
});
}
Schema::create('activity_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('action');
$table->string('module');
$table->string('description')->nullable();
$table->json('details')->nullable();
$table->string('ip_address', 45)->nullable();
$table->timestamp('created_at')->useCurrent();
if (!Schema::hasTable('role_presets')) {
Schema::create('role_presets', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->unique();
$table->string('description')->nullable();
$table->json('permissions');
$table->timestamps();
});
}
$table->index(['user_id', 'module']);
$table->index(['created_at']);
$table->index(['action']);
});
Schema::create('role_presets', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->unique();
$table->string('description')->nullable();
$table->json('permissions');
$table->timestamps();
});
}
public function down(): void
@@ -23,106 +23,96 @@ 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');
$table->string('guard_name');
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
}
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
$table->id(); // permission id
$table->string('name');
$table->string('guard_name');
$table->timestamps();
$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
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
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
}
$table->string('name');
$table->string('guard_name');
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
} else {
$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);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
if ($teams) {
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'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);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
if ($teams) {
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'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);
$table->foreign($pivotPermission)
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->cascadeOnDelete();
$table->foreign($pivotRole)
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->cascadeOnDelete();
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
}
$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,28 +8,26 @@ 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();
$table->integer('imap_port')->nullable()->default(993);
$table->string('imap_encryption')->nullable()->default('ssl');
$table->string('imap_username')->nullable();
$table->string('imap_password')->nullable();
$table->string('smtp_host')->nullable();
$table->integer('smtp_port')->nullable()->default(587);
$table->string('smtp_encryption')->nullable()->default('tls');
$table->string('smtp_username')->nullable();
$table->string('smtp_password')->nullable();
$table->string('email_address')->nullable();
$table->string('email_name')->nullable();
$table->string('reply_to')->nullable();
$table->integer('sync_interval_minutes')->default(5);
$table->timestamp('last_sync_at')->nullable();
$table->boolean('is_active')->default(false);
$table->timestamps();
});
}
Schema::create('email_settings', function (Blueprint $table) {
$table->id();
$table->string('imap_host')->nullable();
$table->integer('imap_port')->nullable()->default(993);
$table->string('imap_encryption')->nullable()->default('ssl');
$table->string('imap_username')->nullable();
$table->string('imap_password')->nullable();
$table->string('smtp_host')->nullable();
$table->integer('smtp_port')->nullable()->default(587);
$table->string('smtp_encryption')->nullable()->default('tls');
$table->string('smtp_username')->nullable();
$table->string('smtp_password')->nullable();
$table->string('email_address')->nullable();
$table->string('email_name')->nullable();
$table->string('reply_to')->nullable();
$table->integer('sync_interval_minutes')->default(5);
$table->timestamp('last_sync_at')->nullable();
$table->boolean('is_active')->default(false);
$table->timestamps();
});
}
public function down(): void
@@ -8,19 +8,17 @@ 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');
$table->string('imap_folder_name')->nullable();
$table->string('type')->default('custom');
$table->string('icon')->default('fa-folder');
$table->string('color')->nullable();
$table->integer('sort_order')->default(0);
$table->boolean('is_system')->default(false);
$table->timestamps();
});
}
Schema::create('email_folders', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('imap_folder_name')->nullable();
$table->string('type')->default('custom');
$table->string('icon')->default('fa-folder');
$table->string('color')->nullable();
$table->integer('sort_order')->default(0);
$table->boolean('is_system')->default(false);
$table->timestamps();
});
}
public function down(): void
@@ -8,37 +8,35 @@ 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');
$table->string('message_id')->nullable()->index();
$table->string('subject')->nullable();
$table->string('from_name')->nullable();
$table->string('from_email')->nullable();
$table->string('to_name')->nullable();
$table->string('to_email')->nullable();
$table->text('cc')->nullable();
$table->text('bcc')->nullable();
$table->longText('body_text')->nullable();
$table->longText('body_html')->nullable();
$table->boolean('is_read')->default(false);
$table->boolean('is_starred')->default(false);
$table->boolean('is_important')->default(false);
$table->boolean('is_sent')->default(false);
$table->boolean('is_draft')->default(false);
$table->boolean('is_trash')->default(false);
$table->timestamp('received_at')->nullable();
$table->timestamp('sent_at')->nullable();
$table->unsignedBigInteger('imap_uid')->nullable();
$table->timestamps();
$table->index('is_read');
$table->index('is_starred');
$table->index('is_sent');
$table->index('received_at');
});
}
Schema::create('email_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('email_folder_id')->constrained('email_folders')->onDelete('cascade');
$table->string('message_id')->nullable()->index();
$table->string('subject')->nullable();
$table->string('from_name')->nullable();
$table->string('from_email')->nullable();
$table->string('to_name')->nullable();
$table->string('to_email')->nullable();
$table->text('cc')->nullable();
$table->text('bcc')->nullable();
$table->longText('body_text')->nullable();
$table->longText('body_html')->nullable();
$table->boolean('is_read')->default(false);
$table->boolean('is_starred')->default(false);
$table->boolean('is_important')->default(false);
$table->boolean('is_sent')->default(false);
$table->boolean('is_draft')->default(false);
$table->boolean('is_trash')->default(false);
$table->timestamp('received_at')->nullable();
$table->timestamp('sent_at')->nullable();
$table->unsignedBigInteger('imap_uid')->nullable();
$table->timestamps();
$table->index('is_read');
$table->index('is_starred');
$table->index('is_sent');
$table->index('received_at');
});
}
public function down(): void
@@ -1,19 +1,22 @@
<?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
{
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
// Application-level validation handles allowed values.
Schema::table('documenti', function (Blueprint $table) {
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro', 'email_attachment'])->default('documento')->change();
});
}
public function down(): void
{
// No-op: column is string, not enum.
Schema::table('documenti', function (Blueprint $table) {
$table->enum('tipologia', ['avatar', 'galleria', 'documento', 'statuto', 'altro'])->default('documento')->change();
});
}
};
@@ -7,15 +7,14 @@ return new class extends Migration
{
public function up(): void
{
DB::table('tipologie_documenti')->updateOrInsert(
['nome' => 'email_attachment'],
[
'descrizione' => 'Allegato email',
'ordine' => 100,
'attiva' => true,
'updated_at' => now(),
]
);
DB::table('tipologie_documenti')->insert([
'nome' => 'email_attachment',
'descrizione' => 'Allegato email',
'ordine' => 100,
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
public function down(): void
@@ -8,19 +8,17 @@ 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');
$table->string('filename');
$table->string('mime_type')->nullable();
$table->bigInteger('size')->default(0);
$table->string('file_path');
$table->boolean('is_saved_to_documenti')->default(false);
$table->foreignId('documenti_id')->nullable()->constrained('documenti')->onDelete('set null');
$table->timestamps();
});
}
Schema::create('email_attachments', function (Blueprint $table) {
$table->id();
$table->foreignId('email_message_id')->constrained('email_messages')->onDelete('cascade');
$table->string('filename');
$table->string('mime_type')->nullable();
$table->bigInteger('size')->default(0);
$table->string('file_path');
$table->boolean('is_saved_to_documenti')->default(false);
$table->foreignId('documenti_id')->nullable()->constrained('documenti')->onDelete('set null');
$table->timestamps();
});
}
public function down(): void
@@ -8,36 +8,34 @@ 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();
$table->string('descrizione', 255)->nullable();
$table->integer('ordine')->default(0);
$table->boolean('attiva')->default(true);
$table->timestamps();
});
Schema::create('ruoli', function (Blueprint $table) {
$table->id();
$table->string('nome', 50)->unique();
$table->string('descrizione', 255)->nullable();
$table->integer('ordine')->default(0);
$table->boolean('attiva')->default(true);
$table->timestamps();
});
$defaultRuoli = [
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
['nome' => 'Catechista', 'descrizione' => 'Catechista del gruppo'],
['nome' => 'Animatore', 'descrizione' => 'Animatore del gruppo'],
['nome' => 'Responsabile', 'descrizione' => 'Responsabile del gruppo'],
['nome' => 'Vice Responsabile', 'descrizione' => 'Vice responsabile del gruppo'],
['nome' => 'Segretario', 'descrizione' => 'Segretario del gruppo'],
['nome' => 'Tesoriere', 'descrizione' => 'Tesoriere del gruppo'],
];
$defaultRuoli = [
['nome' => 'Membro', 'descrizione' => 'Membro del gruppo'],
['nome' => 'Catechista', 'descrizione' => 'Catechista del gruppo'],
['nome' => 'Animatore', 'descrizione' => 'Animatore del gruppo'],
['nome' => 'Responsabile', 'descrizione' => 'Responsabile del gruppo'],
['nome' => 'Vice Responsabile', 'descrizione' => 'Vice responsabile del gruppo'],
['nome' => 'Segretario', 'descrizione' => 'Segretario del gruppo'],
['nome' => 'Tesoriere', 'descrizione' => 'Tesoriere del gruppo'],
];
foreach ($defaultRuoli as $i => $ruolo) {
DB::table('ruoli')->insert([
'nome' => $ruolo['nome'],
'descrizione' => $ruolo['descrizione'],
'ordine' => $i,
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
foreach ($defaultRuoli as $i => $ruolo) {
DB::table('ruoli')->insert([
'nome' => $ruolo['nome'],
'descrizione' => $ruolo['descrizione'],
'ordine' => $i,
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_id')) {
@@ -8,13 +8,10 @@ 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');
});
}
Schema::table('gruppo_individuo', function (Blueprint $table) {
$table->dropForeign(['ruolo_id']);
$table->dropColumn('ruolo_id');
});
if (!Schema::hasColumn('gruppo_individuo', 'ruolo_ids')) {
Schema::table('gruppo_individuo', function (Blueprint $table) {
@@ -1,19 +1,17 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// Column type changed to string in 2024_01_01_000008_create_documenti_table.php.
// Application-level validation handles allowed values.
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro', 'programma', 'locandina') DEFAULT 'documento'");
}
public function down(): void
{
// No-op: column is string, not enum.
DB::statement("ALTER TABLE documenti MODIFY COLUMN tipologia ENUM('avatar', 'galleria', 'documento', 'statuto', 'altro') DEFAULT 'documento'");
}
};
@@ -8,17 +8,15 @@ 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');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->string('tipo_report');
$table->json('config')->nullable();
$table->timestamps();
});
}
Schema::create('report_custom', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->string('nome');
$table->text('descrizione')->nullable();
$table->string('tipo_report');
$table->json('config')->nullable();
$table->timestamps();
});
}
public function down(): void
@@ -10,27 +10,25 @@ 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();
$table->string('descrizione', 255)->nullable();
$table->integer('ordine')->default(0);
$table->boolean('attiva')->default(true);
$table->timestamps();
});
Schema::create('tipologie_eventi', function (Blueprint $table) {
$table->id();
$table->string('nome', 50)->unique();
$table->string('descrizione', 255)->nullable();
$table->integer('ordine')->default(0);
$table->boolean('attiva')->default(true);
$table->timestamps();
});
DB::table('tipologie_eventi')->insert([
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
['nome' => 'liturgia', 'descrizione' => 'Liturgia', 'ordine' => 1, 'attiva' => true],
['nome' => 'animazione', 'descrizione' => 'Animazione', 'ordine' => 2, 'attiva' => true],
['nome' => 'formazione', 'descrizione' => 'Formazione', 'ordine' => 3, 'attiva' => true],
['nome' => 'incontro', 'descrizione' => 'Incontro', 'ordine' => 4, 'attiva' => true],
['nome' => 'riunione', 'descrizione' => 'Riunione', 'ordine' => 5, 'attiva' => true],
['nome' => 'festa', 'descrizione' => 'Festa / Sagra', 'ordine' => 6, 'attiva' => true],
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
]);
}
DB::table('tipologie_eventi')->insert([
['nome' => 'catechesi', 'descrizione' => 'Catechesi', 'ordine' => 0, 'attiva' => true],
['nome' => 'liturgia', 'descrizione' => 'Liturgia', 'ordine' => 1, 'attiva' => true],
['nome' => 'animazione', 'descrizione' => 'Animazione', 'ordine' => 2, 'attiva' => true],
['nome' => 'formazione', 'descrizione' => 'Formazione', 'ordine' => 3, 'attiva' => true],
['nome' => 'incontro', 'descrizione' => 'Incontro', 'ordine' => 4, 'attiva' => true],
['nome' => 'riunione', 'descrizione' => 'Riunione', 'ordine' => 5, 'attiva' => true],
['nome' => 'festa', 'descrizione' => 'Festa / Sagra', 'ordine' => 6, 'attiva' => true],
['nome' => 'altro', 'descrizione' => 'Altro', 'ordine' => 7, 'attiva' => true],
]);
}
public function down(): void
@@ -10,23 +10,21 @@ 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();
$table->string('email_name')->nullable();
$table->string('smtp_host')->nullable();
$table->integer('smtp_port')->default(587);
$table->string('smtp_encryption')->default('tls');
$table->string('smtp_username')->nullable();
$table->text('smtp_password')->nullable();
$table->string('reply_to')->nullable();
$table->string('verify_email')->nullable();
$table->text('note')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
Schema::create('sender_accounts', function (Blueprint $table) {
$table->id();
$table->string('email_address')->unique();
$table->string('email_name')->nullable();
$table->string('smtp_host')->nullable();
$table->integer('smtp_port')->default(587);
$table->string('smtp_encryption')->default('tls');
$table->string('smtp_username')->nullable();
$table->text('smtp_password')->nullable();
$table->string('reply_to')->nullable();
$table->string('verify_email')->nullable();
$table->text('note')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
public function down(): void
@@ -8,14 +8,12 @@ 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');
$table->string('nome');
$table->timestamps();
});
}
Schema::create('documenti_cartelle', function (Blueprint $table) {
$table->id();
$table->foreignId('parent_id')->nullable()->constrained('documenti_cartelle')->onDelete('cascade');
$table->string('nome');
$table->timestamps();
});
}
public function down(): void
@@ -8,17 +8,15 @@ 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');
$table->string('tipo', 50);
$table->text('config');
$table->boolean('is_active')->default(true);
$table->integer('ordine')->default(0);
$table->timestamps();
});
}
Schema::create('storage_repositories', function (Blueprint $table) {
$table->id();
$table->string('nome');
$table->string('tipo', 50);
$table->text('config');
$table->boolean('is_active')->default(true);
$table->integer('ordine')->default(0);
$table->timestamps();
});
}
public function down(): void
@@ -10,23 +10,21 @@ 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');
$table->string('tipo'); // google_calendar, caldav
$table->text('config'); // JSON
$table->string('stato')->default('disconnesso'); // connesso, disconnesso, errore
$table->string('sync_direction')->default('bidirectional'); // import, export, bidirectional
$table->integer('sync_interval_minutes')->default(60);
$table->timestamp('last_sync_at')->nullable();
$table->timestamp('last_error_at')->nullable();
$table->text('last_error_message')->nullable();
$table->boolean('is_active')->default(true);
$table->integer('ordine')->default(0);
$table->timestamps();
});
}
Schema::create('calendario_connessioni', function (Blueprint $table) {
$table->id();
$table->string('nome');
$table->string('tipo'); // google_calendar, caldav
$table->text('config'); // JSON
$table->string('stato')->default('disconnesso'); // connesso, disconnesso, errore
$table->string('sync_direction')->default('bidirectional'); // import, export, bidirectional
$table->integer('sync_interval_minutes')->default(60);
$table->timestamp('last_sync_at')->nullable();
$table->timestamp('last_error_at')->nullable();
$table->text('last_error_message')->nullable();
$table->boolean('is_active')->default(true);
$table->integer('ordine')->default(0);
$table->timestamps();
});
}
public function down(): void
@@ -18,7 +18,7 @@ return new class extends Migration
$table->string('color', 7)->nullable();
$table->integer('order_column')->default(0);
$table->timestamps();
});
});
}
if (!Schema::hasTable('taggables')) {
@@ -26,7 +26,7 @@ return new class extends Migration
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->morphs('taggable');
$table->primary(['tag_id', 'taggable_id', 'taggable_type']);
});
});
}
}
@@ -1,35 +0,0 @@
<?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');
}
};
@@ -1,54 +0,0 @@
<?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']);
}
});
}
};
@@ -1,36 +0,0 @@
<?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']);
});
}
};
@@ -1,31 +0,0 @@
<?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');
}
};
@@ -1,27 +0,0 @@
<?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');
}
};
@@ -1,27 +0,0 @@
<?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');
}
};
@@ -1,25 +0,0 @@
<?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');
});
}
};
@@ -1,25 +0,0 @@
<?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');
});
}
};
@@ -1,29 +0,0 @@
<?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');
}
};
@@ -1,25 +0,0 @@
<?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');
});
}
};
@@ -1,29 +0,0 @@
<?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');
});
}
}
};
@@ -1,28 +0,0 @@
<?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');
});
}
};
@@ -1,24 +0,0 @@
<?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');
}
}
};
+1 -1
View File
@@ -68,7 +68,7 @@ class ComuniSeeder extends Seeder
];
foreach ($comuni as $c) {
Comune::firstOrCreate(['codice_istat' => $c['codice_istat']], $c);
Comune::create($c);
}
}
}
+17 -21
View File
@@ -20,27 +20,23 @@ class DatabaseSeeder extends Seeder
DockerBaseDataSeeder::class,
]);
$admin = User::firstOrCreate(
['email' => 'admin@glastree.local'],
[
'name' => 'Admin',
'password' => bcrypt('password'),
'is_admin' => true,
'status' => 'active',
'permissions' => [
'individui' => 2,
'gruppi' => 2,
'eventi' => 2,
'documenti' => 2,
'mailing' => 2,
'viste' => 2,
],
]
);
$admin = User::create([
'name' => 'Admin',
'email' => 'admin@glastree.local',
'password' => bcrypt('password'),
'is_admin' => true,
'status' => 'active',
'permissions' => [
'individui' => 2,
'gruppi' => 2,
'eventi' => 2,
'documenti' => 2,
'mailing' => 2,
'viste' => 2,
],
]);
if (!$admin->role_preset_id) {
$admin->role_preset_id = 1;
$admin->save();
}
$admin->role_preset_id = 1;
$admin->save();
}
}
+139 -244
View File
@@ -1,7 +1,5 @@
<?php
declare(strict_types=1);
namespace Database\Seeders;
use Illuminate\Database\Seeder;
@@ -11,250 +9,147 @@ 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 = [
'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',
['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'],
];
foreach ($diocesi as $nome) {
Diocesi::firstOrCreate(['nome' => $nome]);
foreach ($diocesi as $d) {
Diocesi::create($d);
}
}
}
}
-1162
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
-1
View File
@@ -1 +0,0 @@
../laravel-vite-plugin/bin/clean.js
Generated Vendored
-1
View File
@@ -1 +0,0 @@
../concurrently/dist/bin/concurrently.js
-1
View File
@@ -1 +0,0 @@
../concurrently/dist/bin/concurrently.js
Generated Vendored
-1
View File
@@ -1 +0,0 @@
../jiti/lib/jiti-cli.mjs
Generated Vendored
-1
View File
@@ -1 +0,0 @@
../nanoid/bin/nanoid.cjs
Generated Vendored
-1
View File
@@ -1 +0,0 @@
../rolldown/bin/cli.mjs
-1
View File
@@ -1 +0,0 @@
../tree-kill/cli.js
Generated Vendored
-1
View File
@@ -1 +0,0 @@
../vite/bin/vite.js

Some files were not shown because too many files have changed in this diff Show More