Posta configurabile dall'app, contatti dei proclamatori, conferma email
- Server di posta in Impostazioni (password cifrata, email di prova): se compilato prende il posto del .env; non si esporta e l'import XML non lo cancella. MAIL_SCHEME=tls nel .env.example non era valido per Laravel 11. - Proclamatori con email e telefono facoltativi, cifrati, nell'XML e cancellati dall'anonimizzazione; il numero apre WhatsApp. - Dopo l'assegnazione un riquadro in evidenza con il link del PDF, "Invia su WhatsApp" con il messaggio pronto e la scelta "inviato / non ancora", che resta finché non si sceglie. - Conferma dell'email obbligatoria per tutti gli utenti, anche esistenti: email in italiano, link valido senza login, nuova conferma a ogni cambio di indirizzo, "Reinvia conferma" in Utenti. Senza conferma restano raggiungibili profilo, cambio password e, per l'admin, il server di posta; comando utenti:conferma-email per l'emergenza. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
132
app/Livewire/Settings/PostaElettronica.php
Normal file
132
app/Livewire/Settings/PostaElettronica.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Settings;
|
||||
|
||||
use App\Livewire\Concerns\Avvisa;
|
||||
use App\Models\Setting;
|
||||
use App\Services\ConfigurazionePosta;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Component;
|
||||
|
||||
/**
|
||||
* Server di posta (solo amministratore).
|
||||
*
|
||||
* Serve alle email di conferma degli utenti. Raggiungibile anche da un
|
||||
* amministratore che non ha ancora confermato la sua email (vedi
|
||||
* routes/web.php): se la posta non funziona, è l'unica pagina da cui
|
||||
* sistemarla senza passare dal server.
|
||||
*/
|
||||
class PostaElettronica extends Component
|
||||
{
|
||||
use Avvisa;
|
||||
|
||||
public string $host = '';
|
||||
|
||||
public string $porta = '';
|
||||
|
||||
public string $cifratura = ConfigurazionePosta::CIFRATURA_AUTOMATICA;
|
||||
|
||||
public string $utente = '';
|
||||
|
||||
/** Vuota = si tiene quella salvata. Non torna mai al browser. */
|
||||
public string $password = '';
|
||||
|
||||
public string $mittente = '';
|
||||
|
||||
public string $nomeMittente = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$impostazioni = Setting::instance();
|
||||
|
||||
$this->host = (string) $impostazioni->mail_host;
|
||||
$this->porta = (string) ($impostazioni->mail_port ?? '');
|
||||
$this->cifratura = $impostazioni->mail_cifratura ?: ConfigurazionePosta::CIFRATURA_AUTOMATICA;
|
||||
$this->utente = (string) $impostazioni->mail_username;
|
||||
$this->mittente = (string) $impostazioni->mail_from_address;
|
||||
$this->nomeMittente = (string) $impostazioni->mail_from_name;
|
||||
}
|
||||
|
||||
public function salva(): void
|
||||
{
|
||||
$this->host = trim($this->host);
|
||||
$this->mittente = trim($this->mittente);
|
||||
|
||||
$this->validate([
|
||||
'host' => ['required', 'string', 'max:255', 'regex:/^[A-Za-z0-9.\-]+$/'],
|
||||
'porta' => ['nullable', 'integer', 'min:1', 'max:65535'],
|
||||
'cifratura' => ['required', Rule::in(array_keys(ConfigurazionePosta::CIFRATURE))],
|
||||
'utente' => ['nullable', 'string', 'max:255'],
|
||||
'password' => ['nullable', 'string', 'max:500'],
|
||||
'mittente' => ['required', 'email', 'max:255'],
|
||||
'nomeMittente' => ['nullable', 'string', 'max:255'],
|
||||
], [
|
||||
'host.required' => 'Indica il server SMTP (es. smtp.gmail.com).',
|
||||
'host.regex' => 'Solo il nome del server, senza "smtp://" né porta.',
|
||||
'mittente.required' => 'Indica l\'indirizzo da cui partono le email.',
|
||||
'mittente.email' => 'L\'indirizzo del mittente non è valido.',
|
||||
]);
|
||||
|
||||
$valori = [
|
||||
'mail_host' => $this->host,
|
||||
'mail_port' => $this->porta !== '' ? (int) $this->porta : null,
|
||||
'mail_cifratura' => $this->cifratura,
|
||||
'mail_username' => trim($this->utente) ?: null,
|
||||
'mail_from_address' => $this->mittente,
|
||||
'mail_from_name' => trim($this->nomeMittente) ?: null,
|
||||
];
|
||||
if ($this->password !== '') {
|
||||
$valori['mail_password'] = $this->password;
|
||||
}
|
||||
|
||||
Setting::instance()->update($valori);
|
||||
$this->password = '';
|
||||
|
||||
activity()->causedBy(auth()->user())->log('posta_configurata');
|
||||
$this->avvisa('Server di posta salvato. Prova l\'invio con "Invia email di prova".');
|
||||
}
|
||||
|
||||
public function inviaProva(ConfigurazionePosta $posta): void
|
||||
{
|
||||
if (! $posta->configurataNellApp()) {
|
||||
$this->avvisaErrore('Salva prima il server di posta.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$posta->inviaProva(auth()->user()->email);
|
||||
} catch (\RuntimeException $e) {
|
||||
$this->avvisaErrore($e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->avvisa('Email di prova inviata a ' . auth()->user()->email . '. Controlla anche lo spam.');
|
||||
}
|
||||
|
||||
/** Torna al server del .env. */
|
||||
public function rimuovi(): void
|
||||
{
|
||||
Setting::instance()->update(array_fill_keys(Setting::CAMPI_POSTA, null));
|
||||
$this->mount();
|
||||
$this->password = '';
|
||||
|
||||
activity()->causedBy(auth()->user())->log('posta_rimossa');
|
||||
$this->avvisa('Configurazione rimossa: valgono di nuovo le impostazioni del server (.env).');
|
||||
}
|
||||
|
||||
public function render(ConfigurazionePosta $posta)
|
||||
{
|
||||
$impostazioni = Setting::instance();
|
||||
|
||||
return view('livewire.settings.posta-elettronica', [
|
||||
'configurataNellApp' => $posta->configurataNellApp(),
|
||||
'funzionante' => $posta->funzionante(),
|
||||
'passwordSalvata' => $impostazioni->getRawOriginal('mail_password') !== null,
|
||||
'cifrature' => ConfigurazionePosta::CIFRATURE,
|
||||
'nonConfermato' => ! auth()->user()->hasVerifiedEmail(),
|
||||
'title' => 'Server di posta',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,30 @@ class UsersIndex extends Component
|
||||
if (! empty($this->availableRoles)) {
|
||||
$this->selectedRole = $this->availableRoles[0];
|
||||
}
|
||||
$this->avvisa('Utente creato con successo.');
|
||||
|
||||
// L'account funziona solo dopo la conferma dell'indirizzo.
|
||||
if ($user->inviaConfermaEmail()) {
|
||||
$this->avvisa("Utente creato. Gli è stata inviata un'email a {$user->email}: l'account funziona dopo che conferma l'indirizzo.");
|
||||
} else {
|
||||
$this->avvisaErrore("Utente creato, ma l'email di conferma non è partita: controlla il server di posta e usa \"Reinvia conferma\".");
|
||||
}
|
||||
}
|
||||
|
||||
public function reinviaConferma(int $userId): void
|
||||
{
|
||||
$user = User::query()->findOrFail($userId);
|
||||
|
||||
if ($user->hasVerifiedEmail()) {
|
||||
$this->avvisa("{$user->email} è già confermato.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->inviaConfermaEmail()) {
|
||||
$this->avvisa("Email di conferma inviata a {$user->email}.");
|
||||
} else {
|
||||
$this->avvisaErrore('L\'email non è partita: controlla il server di posta.');
|
||||
}
|
||||
}
|
||||
|
||||
public function startEdit(int $userId): void
|
||||
@@ -145,10 +168,24 @@ class UsersIndex extends Component
|
||||
$user->password = $validated['editPassword'];
|
||||
}
|
||||
|
||||
$emailCambiata = $user->isDirty('email');
|
||||
$user->save();
|
||||
$user->syncRoles([$validated['editSelectedRole']]);
|
||||
|
||||
$this->cancelEdit();
|
||||
|
||||
// Indirizzo nuovo: torna da confermare (User::booted) e gli si manda
|
||||
// il link, altrimenti il suo account smetterebbe di funzionare senza
|
||||
// che sappia perché.
|
||||
if ($emailCambiata) {
|
||||
$inviata = $user->inviaConfermaEmail();
|
||||
$inviata
|
||||
? $this->avvisa("Utente aggiornato. Il nuovo indirizzo va confermato: email inviata a {$user->email}.")
|
||||
: $this->avvisaErrore('Utente aggiornato, ma l\'email di conferma del nuovo indirizzo non è partita: controlla il server di posta.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->avvisa('Utente aggiornato con successo.');
|
||||
}
|
||||
|
||||
|
||||
@@ -475,6 +475,13 @@ class XmlExchange extends Component
|
||||
Tipologia::query()->delete();
|
||||
Zona::query()->delete();
|
||||
AnnoTeocratico::query()->delete();
|
||||
|
||||
// Il server di posta è configurazione di questa installazione, non
|
||||
// un dato da trasferire: non si esporta (la password è cifrata con
|
||||
// la sua APP_KEY) e un import non deve cancellarlo — senza posta
|
||||
// nessuno riceverebbe più le email di conferma.
|
||||
$posta = Setting::query()->first()?->only(Setting::CAMPI_POSTA) ?? [];
|
||||
|
||||
Setting::query()->delete();
|
||||
|
||||
// The mass delete above fires no model events, so Setting's
|
||||
@@ -502,6 +509,10 @@ class XmlExchange extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
if ($posta !== []) {
|
||||
Setting::instance()->update($posta);
|
||||
}
|
||||
|
||||
// Users are UPSERTED by email, never wiped. Deleting the users
|
||||
// table during a restore risks locking out the administrator
|
||||
// performing the import (e.g. a backup that predates their account,
|
||||
@@ -618,6 +629,9 @@ class XmlExchange extends Component
|
||||
'nome' => (string) $proclamatoreNode->nome,
|
||||
'cognome' => (string) $proclamatoreNode->cognome,
|
||||
'attivo' => ((int) ($proclamatoreNode->attivo ?? 1)) === 1,
|
||||
// Facoltativi, e assenti nei file di prima dell'ottobre 2026.
|
||||
'email' => trim((string) ($proclamatoreNode->email ?? '')) ?: null,
|
||||
'telefono' => trim((string) ($proclamatoreNode->telefono ?? '')) ?: null,
|
||||
]);
|
||||
$this->applyRowMeta('proclamatori', $p->id, $proclamatoreNode, true);
|
||||
$stats['proclamatori_importati']++;
|
||||
@@ -814,7 +828,7 @@ class XmlExchange extends Component
|
||||
])->all(),
|
||||
'zones' => $this->righeEsportate(Zona::query()->orderBy('id'), ['id', 'nome', 'attivo', 'created_at', 'updated_at']),
|
||||
'tipologie' => $this->righeEsportate(Tipologia::query()->orderBy('id'), ['id', 'nome', 'attivo', 'created_at', 'updated_at']),
|
||||
'proclamatori' => $this->righeEsportate(Proclamatore::query()->withTrashed()->orderBy('id'), ['id', 'nome', 'cognome', 'attivo', 'deleted_at', 'created_at', 'updated_at']),
|
||||
'proclamatori' => $this->righeEsportate(Proclamatore::query()->withTrashed()->orderBy('id'), ['id', 'nome', 'cognome', 'email', 'telefono', 'attivo', 'deleted_at', 'created_at', 'updated_at']),
|
||||
'territori' => $this->righeEsportate(Territorio::query()->withTrashed()->orderBy('id'), ['id', 'numero', 'zona_id', 'tipologia_id', 'confini', 'note', 'attivo', 'prioritario', 'pdf_path', 'thumbnail_path', 'deleted_at', 'created_at', 'updated_at']),
|
||||
'anni_teocratici' => $this->righeEsportate(AnnoTeocratico::query()->orderBy('id'), ['id', 'label', 'start_date', 'end_date', 'created_at', 'updated_at']),
|
||||
'campagne' => $this->righeEsportate(Campagna::query()->orderBy('id'), ['id', 'descrizione', 'start_date', 'end_date', 'created_at', 'updated_at']),
|
||||
@@ -1249,6 +1263,8 @@ class XmlExchange extends Component
|
||||
$this->addLegacyIdAttribute($node, $proclamatore);
|
||||
$this->addXmlText($node, 'nome', (string) ($proclamatore['nome'] ?? ''));
|
||||
$this->addXmlText($node, 'cognome', (string) ($proclamatore['cognome'] ?? ''));
|
||||
$this->addXmlText($node, 'email', (string) ($proclamatore['email'] ?? ''));
|
||||
$this->addXmlText($node, 'telefono', (string) ($proclamatore['telefono'] ?? ''));
|
||||
$node->addChild('attivo', (string) ((int) ($proclamatore['attivo'] ?? 1)));
|
||||
$node->addChild('deleted_at', (string) ($proclamatore['deleted_at'] ?? ''));
|
||||
$this->addTimestampNodes($node, $proclamatore);
|
||||
|
||||
Reference in New Issue
Block a user