# 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:** [x] Completato **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:** - `get_current_user` in `middleware/auth.py` esegue una query separata per `name/surname` — aggiornare quella query per includere `avatar_url` (non solo il model). Altrimenti il campo viene ignorato anche se presente in DB. - `update_profile` in `routes/auth.py` costruisce `UserProfile` manualmente: va aggiornato esplicitamente con `avatar_url=user.avatar_url`. - La `OAuthAccount.user` relationship richiede forward reference: il model deve essere dichiarato _dopo_ `User` ma la relationship su `User` usa `OAuthAccount` — SQLAlchemy risolve automaticamente con le stringhe lazy evaluation, ma occorre che il model sia nello stesso modulo. --- ## Step 2: Backend — OAuth Provider + Route **Status:** [x] Completato **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:** - **`authlib` non è necessaria**: il flow PKCE con Google si implementa direttamente con `httpx` (già in requirements). Aggiungere `authlib` sarebbe una dipendenza inutilizzata. Se si vorrà usare authlib in futuro (es. per provider con flow più complessi), aggiungerla allora. - **State store in-memory**: `_pending_states` è un dict a livello modulo — funziona in dev con un solo processo, ma non sopravvive a restart e non scala su più worker. In produzione va sostituito con Redis (o un campo temporaneo su DB). - **`_issue_refresh_token` helper**: la logica di emissione token è condivisa tra i tre branch del callback (link esistente, link email, nuovo utente) — fattorizzarla in un helper evita duplicazione. - **`tuple_` import non usato**: l'import `sql_tuple` da sqlalchemy può essere rimosso (non serve per le query attuali). - **Route `provider` tipizzata come `Literal["google"]`**: FastAPI valida automaticamente il parametro path e risponde 422 per provider sconosciuti, rendendo superfluo un check manuale. Il dict `_PROVIDERS` serve come fallback di sicurezza. - **`await db.flush()` prima di creare `OAuthAccount`**: necessario per ottenere `new_user.id` prima del commit, altrimenti il FK fallisce. --- ## 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)_