diff --git a/docs/superpowers/specs/2026-04-03-login-page-oauth-design.md b/docs/superpowers/specs/2026-04-03-login-page-oauth-design.md new file mode 100644 index 0000000..c4a5dcc --- /dev/null +++ b/docs/superpowers/specs/2026-04-03-login-page-oauth-design.md @@ -0,0 +1,144 @@ +# Login Page Redesign + OAuth Support + +## Overview + +Redesign the login/register pages with a left-right split layout featuring prominent branding, and add Google/GitHub/Apple OAuth login via standard server-side redirect flow. + +## UI Design + +### Layout + +- **Desktop**: 50/50 left-right split +- **Mobile**: Brand area hidden or collapsed to compact top banner; form area full-width + +### Left Panel (Brand Area) + +Shared `AuthBranding` component used by both Login and Register pages. + +- Dark/gradient background (fox-amber → fox-orange gradient from existing CSS variables) +- Large product icon (~80px SVG fox logo) +- Product name "AgentFox" (large heading font) +- Slogan "API Docs for LLMs, Done Right" +- 3 feature highlights, each with icon + text: + - "Multi-level API retrieval for minimal token usage" + - "Import OpenAPI specs in seconds" + - "Works with any MCP-compatible LLM" + +### Right Panel (Form Area) + +- Light background, vertically centered +- Title: "Sign in to your account" (login) / "Create your account" (register) +- Email + Password inputs (reuse existing input styles) +- Primary action button +- Divider: "── or continue with ──" +- Three OAuth buttons in a row: Google / GitHub / Apple (each with official SVG icon) +- Footer link: "Don't have an account? Sign up" / "Already have an account? Sign in" + +## OAuth Architecture + +### Flow (Standard Server-Side Redirect) + +``` +Browser clicks OAuth button + → GET /api/auth/oauth/:provider + → Server builds authorization URL with state param, 302 redirects to Provider + → User authorizes on Provider's page + → Provider redirects to GET /api/auth/oauth/:provider/callback?code=xxx&state=yyy + → Server validates state, exchanges code for access_token + → Server fetches user info (email, name, avatar) + → Server finds or creates user (see Account Linking below) + → Server issues JWT (accessToken + refreshToken) + → Server 302 redirects to frontend /login/callback?accessToken=xxx&refreshToken=xxx +``` + +### Account Linking Strategy + +On OAuth callback, the server resolves the user in this order: + +1. Look up `OAuthAccount` by `(provider, providerAccountId)` → if found, use linked `User` +2. If no OAuthAccount match, look up `User` by `email` → if found, create `OAuthAccount` linking to existing user +3. If no User match, create new `User` (passwordHash=null, name and avatarUrl from provider) + new `OAuthAccount` + +### Security + +- **CSRF protection**: Generate random `state` parameter per auth request, store in in-memory Map with 10-minute TTL, validate on callback +- **Token delivery**: Tokens passed via URL query params; frontend immediately consumes and clears URL +- **Secrets**: All client secrets stay server-side; no OAuth SDK loaded in frontend + +### Provider Configuration + +| Provider | Auth URL | Token URL | UserInfo URL | Scopes | +|----------|----------|-----------|--------------|--------| +| Google | accounts.google.com/o/oauth2/v2/auth | oauth2.googleapis.com/token | www.googleapis.com/oauth2/v2/userinfo | email, profile | +| GitHub | github.com/login/oauth/authorize | github.com/login/oauth/access_token | api.github.com/user + /user/emails | user:email | +| Apple | appleid.apple.com/auth/authorize | appleid.apple.com/auth/token | (decoded from id_token) | name, email | + +### Environment Variables + +```env +# Already in .env.example +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# New +APPLE_CLIENT_ID= +APPLE_TEAM_ID= +APPLE_KEY_ID= +APPLE_PRIVATE_KEY= +OAUTH_CALLBACK_BASE_URL=http://localhost:3000 +``` + +### Frontend Callback Page (`/login/callback`) + +- Extracts `accessToken` and `refreshToken` from URL search params +- Stores in localStorage, updates AuthContext +- Redirects to `/dashboard` (or saved redirect target) +- Shows loading spinner during processing +- Shows error message with retry link on failure + +## File Changes + +### New Files + +| File | Purpose | +|------|---------| +| `packages/server/src/routes/oauth.ts` | OAuth routes (/:provider, /:provider/callback) | +| `packages/server/src/lib/oauth-providers.ts` | Provider configs + token exchange + userinfo fetch | +| `packages/web/src/pages/LoginCallback.tsx` | OAuth callback landing page | +| `packages/web/src/components/AuthBranding.tsx` | Shared left-panel brand component | +| `packages/web/src/components/OAuthButtons.tsx` | Third-party login button group | + +### Modified Files + +| File | Change | +|------|--------| +| `packages/server/src/index.ts` | Register `/auth/oauth` route | +| `packages/web/src/pages/Login.tsx` | Refactor to left-right split layout | +| `packages/web/src/pages/Register.tsx` | Refactor to left-right split layout | +| `packages/web/src/App.tsx` | Add `/login/callback` route | +| `packages/web/src/lib/i18n.tsx` | Add translation keys | +| `.env.example` | Add Apple OAuth env vars | + +### No Changes Needed + +- `prisma/schema.prisma` — OAuthAccount model already exists +- JWT signing logic — reuse existing `generateAccessToken`/`generateRefreshToken` +- Existing email/password auth — unchanged + +### No New Dependencies + +- OAuth token exchange: Node native `fetch` +- Apple JWT client_secret signing: Node `crypto` built-in +- No Passport.js, no OAuth libraries + +## User Action Required + +Before testing OAuth, the developer must register apps on each provider: + +- **Google**: Google Cloud Console → OAuth 2.0 Client → redirect URI: `{OAUTH_CALLBACK_BASE_URL}/auth/oauth/google/callback` +- **GitHub**: GitHub Developer Settings → OAuth App → callback URL: `{OAUTH_CALLBACK_BASE_URL}/auth/oauth/github/callback` +- **Apple**: Apple Developer → Services ID + Key → return URL: `{OAUTH_CALLBACK_BASE_URL}/auth/oauth/apple/callback` (requires HTTPS) + +Apple Sign In requires a paid Apple Developer account ($99/year) and HTTPS for callbacks. If unavailable, the Apple button can be displayed as "Coming Soon".