From d068edc77e24530275574642b770d7b3f6c40c45 Mon Sep 17 00:00:00 2001 From: Roberto Musso Date: Fri, 10 Apr 2026 13:04:14 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Google=20OAuth=20Steps=202-4=20?= =?UTF-8?q?=E2=80=94=20backend=20web-callback,=20Electron=20deep=20link,?= =?UTF-8?q?=20login=20UI,=20avatar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- adiuvAI | 2 +- api | 2 +- docs/plan-google-auth.md | 19 +++++++++++++++---- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/adiuvAI b/adiuvAI index 27bc9d9..5d112c8 160000 --- a/adiuvAI +++ b/adiuvAI @@ -1 +1 @@ -Subproject commit 27bc9d90afd349c73a69c70feb590e8d961935af +Subproject commit 5d112c8dfdde46f2c9241ca57e2de1b5bac0126b diff --git a/api b/api index ce139bb..c510cba 160000 --- a/api +++ b/api @@ -1 +1 @@ -Subproject commit ce139bbac317fe04a98ff54f28da91a46534ee91 +Subproject commit c510cbaae5a421ea91f53e92d69c3a1144722455 diff --git a/docs/plan-google-auth.md b/docs/plan-google-auth.md index a8c48c5..943499d 100644 --- a/docs/plan-google-auth.md +++ b/docs/plan-google-auth.md @@ -91,7 +91,7 @@ il backup encryption locale (`_cachedPassword`) dalla password utente. ## Step 3: Electron — Deep Link + Auth Manager -**Status:** [ ] Da fare +**Status:** [x] Completato **Cosa fare:** - 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` **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 -**Status:** [ ] Da fare +**Status:** [x] Completato **Cosa fare:** - 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) **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. ---