feat: Google OAuth Steps 2-4 — backend web-callback, Electron deep link, login UI, avatar

Backend (api @ feature/batch-agent-v2):
- GET /auth/oauth/{provider}/web-callback: bounces Google redirect to adiuvai://
- OAUTH_REDIRECT_URI default: http://localhost:8000/api/v1/.../web-callback

Electron (adiuvAI @ develop):
- adiuvai:// protocol registered in forge.config.ts and via setAsDefaultProtocolClient
- Single-instance lock + second-instance/open-url deep-link handlers
- AuthManager.loginWithOAuth() + handleOAuthCallback()
- auth.loginWithOAuth tRPC mutation
- LoginForm: Google button, divider, pending state
- AppShell + AccountSection: avatar photo with initials fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Roberto Musso
2026-04-10 13:04:14 +02:00
parent 37d7e65b35
commit d068edc77e
3 changed files with 17 additions and 6 deletions

Submodule adiuvAI updated: 27bc9d90af...5d112c8dfd

2
api

Submodule api updated: ce139bbac3...c510cbaae5

View File

@@ -91,7 +91,7 @@ il backup encryption locale (`_cachedPassword`) dalla password utente.
## Step 3: Electron — Deep Link + Auth Manager ## Step 3: Electron — Deep Link + Auth Manager
**Status:** [ ] Da fare **Status:** [x] Completato
**Cosa fare:** **Cosa fare:**
- Registrare protocollo custom `adiuvai://` in `adiuvAI/forge.config.ts` (packagerConfig.protocols) - Registrare protocollo custom `adiuvai://` in `adiuvAI/forge.config.ts` (packagerConfig.protocols)
@@ -113,13 +113,19 @@ il backup encryption locale (`_cachedPassword`) dalla password utente.
- `adiuvAI/src/main/router/index.ts` - `adiuvAI/src/main/router/index.ts`
**Lessons learned:** **Lessons learned:**
- _(da compilare a step completato)_ - **`adiuvai://` non è accettato da Google Console come redirect URI**: Google accetta solo `http://localhost` e `https://`. Soluzione: il backend espone `GET /auth/oauth/{provider}/web-callback` che riceve il redirect da Google e rimanda subito a `adiuvai://`. Il redirect_uri registrato su Google punta al backend, non direttamente all'Electron app.
- **`OAUTH_REDIRECT_URI` punta al backend, non al dominio website**: `adiuvai.com` è il sito statico. L'API starà su `api.adiuvai.com` (o simile). Il default in `settings.py` è `http://localhost:8000/api/v1/auth/oauth/google/web-callback` per sviluppo locale — sovrascrivere con la var d'ambiente in prod.
- **`app.requestSingleInstanceLock()` è necessario per `second-instance`**: senza il lock, l'evento non viene mai emesso su Windows/Linux. Se `requestSingleInstanceLock()` restituisce `false`, bisogna uscire subito (`app.quit()`).
- **`process.defaultApp` in dev**: in dev mode, Electron è lanciato come `electron .` — il nome del processo non corrisponde all'app. Occorre passare `[path.resolve(process.argv[1])]` come terzo argomento a `setAsDefaultProtocolClient` per includere lo script nella registrazione OS del protocollo.
- **`post()` in auth-manager serializza snake_case**: il backend si aspetta `{ code, state }` in snake_case. La conversione avviene automaticamente via `toSnakeCase()` dentro `post()`, quindi la chiamata da `handleOAuthCallback` è safe.
- **`loginWithOAuth` usa `fetch()` diretto** (non `this.get()`): la route authorize è pubblica (no JWT richiesto), ma `get()` lancia se non autenticati. Usare `fetch()` diretto evita la dipendenza dalla sessione attiva.
- **`avatarUrl` in `UserProfileSchema`**: il backend restituisce `avatar_url` (snake_case) che viene camelCased in `toCamelCase()` prima del parse Zod. Il campo deve stare nello schema Zod come `avatarUrl` (camelCase).
--- ---
## Step 4: Electron — UI Login + Avatar ## Step 4: Electron — UI Login + Avatar
**Status:** [ ] Da fare **Status:** [x] Completato
**Cosa fare:** **Cosa fare:**
- In `adiuvAI/src/renderer/components/auth/LoginForm.tsx`: - In `adiuvAI/src/renderer/components/auth/LoginForm.tsx`:
@@ -139,7 +145,12 @@ il backup encryption locale (`_cachedPassword`) dalla password utente.
- Componenti profilo utente (da identificare) - Componenti profilo utente (da identificare)
**Lessons learned:** **Lessons learned:**
- _(da compilare a step completato)_ - **`oauthMutation.isPending` dura fino a 5 min**: la mutation rimane in `isPending` mentre Electron attende il deep-link. Il bottone mostra "Waiting for browser…" e tutti gli input vengono disabilitati tramite `isBusy = loginMutation.isPending || oauthMutation.isPending` per evitare azioni concorrenti.
- **Google icon inline SVG**: il progetto usa solo `lucide-react` per le icone. Il logo Google 'G' è stato inserito come SVG inline direttamente nel componente — non introdurre librerie di icone esterne.
- **`profile.avatarUrl` è snake_case sul backend ma camelCase dopo `toCamelCase()`**: il campo sul tipo `UserProfile` (Electron) si chiama `avatarUrl`. La proprietà è `nullable().optional()` nello schema Zod — verificare sempre il null prima di passarla a `AvatarImage`.
- **`AvatarImage` come fallback graceful**: Radix UI `AvatarImage` mostra `AvatarFallback` automaticamente se l'immagine non carica (CORS, URL scaduto, etc). Non serve gestire l'errore manualmente.
- **`AccountSection` usa IIFE per evitare variabili di blocco**: il profilo era condizionale — l'IIFE `(() => { ... })()` dentro JSX permette di dichiarare variabili locali (`displayName`, `initials`) senza inquinare il componente con useState o helper esterni.
- **`AppSidebarProps` type non usa `UserProfile` importato**: il tipo è definito inline per evitare dipendenze circolari tra renderer e shared. Se `UserProfile` dovesse cambiare, va aggiornato anche il tipo inline.
--- ---