pre version 2

This commit is contained in:
2026-06-17 13:50:41 +02:00
parent c333bbce4e
commit 6909e21b67
26 changed files with 1213 additions and 136 deletions
+48
View File
@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace App\Enums;
enum GoogleService: string
{
case Email = 'email';
case Drive = 'drive';
case Calendar = 'calendar';
public function scopes(): array
{
return match ($this) {
self::Email => ['https://mail.google.com/'],
self::Drive => ['https://www.googleapis.com/auth/drive'],
self::Calendar => ['https://www.googleapis.com/auth/calendar'],
};
}
public function label(): string
{
return match ($this) {
self::Email => 'Gmail',
self::Drive => 'Google Drive',
self::Calendar => 'Google Calendar',
};
}
public function icon(): string
{
return match ($this) {
self::Email => 'fas fa-envelope',
self::Drive => 'fab fa-google-drive',
self::Calendar => 'fas fa-calendar-alt',
};
}
public function description(): string
{
return match ($this) {
self::Email => 'Invia e ricevi email tramite Gmail API (alternativa a SMTP/IMAP con password)',
self::Drive => 'Accedi ai file su Google Drive per documenti e allegati',
self::Calendar => 'Sincronizza eventi con Google Calendar',
};
}
}
@@ -2,11 +2,13 @@
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\SenderAccount;
use App\Services\GoogleOAuthService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
@@ -19,7 +21,8 @@ class EmailSettingsController extends Controller
$this->authorizeWrite('settings');
$settings = EmailSetting::first() ?? new EmailSetting();
$senderAccounts = SenderAccount::orderBy('email_address')->get();
return view('admin.email-settings.index', compact('settings', 'senderAccounts'));
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
return view('admin.email-settings.index', compact('settings', 'senderAccounts', 'googleStatus'));
}
public function save(Request $request)
@@ -43,6 +46,8 @@ class EmailSettingsController extends Controller
'is_active' => 'boolean',
'signature' => 'nullable|string',
'signature_enabled' => 'boolean',
'auth_method' => 'nullable|in:password,oauth',
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
]);
if (!empty($validated['imap_password'])) {
@@ -61,29 +66,17 @@ class EmailSettingsController extends Controller
$validated
);
// Verifica che i dati siano stati salvati
$verify = EmailSetting::find($saved->id);
if ($verify &&
$allGood = $verify &&
$verify->imap_host === $validated['imap_host'] &&
$verify->email_address === $validated['email_address']) {
$verify->email_address === $validated['email_address'];
$signatureMatch = !array_key_exists('signature', $validated) || $verify->signature === $validated['signature'];
$signatureEnabledMatch = !array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled'];
if (!$signatureMatch) {
\Illuminate\Support\Facades\Log::warning('Signature mismatch after save', [
'expected' => $validated['signature'] ?? null,
'actual' => $verify->signature,
]);
}
if (!$signatureEnabledMatch) {
\Illuminate\Support\Facades\Log::warning('Signature enabled mismatch after save', [
'expected' => $validated['signature_enabled'] ?? null,
'actual' => $verify->signature_enabled,
]);
}
$allGood = $allGood &&
(!array_key_exists('signature', $validated) || $verify->signature === $validated['signature']) &&
(!array_key_exists('signature_enabled', $validated) || (bool)$verify->signature_enabled === (bool)$validated['signature_enabled']);
if ($allGood) {
return back()->with('success', 'Impostazioni email salvate e verificate nel database.');
} else {
return back()->with('error', 'Errore nel salvataggio: i dati non sono stati salvati correttamente.');
@@ -99,7 +92,6 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
}
// Verifica che i dati siano salvati
if (empty($settings->imap_host) || empty($settings->email_address)) {
return response()->json(['success' => false, 'message' => 'Prima salva le impostazioni.']);
}
@@ -129,41 +121,13 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Configurazione non trovata.']);
}
if (empty($settings->smtp_host)) {
$settings->smtp_host = 'smtp.gmail.com';
$settings->smtp_port = 587;
$settings->smtp_encryption = 'tls';
}
try {
$smtpUsername = $settings->smtp_username;
$smtpPassword = $settings->smtp_password;
if (empty($smtpUsername)) {
$smtpUsername = $settings->email_address;
}
if (empty($smtpPassword)) {
$smtpPassword = $settings->getDecryptedPassword();
} else {
$smtpPassword = Crypt::decryptString($smtpPassword);
$mailer = $settings->getSmtpMailer();
if ($mailer === null) {
return response()->json(['success' => false, 'message' => 'Impossibile creare il mailer SMTP. Verifica la configurazione.']);
}
$encryption = $settings->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
$dsn = sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($smtpUsername),
rawurlencode($smtpPassword),
$settings->smtp_host,
$settings->smtp_port ?? 587,
$encryption
);
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
$fromName = $settings->email_name ?? '';
if (filter_var($fromName, FILTER_VALIDATE_EMAIL)) {
$fromName = 'Glastree';
@@ -182,7 +146,7 @@ class EmailSettingsController extends Controller
->to($request->email)
->subject('Test Configurazione SMTP - Glastree')
->text('Test email da Glastree - Configurazione SMTP funziona!');
$mailer->send($email);
\Illuminate\Support\Facades\Log::info('Test SMTP ok', ['to' => $request->email, 'from' => $settings->email_address]);
@@ -227,6 +191,8 @@ class EmailSettingsController extends Controller
'verify_email' => 'nullable|email|max:255',
'note' => 'nullable|string|max:500',
'is_active' => 'boolean',
'auth_method' => 'nullable|in:password,oauth',
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
]);
if (!empty($data['smtp_password'])) {
@@ -257,6 +223,8 @@ class EmailSettingsController extends Controller
'verify_email' => 'nullable|email|max:255',
'note' => 'nullable|string|max:500',
'is_active' => 'boolean',
'auth_method' => 'nullable|in:password,oauth',
'google_oauth_connection_id' => 'nullable|integer|exists:google_oauth_connections,id',
]);
if (!empty($data['smtp_password'])) {
@@ -326,4 +294,4 @@ class EmailSettingsController extends Controller
return response()->json(['success' => false, 'message' => 'Errore invio: ' . $e->getMessage()]);
}
}
}
}
+21 -9
View File
@@ -56,7 +56,7 @@ class DocumentoController extends Controller
} elseif ($folderId) {
$query->where('cartella_id', $folderId);
} else {
$query->whereNull('repository_id');
$query->whereNull('repository_id')->whereNull('cartella_id');
}
$documenti = $query->paginate(20);
@@ -204,24 +204,24 @@ class DocumentoController extends Controller
$data = $request->validate([
'nome_file' => 'required|string|max:255',
'tipologia' => 'required|' . $tipologieRule,
'visibilita' => 'nullable|string',
'contesto_tipo' => 'nullable|in:individuo,gruppo,evento,mailing',
'visibilita_target_id' => 'nullable|integer',
'visibilita_target_type' => 'nullable|string',
'note' => 'nullable|string',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
]);
$data['visibilita'] = $request->contesto_tipo ?: 'pubblico';
$contestoTipo = $data['contesto_tipo'] ?? null;
$data['visibilita'] = $contestoTipo ?: 'pubblico';
if ($request->contesto_tipo) {
if ($contestoTipo) {
$typeMap = [
'individuo' => Individuo::class,
'gruppo' => Gruppo::class,
'evento' => Evento::class,
'mailing' => \App\Models\MailingList::class,
];
$data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? null;
$data['visibilita_target_type'] = $typeMap[$contestoTipo] ?? null;
} else {
$data['visibilita'] = 'pubblico';
$data['visibilita_target_id'] = null;
@@ -236,21 +236,27 @@ class DocumentoController extends Controller
$documento->tags()->detach();
}
return redirect('/documenti')->with('success', 'Documento aggiornato.');
$redirect = $request->input('folder_id') ? '/documenti?folder_id=' . $request->input('folder_id') : '/documenti';
return redirect($redirect)->with('success', 'Documento aggiornato.');
}
public function store(Request $request)
{
$this->authorizeWrite('documenti');
$tipologieValidi = TipologiaDocumento::opzioni();
$tipologieRule = 'in:' . implode(',', $tipologieValidi);
$data = $request->validate([
'nome_file' => 'required|string|max:255',
'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro',
'tipologia' => 'required|' . $tipologieRule,
'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',
]);
$repositoryId = $data['repository_id'] ?? null;
@@ -284,7 +290,7 @@ class DocumentoController extends Controller
$visibilitaTargetType = Gruppo::class;
}
Documento::create([
$doc = Documento::create([
'nome_file' => $data['nome_file'],
'file_path' => $filePath,
'storage_disk' => $disk ?? null,
@@ -300,7 +306,13 @@ class DocumentoController extends Controller
'repository_id' => $repositoryId,
]);
if ($doc && $request->has('tags')) {
$doc->tags()->sync($request->tags);
}
$redirect = $request->_redirect ?? url()->previous();
if ($request->filled('folder_id')) {
$redirect = '/documenti?folder_id=' . $request->input('folder_id');
}
return redirect($redirect)->with('success', 'Documento caricato.');
}
+37
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Evento;
@@ -630,4 +632,39 @@ class EventoController extends Controller
return redirect('/eventi')->with('success', $message);
}
public function massTag(Request $request)
{
$this->authorizeWrite('eventi');
$idsInput = $request->input('ids', '');
$ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []);
if (empty($ids)) {
return back()->with('error', 'Nessun evento selezionato.');
}
$data = $request->validate([
'tags' => 'required|array',
'tags.*' => 'exists:tags,id',
'mode' => 'required|in:assign,remove',
]);
$tagIds = $data['tags'];
$mode = $data['mode'];
$count = 0;
Evento::whereIn('id', $ids)->chunk(100, function ($eventi) use ($tagIds, $mode, &$count) {
foreach ($eventi as $evento) {
if ($mode === 'assign') {
$evento->tags()->syncWithoutDetaching($tagIds);
} else {
$evento->tags()->detach($tagIds);
}
$count++;
}
});
$actionLabel = $mode === 'assign' ? 'assegnati' : 'rimossi';
return back()->with('success', "Tag $actionLabel per $count eventi.");
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Evento;
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\GoogleService;
use App\Models\GoogleOAuthConnection;
use App\Services\GoogleOAuthService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class GoogleOAuthController extends Controller
{
public function __construct(
private readonly GoogleOAuthService $googleOAuthService,
) {}
public function redirect(string $service): RedirectResponse
{
try {
$serviceEnum = GoogleService::from($service);
} catch (\ValueError $e) {
return redirect()->back()->withErrors(['error' => 'Servizio Google non valido: ' . $service]);
}
$authUrl = $this->googleOAuthService->getAuthUrl($service);
return redirect()->away($authUrl);
}
public function callback(Request $request): RedirectResponse
{
$error = $request->input('error');
if ($error) {
$message = match ($error) {
'access_denied' => 'Accesso negato. Autorizzazione non concessa.',
default => 'Errore Google: ' . $error,
};
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => $message]);
}
$code = $request->input('code');
if (!$code) {
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => 'Nessun codice di autorizzazione ricevuto']);
}
$state = $request->input('state', 'email');
try {
$service = GoogleService::from($state);
} catch (\ValueError $e) {
$service = GoogleService::Email;
}
try {
$connection = $this->googleOAuthService->handleCallback($code, $service);
$message = match ($service) {
GoogleService::Email => 'Account email Gmail connesso con successo!',
GoogleService::Drive => 'Account Google Drive connesso con successo!',
GoogleService::Calendar => 'Account Google Calendar connesso con successo!',
};
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->with('success', $message);
} catch (\Exception $e) {
Log::error('Google OAuth callback error', [
'error' => $e->getMessage(),
'service' => $service->value,
]);
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => 'Errore durante la connessione: ' . $e->getMessage()]);
}
}
public function revoke(GoogleOAuthConnection $connection): RedirectResponse
{
if ($connection->user_id !== auth()->id()) {
abort(403);
}
try {
$service = GoogleService::from($connection->service);
$message = match ($service) {
GoogleService::Email => 'Account email Gmail disconnesso.',
GoogleService::Drive => 'Account Google Drive disconnesso.',
GoogleService::Calendar => 'Account Google Calendar disconnesso.',
};
$this->googleOAuthService->revoke($connection);
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->with('success', $message);
} catch (\Exception $e) {
Log::error('Google OAuth revoke error', [
'error' => $e->getMessage(),
'connection_id' => $connection->id,
]);
return redirect()->route('impostazioni.index', ['tab' => 'google'])
->withErrors(['error' => 'Errore durante la disconnessione: ' . $e->getMessage()]);
}
}
public function status(): \Illuminate\Http\JsonResponse
{
$status = $this->googleOAuthService->getConnectionStatus();
return response()->json($status);
}
}
@@ -10,6 +10,7 @@ use App\Models\Ruolo;
use App\Models\SenderAccount;
use App\Models\StorageRepository;
use App\Models\Tag;
use App\Services\GoogleOAuthService;
use App\Models\TipologiaDocumento;
use App\Models\TipologiaEvento;
use Illuminate\Http\RedirectResponse;
@@ -34,8 +35,9 @@ class ImpostazioniController extends Controller
$repositories = StorageRepository::orderBy('ordine')->get();
$googleDriveNewToken = session('google_drive_new_token');
$calendarioConnessioni = CalendarioConnessione::orderBy('ordine')->get();
$googleStatus = app(GoogleOAuthService::class)->getConnectionStatus();
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni'));
return view('impostazioni.index', compact('tipologie', 'tipologieEventi', 'ruoli', 'tags', 'appSettings', 'emailSettings', 'senderAccounts', 'repositories', 'googleDriveNewToken', 'calendarioConnessioni', 'googleStatus'));
}
public function saveAppSettings(Request $request)
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Traits\HasTagsLight;
+120 -7
View File
@@ -2,7 +2,9 @@
namespace App\Models;
use App\Services\GoogleOAuthService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\Crypt;
@@ -14,13 +16,15 @@ class EmailSetting extends Model
'imap_host', 'imap_port', 'imap_encryption', 'imap_username', 'imap_password',
'smtp_host', 'smtp_port', 'smtp_encryption', 'smtp_username', 'smtp_password',
'email_address', 'email_name', 'reply_to', 'sync_interval_minutes',
'last_sync_at', 'is_active', 'signature', 'signature_enabled'
'last_sync_at', 'is_active', 'signature', 'signature_enabled',
'auth_method', 'google_oauth_connection_id',
];
protected $casts = [
'is_active' => 'boolean',
'signature_enabled' => 'boolean',
'last_sync_at' => 'datetime',
'google_oauth_connection_id' => 'integer',
];
public function folders(): HasMany
@@ -28,8 +32,25 @@ class EmailSetting extends Model
return $this->hasMany(EmailFolder::class)->orderBy('sort_order');
}
public function googleOAuthConnection(): BelongsTo
{
return $this->belongsTo(GoogleOAuthConnection::class);
}
public function getImapConfig(): array
{
if ($this->auth_method === 'oauth') {
$token = $this->resolveOAuthToken();
return [
'host' => $this->imap_host,
'port' => $this->imap_port,
'encryption' => $this->imap_encryption,
'username' => $this->imap_username,
'authentication' => 'oauth',
'password' => $token,
];
}
return [
'host' => $this->imap_host,
'port' => $this->imap_port,
@@ -41,6 +62,10 @@ class EmailSetting extends Model
public function getDecryptedPassword(): string
{
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->imap_password)) {
return '';
}
@@ -53,6 +78,10 @@ class EmailSetting extends Model
public function getDecryptedSmtpPassword(): string
{
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->smtp_password)) {
return $this->getDecryptedPassword();
}
@@ -63,6 +92,22 @@ class EmailSetting extends Model
}
}
protected function resolveOAuthToken(): string
{
if (!$this->relationLoaded('googleOAuthConnection')) {
$this->load('googleOAuthConnection');
}
$connection = $this->googleOAuthConnection;
if (!$connection) {
return '';
}
try {
return app(GoogleOAuthService::class)->getAccessToken($connection);
} catch (\Exception $e) {
return '';
}
}
public function getSmtpConfig(): array
{
return [
@@ -87,33 +132,101 @@ class EmailSetting extends Model
default => 'none',
};
return new \DirectoryTree\ImapEngine\Mailbox([
$config = [
'host' => $this->imap_host,
'port' => $this->imap_port ?? 993,
'encryption' => $encryption,
'validate_cert' => false,
'username' => $this->imap_username,
'password' => $this->getDecryptedPassword(),
]);
];
if ($this->auth_method === 'oauth') {
$config['authentication'] = 'oauth';
}
return new \DirectoryTree\ImapEngine\Mailbox($config);
} catch (\Exception $e) {
return null;
}
}
public function getSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
{
if ($this->auth_method === 'oauth') {
return $this->buildOAuthSmtpMailer();
}
$dsn = $this->buildSmtpDsn();
if (!$dsn) {
return null;
}
try {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
return new \Symfony\Component\Mailer\Mailer($transport);
} catch (\Exception $e) {
return null;
}
}
protected function buildOAuthSmtpMailer(): ?\Symfony\Component\Mailer\Mailer
{
$token = $this->resolveOAuthToken();
if (empty($token)) {
return null;
}
$username = $this->smtp_username ?? $this->email_address;
$host = $this->smtp_host ?? 'smtp.gmail.com';
$port = $this->smtp_port ?? 587;
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
$transport->setUsername($username);
$transport->setPassword($token);
$transport->setAuthenticators([
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
]);
return new \Symfony\Component\Mailer\Mailer($transport);
}
public function buildSmtpDsn(): ?string
{
$password = $this->getDecryptedSmtpPassword();
if (empty($password)) {
return null;
}
$username = $this->smtp_username ?? $this->email_address;
$encryption = $this->smtp_encryption ?? 'tls';
$scheme = ($encryption === 'ssl') ? 'smtps' : 'smtp';
return sprintf(
'%s://%s:%s@%s:%d?encryption=%s',
$scheme,
rawurlencode($username),
rawurlencode($password),
$this->smtp_host ?? 'smtp.gmail.com',
$this->smtp_port ?? 587,
$encryption
);
}
public function testConnection(): array
{
try {
$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) {
@@ -134,4 +247,4 @@ class EmailSetting extends Model
{
return $this->signature;
}
}
}
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Traits\HasTagsLight;
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Models;
use App\Services\GoogleOAuthService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class GoogleOAuthConnection extends Model
{
protected $table = 'google_oauth_connections';
protected $fillable = [
'user_id',
'service',
'email',
'access_token',
'refresh_token',
'expires_at',
];
protected $casts = [
'expires_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function isExpired(): bool
{
return $this->expires_at && $this->expires_at->isPast();
}
public function refresh(): void
{
if (!$this->refresh_token) {
throw new \RuntimeException('Nessun refresh token disponibile per ' . $this->email);
}
app(GoogleOAuthService::class)->refreshToken($this);
}
public function getValidAccessToken(): string
{
if ($this->isExpired() && $this->refresh_token) {
$this->refresh();
}
return $this->access_token;
}
}
+54 -2
View File
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace App\Models;
use App\Services\GoogleOAuthService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Crypt;
class SenderAccount extends Model
@@ -24,11 +26,14 @@ class SenderAccount extends Model
'verify_email',
'note',
'is_active',
'auth_method',
'google_oauth_connection_id',
];
protected $casts = [
'is_active' => 'boolean',
'smtp_port' => 'integer',
'google_oauth_connection_id' => 'integer',
];
public function scopeActive(Builder $query): Builder
@@ -36,8 +41,33 @@ class SenderAccount extends Model
return $query->where('is_active', true);
}
public function googleOAuthConnection(): BelongsTo
{
return $this->belongsTo(GoogleOAuthConnection::class);
}
protected function resolveOAuthToken(): string
{
if (!$this->relationLoaded('googleOAuthConnection')) {
$this->load('googleOAuthConnection');
}
$connection = $this->googleOAuthConnection;
if (!$connection) {
return '';
}
try {
return app(GoogleOAuthService::class)->getAccessToken($connection);
} catch (\Exception $e) {
return '';
}
}
public function getDecryptedPassword(): string
{
if ($this->auth_method === 'oauth') {
return $this->resolveOAuthToken();
}
if (empty($this->smtp_password)) {
return '';
}
@@ -121,11 +151,33 @@ class SenderAccount extends Model
$email->attachFromPath($path);
}
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
if ($this->auth_method === 'oauth') {
$mailer = $this->buildOAuthMailer();
} else {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($this->buildDsn());
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
}
$mailer->send($email);
}
protected function buildOAuthMailer(): \Symfony\Component\Mailer\Mailer
{
$token = $this->resolveOAuthToken();
$username = $this->smtp_username ?? $this->email_address;
$host = $this->smtp_host ?? 'smtp.gmail.com';
$port = $this->smtp_port ?? 587;
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport($host, $port, true);
$transport->setUsername($username);
$transport->setPassword($token);
$transport->setAuthenticators([
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
]);
return new \Symfony\Component\Mailer\Mailer($transport);
}
public function sendReport(string $subject, string $body): bool
{
if (empty($this->verify_email)) {
+2
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
+178
View File
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\GoogleOAuthConnection;
use App\Enums\GoogleService;
use Google_Client;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class GoogleOAuthService
{
private const TOKEN_CACHE_TTL = 300;
private const MAX_RETRIES = 2;
public function buildClient(): Google_Client
{
$client = new Google_Client();
$client->setClientId(config('services.google.client_id'));
$client->setClientSecret(config('services.google.client_secret'));
$client->setRedirectUri(route('google-oauth.callback'));
$client->setAccessType('offline');
$client->setPrompt('consent');
$client->setIncludeGrantedScopes(true);
return $client;
}
public function getAuthUrl(string $service): string
{
$serviceEnum = GoogleService::from($service);
$client = $this->buildClient();
$client->setScopes($serviceEnum->scopes());
$client->setState($service);
return $client->createAuthUrl();
}
public function handleCallback(string $authorizationCode, GoogleService $service): GoogleOAuthConnection
{
$client = $this->buildClient();
$client->setScopes($service->scopes());
$client->fetchAccessTokenWithAuthCode($authorizationCode);
$tokenData = $client->getAccessToken();
if (isset($tokenData['error'])) {
throw new \RuntimeException('Google OAuth error: ' . ($tokenData['error_description'] ?? $tokenData['error']));
}
$token = $tokenData['access_token'];
$refreshToken = $tokenData['refresh_token'] ?? null;
$expiresAt = now()->addSeconds($tokenData['expires_in']);
$email = $this->getEmailFromToken($token);
$connection = GoogleOAuthConnection::updateOrCreate([
'user_id' => auth()->id(),
'service' => $service->value,
'email' => $email,
], [
'access_token' => $token,
'refresh_token' => $refreshToken,
'expires_at' => $expiresAt,
]);
return $connection;
}
public function getAccessToken(GoogleOAuthConnection $connection): string
{
$cacheKey = "google_oauth_token_{$connection->id}";
return Cache::remember($cacheKey, self::TOKEN_CACHE_TTL, function () use ($connection) {
if ($connection->isExpired()) {
$this->refreshToken($connection);
}
return $connection->access_token;
});
}
public function refreshToken(GoogleOAuthConnection $connection): void
{
if (!$connection->refresh_token) {
throw new \RuntimeException('Impossibile rinnovare: nessun refresh token');
}
$client = $this->buildClient();
$client->fetchAccessTokenWithRefreshToken($connection->refresh_token);
$tokenData = $client->getAccessToken();
if (isset($tokenData['error'])) {
$connection->delete();
Cache::forget("google_oauth_token_{$connection->id}");
throw new \RuntimeException(
'Refresh token scaduto o revocato per ' . $connection->email
);
}
$connection->access_token = $tokenData['access_token'];
$connection->expires_at = now()->addSeconds($tokenData['expires_in']);
if (isset($tokenData['refresh_token'])) {
$connection->refresh_token = $tokenData['refresh_token'];
}
$connection->save();
Cache::forget("google_oauth_token_{$connection->id}");
}
public function revoke(GoogleOAuthConnection $connection): void
{
$client = $this->buildClient();
$client->revokeToken($connection->access_token);
$connection->delete();
Cache::forget("google_oauth_token_{$connection->id}");
}
public function createXoAuth2SmtpTransport(
string $email,
string $accessToken
): \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport {
$transport = new \Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport(
'smtp.gmail.com',
587,
true,
);
$transport->setUsername($email);
$transport->setPassword($accessToken);
$transport->setAuthenticators([
new \Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator(),
]);
return $transport;
}
private function getEmailFromToken(string $accessToken): string
{
$client = $this->buildClient();
$payload = $client->verifyIdToken($accessToken);
if ($payload && isset($payload['email'])) {
return $payload['email'];
}
$http = new \GuzzleHttp\Client();
$response = $http->get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', [
'headers' => ['Authorization' => "Bearer $accessToken"],
]);
$data = json_decode((string) $response->getBody(), true);
return $data['email'];
}
public function getConnectionStatus(): array
{
$connections = GoogleOAuthConnection::where('user_id', auth()->id())->get();
$status = [];
foreach (GoogleService::cases() as $service) {
$connection = $connections->firstWhere('service', $service->value);
$status[$service->value] = [
'connected' => $connection !== null,
'connection' => $connection,
'email' => $connection?->email,
'expired' => $connection?->isExpired() ?? false,
];
}
return $status;
}
}