From 33fcd884e34c501c694d726bf4015898c1df5435 Mon Sep 17 00:00:00 2001 From: Roberto Date: Fri, 10 Apr 2026 08:44:23 +0200 Subject: [PATCH] add implementation plan for Google OAuth login + avatar Co-Authored-By: Claude Opus 4.6 --- docs/plan-google-auth.md | 166 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 docs/plan-google-auth.md diff --git a/docs/plan-google-auth.md b/docs/plan-google-auth.md new file mode 100644 index 0000000..304ed7e --- /dev/null +++ b/docs/plan-google-auth.md @@ -0,0 +1,166 @@ +# Piano: Integrazione Google Login + Avatar + +## Contesto + +L'app adiuvAI ha un sistema auth custom (email+password, bcrypt, JWT HS256) funzionante. +Obiettivo: aggiungere Google Login come primo social provider, con la prospettiva di aggiungerne 2-3 in futuro (Microsoft, GitHub). + +**Decisione architetturale:** OAuth diretto nel sistema esistente (non Keycloak, non Auth0). +- Il backend continua a emettere i propri JWT +- Ogni provider e' un'implementazione concreta di un'astrazione condivisa +- Email+password resta il metodo primario di registrazione/login +- L'avatar utente viene preso da Google; per utenti email si usano iniziali o icona default + +**Nota encryption:** La `encryption_key` Fernet e' generata random, NON derivata dalla password. +Il social login non rompe la crittografia server-side. L'unico fix necessario e' sganciare +il backup encryption locale (`_cachedPassword`) dalla password utente. + +--- + +## Regole + +- **Completamento step:** aggiornare questo documento marcando lo step come `[x]` e annotare eventuali lessons learned +- **Commit:** al termine di ogni step, eseguire commit del codice + +--- + +## Step 1: Backend — DB + Model + Schema + +**Status:** [ ] Da fare + +**Cosa fare:** +- Migration Alembic: creare tabella `oauth_accounts` (`id`, `user_id FK`, `provider`, `provider_user_id`, `provider_email`, `created_at`; UNIQUE su `provider, provider_user_id`) +- Migration Alembic: rendere `users.password_hash` nullable +- Migration Alembic: aggiungere colonna `users.avatar_url` (VARCHAR, nullable) +- Model `OAuthAccount` in `api/app/models.py` con relationship a `User` +- Campo `avatar_url: str | None` sul model `User` +- Aggiornare `UserProfile` in `api/app/schemas.py` con `avatar_url` + +**File coinvolti:** +- `api/alembic/versions/XXX_add_oauth_and_avatar.py` (nuovo) +- `api/app/models.py` +- `api/app/schemas.py` + +**Lessons learned:** +- _(da compilare a step completato)_ + +--- + +## Step 2: Backend — OAuth Provider + Route + +**Status:** [ ] Da fare + +**Cosa fare:** +- Creare astrazione provider OAuth riusabile in `api/app/auth/oauth_providers.py` (nuovo) + - Classe base con: `get_authorization_url()`, `exchange_code()`, `get_userinfo()` + - Implementazione concreta `GoogleOAuthProvider` +- Aggiungere settings in `api/app/config/settings.py`: `GOOGLE_AUTH_CLIENT_ID`, `GOOGLE_AUTH_CLIENT_SECRET`, `OAUTH_REDIRECT_URI` + - Separati da `GMAIL_CLIENT_ID/SECRET` (scope diversi: `openid email profile` vs `gmail.readonly`) +- Aggiungere route in `api/app/api/routes/auth.py`: + - `GET /auth/oauth/{provider}/authorize` — genera state + PKCE code_challenge, ritorna authorize URL + - `POST /auth/oauth/{provider}/callback` — valida state, scambia code, fetch userinfo, crea/linka utente, salva avatar_url da Google, emette JWT +- Logica utente nel callback: + - `oauth_accounts` match? -> login utente esistente + - Email match + `email_verified=true`? -> link account a utente esistente (aggiorna avatar se mancante) + - Nessun match? -> crea nuovo utente (Fernet key, password_hash=None, avatar da Google) +- Aggiungere dependency `authlib` in requirements + +**Sicurezza:** +- PKCE obbligatorio (desktop app = public client) +- Auto-link email solo se `email_verified=true` da Google +- State param per prevenire CSRF + +**File coinvolti:** +- `api/app/auth/__init__.py` (nuovo) +- `api/app/auth/oauth_providers.py` (nuovo) +- `api/app/api/routes/auth.py` +- `api/app/config/settings.py` +- `api/requirements.txt` (o pyproject.toml) + +**Lessons learned:** +- _(da compilare a step completato)_ + +--- + +## Step 3: Electron — Deep Link + Auth Manager + +**Status:** [ ] Da fare + +**Cosa fare:** +- Registrare protocollo custom `adiuvai://` in `adiuvAI/forge.config.ts` (packagerConfig.protocols) +- In `adiuvAI/src/main/index.ts`: + - `app.setAsDefaultProtocolClient('adiuvai')` + - Windows/Linux: gestire `second-instance` event, parsare argv per deep link + - macOS: gestire `open-url` event + - Forward code+state all'auth manager +- In `adiuvAI/src/main/auth/auth-manager.ts`: + - Nuovo metodo `loginWithOAuth(provider)`: chiama authorize, apre browser, attende callback, scambia code, salva token + - Nuovo metodo `handleOAuthCallback(code, state)`: risolve la promise pendente +- In `adiuvAI/src/main/router/index.ts`: + - Aggiungere procedura tRPC `auth.loginWithOAuth` + +**File coinvolti:** +- `adiuvAI/forge.config.ts` +- `adiuvAI/src/main/index.ts` +- `adiuvAI/src/main/auth/auth-manager.ts` +- `adiuvAI/src/main/router/index.ts` + +**Lessons learned:** +- _(da compilare a step completato)_ + +--- + +## Step 4: Electron — UI Login + Avatar + +**Status:** [ ] Da fare + +**Cosa fare:** +- In `adiuvAI/src/renderer/components/auth/LoginForm.tsx`: + - Email+password resta il form principale (prima opzione, in alto) + - Divider "oppure" sotto il form + - Bottone "Sign in with Google" sotto il divider + - Stato "In attesa del browser..." durante il flow OAuth + - Su successo: invalidare `auth.status` +- Mostrare avatar nel profilo utente: + - Avatar da `avatar_url` (immagine circolare) se disponibile + - Fallback: iniziali del nome o icona default per utenti senza avatar +- Aggiornare tipo `UserProfile` in `adiuvAI/src/shared/api-types.ts` con `avatar_url` + +**File coinvolti:** +- `adiuvAI/src/renderer/components/auth/LoginForm.tsx` +- `adiuvAI/src/shared/api-types.ts` +- Componenti profilo utente (da identificare) + +**Lessons learned:** +- _(da compilare a step completato)_ + +--- + +## Step 5: Fix Backup Encryption + Test + +**Status:** [ ] Da fare + +**Cosa fare:** +- Sganciare BackupManager da `_cachedPassword`: + - Generare chiave backup random 256-bit al primo login (qualsiasi metodo auth) + - Salvarla in electron-store via `safeStorage` (stessa pattern di token.ts) + - Usare questa chiave al posto di `_cachedPassword` per AES backup + - Per utenti esistenti: generare nuova chiave, usarla per backup futuri +- Test backend (`api/tests/test_auth.py`): + - Test authorize URL generation + - Test callback: creazione utente, linking account, duplicati + - Test callback con email match -> auto-link + - Test state mismatch -> 401 +- Test manuale end-to-end: + - Click "Sign in with Google" -> browser -> consent -> redirect -> autenticato + - Account linking: stessa email via password e Google -> stesso utente + - Utente solo-social: nuovo utente senza password + - Backup con utente social -> chiave device-specific + +**File coinvolti:** +- `adiuvAI/src/main/auth/auth-manager.ts` +- BackupManager (da identificare path esatto) +- `api/tests/test_auth.py` + +**Lessons learned:** +- _(da compilare a step completato)_