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:
2
adiuvAI
2
adiuvAI
Submodule adiuvAI updated: 27bc9d90af...5d112c8dfd
2
api
2
api
Submodule api updated: ce139bbac3...c510cbaae5
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user