add implementation plan for Google OAuth login + avatar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
166
docs/plan-google-auth.md
Normal file
166
docs/plan-google-auth.md
Normal file
@@ -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)_
|
||||||
Reference in New Issue
Block a user