add OAUth2.0

This commit is contained in:
2026-06-15 11:14:35 +02:00
parent 7973a94498
commit 69f4d6a602
32 changed files with 1126 additions and 51 deletions
@@ -7,7 +7,6 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AppSetting;
use App\Services\BackupService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -109,10 +108,4 @@ class BackupController extends Controller
->with('success', 'Configurazione backup automatico salvata.');
}
public function downloadMigrationScript(): JsonResponse
{
$script = view('admin.backup.migration-script')->render();
return response()->json(['script' => $script]);
}
}
@@ -4,14 +4,12 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Models\EmailSetting;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\ValidationException;
use Symfony\Component\Mime\Address;
class AuthController extends Controller
{
@@ -134,16 +134,6 @@ class DocumentoController extends Controller
));
}
public function create()
{
$this->authorizeWrite('documenti');
$individui = Individuo::orderBy('cognome')->orderBy('nome')->get();
$gruppi = Gruppo::orderBy('nome')->get();
$eventi = Evento::orderBy('nome_evento')->get();
$tipologie = TipologiaDocumento::attive();
return view('documenti.create', compact('individui', 'gruppi', 'eventi', 'tipologie'));
}
public function edit($documento)
{
$this->authorizeWrite('documenti');
+160 -1
View File
@@ -12,7 +12,6 @@ use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Documento;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@@ -402,6 +401,10 @@ class EmailController extends Controller
$this->ensureFoldersExist();
if ($settings->isOauth()) {
return $this->syncEmailsViaOAuth($settings);
}
$mailbox = $this->connectImap($settings);
\Illuminate\Support\Facades\Log::info('IMAP connection established', ['host' => $settings->imap_host]);
@@ -523,6 +526,10 @@ class EmailController extends Controller
$encryption
);
if ($settings->isOauth()) {
$dsn .= '&auth_mode=xoauth2';
}
try {
$transport = \Symfony\Component\Mailer\Transport::fromDsn($dsn);
$mailer = new \Symfony\Component\Mailer\Mailer($transport);
@@ -643,6 +650,10 @@ class EmailController extends Controller
throw new \Exception('Configurazione IMAP non valida');
}
if ($settings->isOauth()) {
return $this->connectOAuthImap($settings);
}
$encryption = match ($settings->imap_encryption) {
'ssl' => 'ssl',
'tls' => 'tls',
@@ -659,6 +670,27 @@ class EmailController extends Controller
]);
}
private function connectOAuthImap($settings): \Webklex\PHPIMAP\Client
{
$token = $settings->getOAuthAccessToken();
if (!$token) {
throw new \Exception('Impossibile ottenere il token OAuth per IMAP');
}
$client = new \Webklex\PHPIMAP\Client([
'host' => $settings->imap_host,
'port' => $settings->imap_port ?? 993,
'encryption' => $settings->imap_encryption ?? 'ssl',
'validate_cert' => false,
'username' => $settings->imap_username,
'password' => $token,
'authentication' => 'oauth',
]);
$client->connect();
return $client;
}
private function processImapMessage($imapMessage, $imapFolder)
{
$folderType = match ($imapFolder) {
@@ -717,6 +749,133 @@ class EmailController extends Controller
}
}
private function syncEmailsViaOAuth($settings): bool
{
$this->ensureFoldersExist();
$client = $this->connectOAuthImap($settings);
$folderMappings = [
'INBOX' => 'inbox',
'Sent' => 'sent',
'[Gmail]/Sent Mail' => 'sent',
'Sent Mail' => 'sent',
'Drafts' => 'drafts',
'[Gmail]/Drafts' => 'drafts',
'Trash' => 'trash',
'[Gmail]/Trash' => 'trash',
];
$totalNew = 0;
foreach (array_keys($folderMappings) as $imapFolderName) {
try {
$folder = $client->getFolder($imapFolderName);
if (!$folder) {
continue;
}
$messages = $folder->messages()
->setFetchFlags(true)
->setFetchBody(true)
->limit(100)
->get();
foreach ($messages as $imapMessage) {
$messageId = $imapMessage->getMessageId() ?? uniqid('msg_');
$existing = EmailMessage::where('message_id', $messageId)->first();
if ($existing) {
continue;
}
$this->processWebklexMessage($imapMessage, $imapFolderName);
$totalNew++;
}
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Error processing OAuth IMAP folder', ['folder' => $imapFolderName, 'error' => $e->getMessage()]);
continue;
}
}
$client->disconnect();
\Illuminate\Support\Facades\Log::info('OAuth email sync completed', ['new' => $totalNew]);
return true;
}
private function processWebklexMessage($imapMessage, $imapFolder)
{
$folderType = match ($imapFolder) {
'INBOX', 'inbox' => 'inbox',
'Sent', 'sent', '[Gmail]/Sent Mail' => 'sent',
'Drafts', 'drafts', '[Gmail]/Drafts' => 'drafts',
'Trash', 'trash', '[Gmail]/Trash' => 'trash',
'Archive', 'archive', '[Gmail]/All Mail' => 'archive',
'Starred', 'starred', '[Gmail]/Starred' => 'starred',
default => 'inbox',
};
$folder = EmailFolder::where('type', $folderType)->first();
if (!$folder) return;
$messageId = $imapMessage->getMessageId() ?? uniqid('msg_');
$existing = EmailMessage::where('message_id', $messageId)->first();
if ($existing) return;
$from = $imapMessage->getFrom();
$to = $imapMessage->getTo();
$fromName = '';
$fromEmail = '';
if ($from && $from->first()) {
$fromName = $from->first()->personal ?? '';
$fromEmail = ($from->first()->mailbox ?? '') . '@' . ($from->first()->host ?? '');
}
$toEmail = '';
if ($to && $to->first()) {
$toEmail = ($to->first()->mailbox ?? '') . '@' . ($to->first()->host ?? '');
}
$message = EmailMessage::create([
'email_folder_id' => $folder->id,
'message_id' => $messageId,
'subject' => mb_substr($imapMessage->getSubject() ?? '(senza oggetto)', 0, 255),
'from_name' => mb_substr($fromName, 0, 255),
'from_email' => mb_substr($fromEmail, 0, 255),
'to_email' => mb_substr($toEmail, 0, 255),
'body_text' => $imapMessage->getTextBody() ?? '',
'body_html' => $imapMessage->getHTMLBody() ?? '',
'is_read' => !($imapMessage->getFlags()['UNSEEN'] ?? false),
'is_starred' => !empty($imapMessage->getFlags()['FLAGGED']),
'is_sent' => $folderType === 'sent',
'received_at' => $imapMessage->getDate() ?? now(),
'imap_uid' => $imapMessage->getUid() ?? null,
]);
$attachments = $imapMessage->getAttachments();
if ($attachments && count($attachments) > 0) {
foreach ($attachments as $attachment) {
try {
$filename = $attachment->getName() ?? 'attachment';
$contents = $attachment->getContent();
$path = 'email-attachments/' . \Illuminate\Support\Str::uuid() . '_' . $filename;
\Illuminate\Support\Facades\Storage::put($path, $contents);
EmailAttachment::create([
'email_message_id' => $message->id,
'filename' => $filename,
'mime_type' => $attachment->getMimeType() ?? 'application/octet-stream',
'size' => strlen($contents),
'file_path' => $path,
]);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Error saving OAuth attachment', ['error' => $e->getMessage()]);
}
}
}
}
private function ensureFoldersExist()
{
$systemFolders = EmailFolder::getSystemFolders();
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\GoogleService;
use App\Services\GoogleOAuthService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class GoogleOAuthController extends Controller
{
public function __construct(
private readonly GoogleOAuthService $oauthService,
) {}
public function connect(GoogleService $service): RedirectResponse
{
$url = $this->oauthService->getAuthorizationUrl($service);
return redirect()->away($url);
}
public function callback(Request $request): RedirectResponse
{
$request->validate([
'code' => 'required|string',
'state' => 'required|string|in:' . implode(',', array_column(GoogleService::cases(), 'value')),
]);
$service = GoogleService::from($request->state);
try {
$this->oauthService->handleCallback($request->code, $service);
return redirect()->route('impostazioni.index', ['#google-services'])
->with('success', "Google {$service->label()} connected successfully.");
} catch (\Exception $e) {
return redirect()->route('impostazioni.index', ['#google-services'])
->with('error', 'Google OAuth error: ' . $e->getMessage());
}
}
public function revoke(GoogleService $service): RedirectResponse
{
try {
$this->oauthService->revoke($service);
return redirect()->route('impostazioni.index', ['#google-services'])
->with('success', "Google {$service->label()} revoked successfully.");
} catch (\Exception $e) {
return redirect()->route('impostazioni.index', ['#google-services'])
->with('error', 'Revocation error: ' . $e->getMessage());
}
}
public function status(): View
{
$services = [];
foreach (GoogleService::cases() as $service) {
$services[] = [
'service' => $service,
'authorized' => $this->oauthService->isAuthorized($service),
];
}
return view('google-oauth.status', ['services' => $services]);
}
}
@@ -3,8 +3,6 @@
namespace App\Http\Controllers;
use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\Ruolo;
use Illuminate\Http\Request;
class GruppoMembroController extends Controller
-1
View File
@@ -10,7 +10,6 @@ use App\Models\Gruppo;
use App\Models\Individuo;
use App\Models\EmailMessage;
use App\Models\MailingList;
use App\Models\Notifica;
use App\Services\BackupService;
use Illuminate\Support\Facades\Auth;
@@ -14,7 +14,6 @@ use App\Models\TipologiaDocumento;
use App\Models\TipologiaEvento;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
@@ -3,7 +3,6 @@
namespace App\Http\Controllers;
use App\Models\Individuo;
use App\Models\Contatto;
use App\Models\Documento;
use App\Models\Gruppo;
use App\Models\Comune;
@@ -10,7 +10,6 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
class StorageRepositoryController extends Controller
@@ -19,20 +18,7 @@ class StorageRepositoryController extends Controller
private readonly StorageRepositoryService $repoService
) {}
public function index(Request $request)
{
$this->authorizeWrite('settings');
$repositories = StorageRepository::orderBy('ordine')->get();
if ($request->ajax()) {
return response()->json($repositories);
}
return view('storage-repositories.index', compact('repositories'));
}
public function store(Request $request): RedirectResponse
public function store(Request $request): RedirectResponse
{
$this->authorizeWrite('settings');