Revert OAuth2.0
This commit is contained in:
@@ -2,11 +2,15 @@ APP_NAME=Glastree
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:aWz7908H9c7s+it9uMTwb6pyUrpddyMclcuN9Kzv7Ao=
|
||||
APP_DEBUG=true
|
||||
APP_URL=
|
||||
APP_URL=https://glastree.soon.it
|
||||
APP_PROTOCOL=https
|
||||
FORCE_HTTPS=true
|
||||
TRUSTED_PROXIES=*
|
||||
|
||||
GOOGLE_CLIENT_ID=980774223097-o5h7a6kepvg69te34fof2otn7ibi9uha.apps.googleusercontent.com
|
||||
GOOGLE_CLIENT_SECRET=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
|
||||
|
||||
|
||||
APP_LOCALE=it
|
||||
APP_FALLBACK_LOCALE=it
|
||||
APP_FAKER_LOCALE=it_IT
|
||||
|
||||
@@ -46,6 +46,3 @@ MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
# 🚀 Deploy & Aggiornamento — Glastree
|
||||
|
||||
## Generare il Pacchetto (Server di Sviluppo)
|
||||
|
||||
```bash
|
||||
cd /var/www/html/glastree
|
||||
|
||||
# Installa dipendenze PHP production (esclude dev)
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Build asset Vite (se node_modules/ presente)
|
||||
npm install && npm run build
|
||||
|
||||
# Genera archivio
|
||||
bash build-dist.sh
|
||||
```
|
||||
|
||||
L'archivio `glastree-YYYYMMDD_HHMM.tar.gz` viene creato nella directory corrente.
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Installazione Fresca (Nuovo Server)
|
||||
|
||||
```bash
|
||||
# 1. Estrai archivio
|
||||
tar xzf glastree-YYYYMMDD_HHMM.tar.gz
|
||||
cd glastree
|
||||
|
||||
# 2. Crea directory necessarie
|
||||
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
|
||||
|
||||
# 3. Permessi
|
||||
chmod -R 775 storage bootstrap/cache
|
||||
chown -R www-data:www-data storage bootstrap/cache
|
||||
|
||||
# 4. Crea .gitignore in storage/app/public (serve a Laravel)
|
||||
echo "*" > storage/app/public/.gitignore
|
||||
echo "!.gitignore" >> storage/app/public/.gitignore
|
||||
|
||||
# 5. Configura ambiente
|
||||
cp .env.example .env
|
||||
# Modifica manualmente: DB, APP_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
|
||||
php artisan key:generate
|
||||
|
||||
# 6. Pulisci cache e ricrea symlink
|
||||
rm -f public/storage
|
||||
ln -sf ../storage/app/public public/storage
|
||||
php artisan config:clear
|
||||
php artisan route:clear
|
||||
php artisan view:clear
|
||||
|
||||
# 7. Avvia installazione DB
|
||||
php install.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Aggiornamento (Server Esistente)
|
||||
|
||||
```bash
|
||||
# 1. BACKUP
|
||||
cp .env .env.backup.$(date +%Y%m%d_%H%M%S)
|
||||
mysqldump -u USER -p DBNAME > backup_$(date +%Y%m%d_%H%M%S).sql
|
||||
|
||||
# 2. Estrai archivio
|
||||
tar xzf glastree-YYYYMMDD_HHMM.tar.gz
|
||||
|
||||
# 3. Ripristina .env
|
||||
cp .env.backup.* .env
|
||||
|
||||
# 4. Aggiungi nuove variabili d'ambiente
|
||||
echo "GOOGLE_CLIENT_ID=" >> .env
|
||||
echo "GOOGLE_CLIENT_SECRET=" >> .env
|
||||
|
||||
# 5. Permessi
|
||||
chmod -R 775 storage bootstrap/cache
|
||||
chown -R www-data:www-data storage bootstrap/cache
|
||||
|
||||
# 6. Symlink storage
|
||||
rm -f public/storage
|
||||
ln -sf ../storage/app/public public/storage
|
||||
|
||||
# 7. Migration
|
||||
php artisan migrate --force
|
||||
|
||||
# 8. Pulisci cache
|
||||
php artisan config:clear
|
||||
php artisan route:clear
|
||||
php artisan view:clear
|
||||
|
||||
# 9. Asset Vite
|
||||
npm install && npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Contenuto dell'Archivio
|
||||
|
||||
| Include | Esclude |
|
||||
|---------|---------|
|
||||
| Sorgenti PHP (`app/`, `config/`, `routes/`, `resources/`) | `.git/`, `node_modules/`, `tests/` |
|
||||
| Vendor production | `.env`, `.env.example` |
|
||||
| Asset Vite compilati (`public/build/`) | `storage/app/public/`, `storage/app/private/` |
|
||||
| Migration e seeder | `storage/framework/cache/*`, `sessions/*`, `views/*` |
|
||||
| | `storage/logs/*`, `storage/debugbar/*` |
|
||||
| | `storage/app/backups/`, `storage/app/documenti/` |
|
||||
| | `MEMORY.md`, `AGENTS.md`, `build-dist.sh`, Docker files |
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Variabili d'Ambiente Richieste
|
||||
|
||||
```env
|
||||
APP_URL=https://tuodominio.it
|
||||
APP_SUBFOLDER=/app
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=glastree
|
||||
DB_USERNAME=glastree
|
||||
DB_PASSWORD=...
|
||||
|
||||
GOOGLE_CLIENT_ID=....
|
||||
GOOGLE_CLIENT_SECRET=....
|
||||
```
|
||||
|
||||
## 🌐 Google OAuth 2.0 — Post-Deploy
|
||||
|
||||
1. [Google Cloud Console](https://console.cloud.google.com/) → Abilita Gmail API, Drive API, Calendar API
|
||||
2. Credenziali → OAuth Client ID → Web Application
|
||||
3. Redirect URI: `https://tuodominio.it/app/auth/google/callback`
|
||||
4. Inserisci Client ID/Secret in `.env`
|
||||
5. **Impostazioni → Google Services** → Connetti Gmail, Drive, Calendar
|
||||
6. Per Gmail: **Impostazioni → Email → IMAP** → seleziona "OAuth 2.0"
|
||||
@@ -291,11 +291,8 @@ if (!empty($contatto['individuo_id'])) { create }
|
||||
```
|
||||
|
||||
## Prossimi Passi
|
||||
- [DONE] ... (existing items)
|
||||
- Verificare end-to-end su remote: tutte le nuove mass action, mailing list con contatti senza email, ricerca multi-tag
|
||||
- Test end-to-end OAuth con credenziali Google reali (configurare Google Cloud Console, registrare callback URI, ottenere refresh token)
|
||||
- Verificare EmailSetting::isOauth() integration in email sync flow
|
||||
- Verificare StorageRepositoryService::buildGoogleDrive() OAuth fallback con repos esistenti
|
||||
- Verificare GoogleCalendarSyncService::buildClient() OAuth fallback con connessioni esistenti
|
||||
- [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`
|
||||
@@ -383,102 +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-15 — Centralized Google OAuth 2.0 System (Gmail, Drive, Calendar)
|
||||
|
||||
**Obiettivo**: Implementare autenticazione OAuth 2.0 centralizzata per Gmail (IMAP/SMTP), Google Drive e Google Calendar come alternativa opzionale all'auth con password, con revoca per-servizio.
|
||||
|
||||
### Files Creati
|
||||
- **Enum** `app/Enums/GoogleService.php` — 3 casi (Gmail, Drive, Calendar) con scope/label/icon/hexColor
|
||||
- **Migration 1** `2026_06_15_000001_create_google_oauth_connections_table.php` — tabella google_oauth_connections
|
||||
- **Migration 2** `2026_06_15_000002_add_auth_method_to_email_settings_table.php` — auth_method a email_settings
|
||||
- **Model** `app/Models/GoogleOAuthConnection.php` — encrypted field accessor/mutator
|
||||
- **Service** `app/Services/GoogleOAuthService.php` — full OAuth flow methods
|
||||
- **Controller** `app/Http/Controllers/GoogleOAuthController.php` — connect/callback/revoke/status
|
||||
- **Routes** in `routes/web.php` — 4 nuove route
|
||||
- **View** `resources/views/google-oauth/status.blade.php`
|
||||
|
||||
### Problemi Risolti
|
||||
1. Filesystem permission: directory 755 (www-data). Script PHP via Apache da storage/app/public/
|
||||
2. routes/web.php root:root 644 -> rename trick
|
||||
3. authorize() method conflict -> rinominato in connect()
|
||||
|
||||
### Architettura
|
||||
- Fallback pattern: Drive/Calendar provano OAuth centralizzata, poi config esistente
|
||||
- Per-service revoca: record separato per servizio (unique su service)
|
||||
- Single callback URI: /auth/google/callback, routing via state
|
||||
- Token encryption: AES-256-CBC + APP_KEY
|
||||
|
||||
### Completato
|
||||
1. ✅ **EmailSetting model** (`app/Models/EmailSetting.php`): `auth_method` in ``, `isOauth()` method, `getOAuthAccessToken()` via `GoogleOAuthService`, `getDecryptedPassword()` returns OAuth token when isOauth, `getDecryptedSmtpPassword()` same, `getImapClient()` returns null for OAuth, `testConnection()` has OAuth path via Webklex\PHPIMAP
|
||||
2. ✅ **EmailController** (`app/Http/Controllers/EmailController.php`): `syncEmails()` redirects to `syncEmailsViaOAuth()`, `connectImap()` redirects to `connectOAuthImap()`, `sendViaImap()` appends `auth_mode=xoauth2` to DSN, added `processWebklexMessage()` helper
|
||||
3. ✅ **StorageRepositoryService** (`app/Services/StorageRepositoryService.php`): `buildGoogleDrive()` tries `GoogleOAuthService::buildClient(GoogleService::Drive)` first, falls back to config. Same for `readGoogleDriveFile()` and `checkGoogleDriveApiStatus()`
|
||||
4. ✅ **GoogleCalendarSyncService** (`app/Services/GoogleCalendarSyncService.php`): `buildClient()` tries `GoogleOAuthService::buildClient(GoogleService::Calendar)` first, falls back to config
|
||||
5. ✅ **Tab Google Services** in `resources/views/impostazioni/index.blade.php` — nav tab + tab-pane with Connect/Revoke per service (Gmail, Drive, Calendar), status badges, alert linking to email tab. Added `auth_method` select in email IMAP tab
|
||||
6. ⏳ **Test end-to-end** — requires user to configure Google Cloud Console (OAuth consent screen, callback URI) and provide credentials
|
||||
|
||||
### Files Modified
|
||||
- `app/Models/EmailSetting.php` — OAuth-aware auth_method, decrypt overrides, IMAP client
|
||||
- `app/Http/Controllers/EmailController.php` — OAuth sync/send/connect paths
|
||||
- `app/Services/StorageRepositoryService.php` — Drive OAuth fallback
|
||||
- `app/Services/GoogleCalendarSyncService.php` — Calendar OAuth fallback
|
||||
- `resources/views/impostazioni/index.blade.php` — Google Services tab + auth_method select
|
||||
|
||||
### Bug Fix
|
||||
- **2026-06-15 — Variable interpolation in str_replace**: In `apply_all_mods.php`, the second argument to `str_replace()` used double quotes, causing PHP to interpolate $fileId, $service, $googleMimeType, $originalBasename as empty strings. Result: method signature `exportGoogleDriveDoc` lost all parameter variable names. Fixed by running a second script that used `preg_replace` to restore the signature. (file -> app/Services/StorageRepositoryService.php)
|
||||
|
||||
|
||||
### Pre-Production Review Fixes
|
||||
|
||||
**2026-06-15 — 4 criticità risolte prima del deploy**
|
||||
|
||||
| # | Problema | File | Fix |
|
||||
|---|----------|------|-----|
|
||||
| 1 | Credenziali Google vuote | `GoogleOAuthService.php:createClient()` | Sostituito `setAuthConfig([client_id:'']` con `setClientId(config(...))`, `setClientSecret(config(...))`, `setRedirectUri(...)`. Rimossi 4 import inutilizzati |
|
||||
| 2 | Route name errato | `google-oauth/status.blade.php:55` | `google.oauth.connect` → `google.oauth.authorize` |
|
||||
| 3 | Metodo inesistente `description()` | `google-oauth/status.blade.php:43` | Sostituito con `label() . ' - ' . scope()`. Layout cambiato da `layouts.app` a `layouts.adminlte` |
|
||||
| 4 | `auth_method` select mancante | `impostazioni/index.blade.php` | Inserito `<select name="auth_method">` all'inizio del card-body in `#email-imap` |
|
||||
| — | `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET` mancanti | `config/services.php`, `.env.example` | Aggiunte sezione google in services.php con `env()`, variabili a `.env.example` |
|
||||
| — | Istruzioni aggiornamento mancanti | `build-dist.sh` | Aggiunta sezione "AGGIORNAMENTO (SERVER ESISTENTE)" con backup, migrate, env vars |
|
||||
|
||||
### Files Review Round
|
||||
- `app/Services/GoogleOAuthService.php` — createClient() legge da config, import puliti
|
||||
- `resources/views/google-oauth/status.blade.php` — route, description, layout fix
|
||||
- `resources/views/impostazioni/index.blade.php` — auth_method select
|
||||
- `config/services.php` — sezione google
|
||||
- `.env.example` — GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
|
||||
- `build-dist.sh` — update instructions
|
||||
|
||||
|
||||
### 2026-06-15 — Cleanup: file inutilizzati, codice morto e import superflui
|
||||
|
||||
**File eliminati (7 backup .bak):**
|
||||
- `MEMORY.md.bak`, `EmailController.php.bak`, `web.php.bak`, `web.php.bak2`
|
||||
- `index.blade.php.bak` (141 KB), `build-dist.sh.bak`, `.env.example.bak`
|
||||
- Directory vuota `tests/Feature/Auth/`
|
||||
|
||||
**Metodi morti rimossi (3):**
|
||||
| Metodo | File | Motivo |
|
||||
|--------|------|--------|
|
||||
| `create()` | `DocumentoController.php` | Nessuna route registrata, view inesistente |
|
||||
| `index()` | `StorageRepositoryController.php` | Nessuna route GET, view inesistente |
|
||||
| `downloadMigrationScript()` | `BackupController.php` | Nessuna route, view inesistente |
|
||||
|
||||
**Import superflui rimossi (20):**
|
||||
- `EmailController.php`: `GoogleService`, `GoogleOAuthService`, `Crypt` (non usati direttamente, logica passa da `EmailSetting` model)
|
||||
- `BackupController.php`: `JsonResponse` (usato dal metodo rimosso)
|
||||
- `StorageRepositoryController.php`: `View` (usato dal metodo rimosso)
|
||||
- `GruppoMembroController.php`: `Individuo`, `Ruolo`
|
||||
- `HomeController.php`: `Notifica`
|
||||
- `ImpostazioniController.php`: `Crypt`
|
||||
- `AuthController.php`: `EmailSetting`, `Address`
|
||||
- `IndividuoController.php`: `Contatto`
|
||||
- `AppSetting.php`: `File`
|
||||
- `EmailAttachment.php`: `Storage`
|
||||
- `Evento.php`: `HasMany`
|
||||
- `ExportHelpPdf.php`: `Storage`
|
||||
- `BackupService.php`: `Notifica`, `Crypt`, `Storage`
|
||||
- `AuthServiceProvider.php`: `Route`
|
||||
|
||||
**Verifica:** Tutti i 14 file modificati passano `php -l`
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Console\Commands;
|
||||
use App\Models\AppSetting;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ExportHelpPdf extends Command
|
||||
{
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum GoogleService: string
|
||||
{
|
||||
case Gmail = 'gmail';
|
||||
case Drive = 'drive';
|
||||
case Calendar = 'calendar';
|
||||
|
||||
public function scope(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Gmail => '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::Gmail => 'Gmail (lettura e invio email)',
|
||||
self::Drive => 'Google Drive (file)',
|
||||
self::Calendar => 'Google Calendar (eventi)',
|
||||
};
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Gmail => 'fa-google',
|
||||
self::Drive => 'fa-google-drive',
|
||||
self::Calendar => 'fa-calendar-alt',
|
||||
};
|
||||
}
|
||||
|
||||
public function hexColor(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Gmail => '#EA4335',
|
||||
self::Drive => '#34A853',
|
||||
self::Calendar => '#4285F4',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -108,4 +109,10 @@ 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,12 +4,14 @@ 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,6 +134,16 @@ 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');
|
||||
|
||||
@@ -12,6 +12,7 @@ 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;
|
||||
|
||||
@@ -401,10 +402,6 @@ 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]);
|
||||
@@ -526,10 +523,6 @@ 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);
|
||||
@@ -650,10 +643,6 @@ 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',
|
||||
@@ -670,27 +659,6 @@ 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) {
|
||||
@@ -749,133 +717,6 @@ 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();
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<?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,6 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Gruppo;
|
||||
use App\Models\Individuo;
|
||||
use App\Models\Ruolo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GruppoMembroController extends Controller
|
||||
|
||||
@@ -10,6 +10,7 @@ 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,6 +14,7 @@ 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,6 +3,7 @@
|
||||
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,6 +10,7 @@ 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
|
||||
@@ -18,6 +19,19 @@ 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
|
||||
{
|
||||
$this->authorizeWrite('settings');
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class EmailAttachment extends Model
|
||||
{
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use App\Enums\GoogleService;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class EmailSetting extends Model
|
||||
@@ -16,7 +14,7 @@ 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', 'auth_method'
|
||||
'last_sync_at', 'is_active', 'signature', 'signature_enabled'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -43,9 +41,6 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
if ($this->isOauth()) {
|
||||
return $this->getOAuthAccessToken() ?? '';
|
||||
}
|
||||
if (empty($this->imap_password)) {
|
||||
return '';
|
||||
}
|
||||
@@ -58,9 +53,6 @@ class EmailSetting extends Model
|
||||
|
||||
public function getDecryptedSmtpPassword(): string
|
||||
{
|
||||
if ($this->isOauth()) {
|
||||
return $this->getOAuthAccessToken() ?? '';
|
||||
}
|
||||
if (empty($this->smtp_password)) {
|
||||
return $this->getDecryptedPassword();
|
||||
}
|
||||
@@ -71,26 +63,6 @@ class EmailSetting extends Model
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function isOauth(): bool
|
||||
{
|
||||
return $this->auth_method === 'oauth';
|
||||
}
|
||||
|
||||
public function getOAuthAccessToken(): ?string
|
||||
{
|
||||
if (!$this->isOauth()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$oauth = app(GoogleOAuthService::class);
|
||||
return $oauth->getAccessToken(GoogleService::Gmail);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('OAuth token refresh failed: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSmtpConfig(): array
|
||||
{
|
||||
return [
|
||||
@@ -108,10 +80,6 @@ class EmailSetting extends Model
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isOauth()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$encryption = match ($this->imap_encryption) {
|
||||
'ssl' => 'ssl',
|
||||
@@ -135,10 +103,6 @@ class EmailSetting extends Model
|
||||
public function testConnection(): array
|
||||
{
|
||||
try {
|
||||
if ($this->isOauth()) {
|
||||
return $this->testOAuthImapConnection();
|
||||
}
|
||||
|
||||
$mailbox = $this->getImapClient();
|
||||
|
||||
if (!$mailbox) {
|
||||
@@ -161,32 +125,6 @@ class EmailSetting extends Model
|
||||
}
|
||||
}
|
||||
|
||||
private function testOAuthImapConnection(): array
|
||||
{
|
||||
try {
|
||||
$token = $this->getOAuthAccessToken();
|
||||
if (!$token) {
|
||||
return ['success' => false, 'message' => 'Impossibile ottenere il token OAuth. Verifica la connessione Google.'];
|
||||
}
|
||||
|
||||
$client = new \Webklex\PHPIMAP\Client([
|
||||
'host' => $this->imap_host,
|
||||
'port' => $this->imap_port ?? 993,
|
||||
'encryption' => $this->imap_encryption ?? 'ssl',
|
||||
'validate_cert' => false,
|
||||
'username' => $this->imap_username,
|
||||
'password' => $token,
|
||||
'authentication' => 'oauth',
|
||||
]);
|
||||
$client->connect();
|
||||
$client->disconnect();
|
||||
|
||||
return ['success' => true, 'message' => 'Connessione IMAP OAuth riuscita! Server: ' . $this->imap_host];
|
||||
} catch (\Exception $e) {
|
||||
return ['success' => false, 'message' => 'Errore connessione IMAP OAuth: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public static function getActive()
|
||||
{
|
||||
return self::where('is_active', true)->first();
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Traits\HasTagsLight;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Evento extends Model
|
||||
{
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class GoogleOAuthConnection extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'service',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'refresh_token',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'service' => GoogleService::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function getClientSecretAttribute(?string $value): ?string
|
||||
{
|
||||
return $value ? Crypt::decryptString($value) : null;
|
||||
}
|
||||
|
||||
public function setClientSecretAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['client_secret'] = $value ? Crypt::encryptString($value) : null;
|
||||
}
|
||||
|
||||
public function getRefreshTokenAttribute(?string $value): ?string
|
||||
{
|
||||
return $value ? Crypt::decryptString($value) : null;
|
||||
}
|
||||
|
||||
public function setRefreshTokenAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['refresh_token'] = $value ? Crypt::encryptString($value) : null;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
|
||||
@@ -5,8 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\Notifica;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
|
||||
|
||||
@@ -4,9 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Models\CalendarioConnessione;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use App\Models\Evento;
|
||||
use Carbon\Carbon;
|
||||
use Google\Client as GoogleClient;
|
||||
@@ -329,16 +327,6 @@ class GoogleCalendarSyncService
|
||||
|
||||
private function buildClient(CalendarioConnessione $connessione): GoogleClient
|
||||
{
|
||||
try {
|
||||
$oauth = app(GoogleOAuthService::class);
|
||||
$connection = $oauth->getConnection(GoogleService::Calendar);
|
||||
if ($connection) {
|
||||
return $oauth->buildClient(GoogleService::Calendar);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::warning('Central Calendar OAuth failed, falling back: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$config = $connessione->getDecryptedConfig();
|
||||
|
||||
$client = new GoogleClient();
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Models\GoogleOAuthConnection;
|
||||
use Google\Client;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GoogleOAuthService
|
||||
{
|
||||
private const TOKEN_URI = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
private static function getRedirectUri(): string
|
||||
{
|
||||
$subfolder = config('app.url');
|
||||
return rtrim($subfolder, '/') . '/auth/google/callback';
|
||||
}
|
||||
|
||||
public function getAuthorizationUrl(GoogleService $service): string
|
||||
{
|
||||
$client = $this->createClient($service);
|
||||
$client->setRedirectUri(self::getRedirectUri());
|
||||
$client->setState($service->value);
|
||||
$client->setAccessType('offline');
|
||||
$client->setPrompt('consent');
|
||||
$client->setIncludeGrantedScopes(true);
|
||||
|
||||
return $client->createAuthUrl();
|
||||
}
|
||||
|
||||
public function handleCallback(string $code, GoogleService $service): GoogleOAuthConnection
|
||||
{
|
||||
$client = $this->createClient($service);
|
||||
$client->setRedirectUri(self::getRedirectUri());
|
||||
|
||||
$token = $client->fetchAccessTokenWithAuthCode($code);
|
||||
|
||||
if (isset($token['error'])) {
|
||||
throw new \RuntimeException('Google OAuth error: ' . ($token['error_description'] ?? $token['error']));
|
||||
}
|
||||
|
||||
if (!isset($token['refresh_token'])) {
|
||||
throw new \RuntimeException('No refresh token returned. Ensure access_type=offline and prompt=consent are set.');
|
||||
}
|
||||
|
||||
$clientId = $client->getClientId();
|
||||
$clientSecret = $client->getClientSecret();
|
||||
|
||||
return GoogleOAuthConnection::updateOrCreate(
|
||||
['service' => $service->value],
|
||||
[
|
||||
'client_id' => $clientId,
|
||||
'client_secret' => $clientSecret,
|
||||
'refresh_token' => $token['refresh_token'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function revoke(GoogleService $service): void
|
||||
{
|
||||
$connection = $this->getConnection($service);
|
||||
if (!$connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
$client = $this->createClient($service);
|
||||
$client->revokeToken();
|
||||
$connection->delete();
|
||||
}
|
||||
|
||||
public function getAccessToken(GoogleService $service): ?string
|
||||
{
|
||||
$connection = $this->getConnection($service);
|
||||
if (!$connection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$client = $this->createClient($service);
|
||||
$client->setAccessToken([
|
||||
'access_token' => '',
|
||||
'refresh_token' => $connection->refresh_token,
|
||||
'client_id' => $connection->client_id,
|
||||
'client_secret' => $connection->client_secret,
|
||||
]);
|
||||
|
||||
if ($client->isAccessTokenExpired()) {
|
||||
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
|
||||
$newToken = $client->getAccessToken();
|
||||
if (isset($newToken['refresh_token'])) {
|
||||
$connection->refresh_token = $newToken['refresh_token'];
|
||||
$connection->save();
|
||||
}
|
||||
}
|
||||
|
||||
$accessToken = $client->getAccessToken();
|
||||
return $accessToken['access_token'] ?? null;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Google OAuth token refresh failed for ' . $service->value . ': ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function isAuthorized(GoogleService $service): bool
|
||||
{
|
||||
return $this->getConnection($service) !== null;
|
||||
}
|
||||
|
||||
public function getConnection(GoogleService $service): ?GoogleOAuthConnection
|
||||
{
|
||||
return GoogleOAuthConnection::where('service', $service->value)->first();
|
||||
}
|
||||
|
||||
public function buildClient(GoogleService $service): Client
|
||||
{
|
||||
$client = $this->createClient($service);
|
||||
$connection = $this->getConnection($service);
|
||||
|
||||
if ($connection) {
|
||||
$client->setAccessToken([
|
||||
'access_token' => '',
|
||||
'refresh_token' => $connection->refresh_token,
|
||||
'client_id' => $connection->client_id,
|
||||
'client_secret' => $connection->client_secret,
|
||||
]);
|
||||
|
||||
if ($client->isAccessTokenExpired()) {
|
||||
$client->fetchAccessTokenWithRefreshToken($client->getRefreshToken());
|
||||
}
|
||||
}
|
||||
|
||||
return $client;
|
||||
}
|
||||
|
||||
private function createClient(GoogleService $service): Client
|
||||
{
|
||||
$client = new Client();
|
||||
$client->setApplicationName(config('app.name'));
|
||||
$client->setScopes([$service->scope()]);
|
||||
$client->setClientId(config('services.google.client_id'));
|
||||
$client->setClientSecret(config('services.google.client_secret'));
|
||||
$client->setRedirectUri(self::getRedirectUri());
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\GoogleService;
|
||||
use App\Models\StorageRepository;
|
||||
use App\Services\GoogleOAuthService;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use League\Flysystem\Filesystem;
|
||||
@@ -71,20 +69,6 @@ class StorageRepositoryService
|
||||
private function checkGoogleDriveApiStatus(array $config): ?string
|
||||
{
|
||||
try {
|
||||
try {
|
||||
$oauth = app(GoogleOAuthService::class);
|
||||
$connection = $oauth->getConnection(GoogleService::Drive);
|
||||
if ($connection) {
|
||||
$client = $oauth->buildClient(GoogleService::Drive);
|
||||
$service = new \Google_Service_Drive($client);
|
||||
$rootId = $config['root_folder_id'] ?? 'root';
|
||||
$service->files->get($rootId, ['fields' => 'id']);
|
||||
return null;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// fall through to config-based auth
|
||||
}
|
||||
|
||||
$client = new \Google_Client();
|
||||
$client->setClientId($config['client_id']);
|
||||
$client->setClientSecret($config['client_secret']);
|
||||
@@ -260,18 +244,6 @@ class StorageRepositoryService
|
||||
{
|
||||
$config = $repo->getDecryptedConfig();
|
||||
|
||||
try {
|
||||
$oauth = app(GoogleOAuthService::class);
|
||||
$connection = $oauth->getConnection(GoogleService::Drive);
|
||||
if ($connection) {
|
||||
$client = $oauth->buildClient(GoogleService::Drive);
|
||||
$service = new \Google_Service_Drive($client);
|
||||
return $this->readGoogleDriveFileWithService($service, $config, $normalizedPath, $basename);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::warning('Central Google Drive OAuth failed in readGoogleDriveFile, falling back: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
$client = new \Google_Client();
|
||||
$client->setClientId($config['client_id']);
|
||||
$client->setClientSecret($config['client_secret']);
|
||||
@@ -311,40 +283,6 @@ class StorageRepositoryService
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function readGoogleDriveFileWithService(\Google_Service_Drive $service, array $config, string $normalizedPath, string $basename): array
|
||||
{
|
||||
$rootFolderId = $config['root_folder_id'] ?? 'root';
|
||||
|
||||
$fileId = $this->findGoogleDriveFileId($service, $rootFolderId, $normalizedPath);
|
||||
$file = $service->files->get($fileId, ['fields' => 'id,name,mimeType,size']);
|
||||
|
||||
$mimeType = $file->getMimeType();
|
||||
|
||||
if ($mimeType && str_starts_with($mimeType, 'application/vnd.google-apps.')) {
|
||||
return $this->exportGoogleDriveDoc($service, $fileId, $mimeType, $basename);
|
||||
}
|
||||
|
||||
$response = $service->files->get($fileId, ['alt' => 'media']);
|
||||
$body = $response->getBody();
|
||||
$stream = $body instanceof \Psr\Http\Message\StreamInterface ? $body->detach() : null;
|
||||
|
||||
if (!$stream) {
|
||||
$stream = fopen('php://temp', 'r+');
|
||||
fwrite($stream, (string) $body);
|
||||
rewind($stream);
|
||||
}
|
||||
|
||||
$fileSize = $file->getSize();
|
||||
|
||||
return [
|
||||
'stream' => $stream,
|
||||
'mimeType' => $mimeType,
|
||||
'basename' => $basename,
|
||||
'fileSize' => $fileSize !== null ? (int) $fileSize : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function exportGoogleDriveDoc(\Google_Service_Drive $service, string $fileId, string $googleMimeType, string $originalBasename): array
|
||||
{
|
||||
$exportMap = [
|
||||
|
||||
Regular → Executable
-33
@@ -94,36 +94,3 @@ echo " php artisan view:clear"
|
||||
echo ""
|
||||
echo " # 6. Avvia installazione MySQL"
|
||||
echo " php install.php"
|
||||
|
||||
echo ""
|
||||
echo "=== AGGIORNAMENTO (SERVER ESISTENTE) ==="
|
||||
echo ""
|
||||
echo " 1. BACKUP:"
|
||||
echo " cp .env .env.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
echo " mysqldump -u USER -p DBNAME > backup_$(date +%Y%m%d_%H%M%S).sql"
|
||||
echo ""
|
||||
echo " 2. Estrai archivio (sovrascrive):"
|
||||
echo " tar xzf ${ARCHIVE}"
|
||||
echo ""
|
||||
echo " 3. Ripristina .env e aggiungi nuove variabili:"
|
||||
echo " cp .env.backup.* .env"
|
||||
echo " # Aggiungi se mancanti:"
|
||||
echo ' echo "GOOGLE_CLIENT_ID=" >> .env'
|
||||
echo ' echo "GOOGLE_CLIENT_SECRET=" >> .env'
|
||||
echo ""
|
||||
echo " 4. Permessi:"
|
||||
echo ' chmod -R 775 storage bootstrap/cache'
|
||||
echo ' chown -R www-data:www-data storage bootstrap/cache'
|
||||
echo ""
|
||||
echo " 5. Ricrea symlink storage:"
|
||||
echo ' rm -f public/storage'
|
||||
echo ' ln -sf ../storage/app/public public/storage'
|
||||
echo ""
|
||||
echo " 6. Esegui migration DB:"
|
||||
echo " php artisan migrate --force"
|
||||
echo ""
|
||||
echo " 7. Pulisci cache e ricostruisci asset:"
|
||||
echo " php artisan config:clear"
|
||||
echo " php artisan route:clear"
|
||||
echo " php artisan view:clear"
|
||||
echo " # npm install && npm run build (se serve ricostruire Vite)"
|
||||
|
||||
@@ -35,9 +35,4 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
'google' => [
|
||||
'client_id' => env('GOOGLE_CLIENT_ID'),
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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('google_oauth_connections', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('service', 20)->unique();
|
||||
$table->string('client_id', 255);
|
||||
$table->text('client_secret');
|
||||
$table->text('refresh_token');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('google_oauth_connections');
|
||||
}
|
||||
};
|
||||
@@ -1,24 +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('email_settings', function (Blueprint $table) {
|
||||
$table->string('auth_method', 20)->default('password');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('email_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('auth_method');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
@php
|
||||
$title = 'Google Services - ' . config('app.name');
|
||||
@endphp
|
||||
@extends('layouts.adminlte')
|
||||
@section('content')
|
||||
<section class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1>Google Services</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
@foreach($services as $svc)
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="{{ $svc['service']->icon() }}"></i>
|
||||
{{ $svc['service']->label() }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ $svc['service']->label() . ' - ' . $svc['service']->scope() }}</p>
|
||||
@if($svc['authorized'])
|
||||
<span class="badge bg-success">Connected</span>
|
||||
<form action="{{ route('google.oauth.revoke', $svc['service']->value) }}" method="POST" class="d-inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Revoke {{ $svc['service']->label() }} access?')">
|
||||
Revoke
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="badge bg-secondary">Not connected</span>
|
||||
<a href="{{ route('google.oauth.authorize', $svc['service']->value) }}" class="btn btn-primary btn-sm">
|
||||
Connect
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -35,9 +35,6 @@
|
||||
<a href="#calendario" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-calendar-alt mr-2"></i> Calendario
|
||||
</a>
|
||||
<a href="#google-services" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fab fa-google mr-2"></i> Google Services
|
||||
</a>
|
||||
<a href="#tipologie" class="list-group-item list-group-item-action" data-toggle="tab">
|
||||
<i class="nav-icon fas fa-file mr-2"></i> Tipologie Documenti
|
||||
</a>
|
||||
@@ -400,19 +397,6 @@
|
||||
<h3 class="card-title">Configurazione Server IMAP</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="auth_method">Metodo di autenticazione</label>
|
||||
<select name="auth_method" id="auth_method" class="form-control">
|
||||
<option value="password" {{ ($emailSettings->auth_method ?? 'password') === 'password' ? 'selected' : '' }}>Password / App Password</option>
|
||||
<option value="oauth" {{ ($emailSettings->auth_method ?? 'password') === 'oauth' ? 'selected' : '' }}>OAuth 2.0 (Google)</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Scegli "OAuth 2.0" per usare il token OAuth centralizzato di Google.
|
||||
Per Gmail con password, usa una
|
||||
<a href="https://support.google.com/accounts/answer/185833" target="_blank">App Password</a>.
|
||||
</small>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
@@ -862,85 +846,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{{-- === GOOGLE SERVICES === --}}
|
||||
<div class="tab-pane" id="google-services">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title"><i class="fab fa-google mr-2"></i> Google Services</h3>
|
||||
<div class="card-tools">
|
||||
<a href="{{ route('google.oauth.status') }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye mr-1"></i> Stato Dettagliato
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Gestisci le connessioni OAuth 2.0 centralizzate per Gmail, Google Drive e Google Calendar. Ogni servizio può essere configurato separatamente.</p>
|
||||
|
||||
@php
|
||||
$gsvc = app(\App\Services\GoogleOAuthService::class);
|
||||
$gservices = [
|
||||
\App\Enums\GoogleService::Gmail,
|
||||
\App\Enums\GoogleService::Drive,
|
||||
\App\Enums\GoogleService::Calendar,
|
||||
];
|
||||
@endphp
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success alert-dismissible">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row">
|
||||
@foreach($gservices as $svc)
|
||||
@php $gAuth = $gsvc->isAuthorized($svc); @endphp
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header" style="border-left: 4px solid {{ $svc->hexColor() }}">
|
||||
<h3 class="card-title">
|
||||
<i class="{{ $svc->icon() }}"></i>
|
||||
{{ $svc->label() }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<hr>
|
||||
@if($gAuth)
|
||||
<span class="badge bg-success"><i class="fas fa-check-circle"></i> Connected</span>
|
||||
<form action="{{ route('google.oauth.revoke', $svc->value) }}" method="POST" class="d-inline">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Revocare l\'accesso a {{ $svc->label() }}?')">
|
||||
<i class="fas fa-ban mr-1"></i> Revoca
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<span class="badge bg-secondary"><i class="fas fa-times-circle"></i> Non connesso</span>
|
||||
<a href="{{ route('google.oauth.authorize', $svc->value) }}" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-plug mr-1"></i> Connetti
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
<strong>Nota:</strong> Se utilizzi Gmail con OAuth 2.0, torna al tab
|
||||
<a href="#email" class="alert-link" data-toggle="tab">Email</a>
|
||||
e imposta il campo "Metodo di autenticazione" su "OAuth 2.0" nelle impostazioni IMAP.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- === TIPOLOGIE DOCUMENTI === --}}
|
||||
<div class="tab-pane active" id="tipologie">
|
||||
<div class="card card-primary">
|
||||
|
||||
@@ -276,18 +276,3 @@ Route::prefix('admin')->middleware(['auth', AdminOnly::class])->group(function (
|
||||
Route::post('/backup/toggle-auto', [\App\Http\Controllers\Admin\BackupController::class, 'toggleAuto'])->name('admin.backup.toggle-auto');
|
||||
Route::post('/backup/save-auto', [\App\Http\Controllers\Admin\BackupController::class, 'saveAutoConfig'])->name('admin.backup.save-auto');
|
||||
});
|
||||
|
||||
// Google OAuth 2.0 centralized routes
|
||||
Route::get('/auth/google/{service}/authorize', [\App\Http\Controllers\GoogleOAuthController::class, 'connect'])
|
||||
->name('google.oauth.authorize')
|
||||
->whereIn('service', ['gmail', 'drive', 'calendar'])
|
||||
->middleware('auth');
|
||||
Route::get('/auth/google/callback', [\App\Http\Controllers\GoogleOAuthController::class, 'callback'])
|
||||
->name('google.oauth.callback');
|
||||
Route::delete('/auth/google/{service}/revoke', [\App\Http\Controllers\GoogleOAuthController::class, 'revoke'])
|
||||
->name('google.oauth.revoke')
|
||||
->whereIn('service', ['gmail', 'drive', 'calendar'])
|
||||
->middleware('auth');
|
||||
Route::get('/auth/google/status', [\App\Http\Controllers\GoogleOAuthController::class, 'status'])
|
||||
->name('google.oauth.status')
|
||||
->middleware('auth');
|
||||
|
||||
Reference in New Issue
Block a user