diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 62f222e0..68d65585 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -34,7 +34,7 @@ jobs: - name: Install app dependencies run: npm ci - - name: Install Playwright browsers + - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Run Playwright tests diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 702a3bab..73f47470 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -263,11 +263,83 @@ export async function GET() { ``` [next-auth][error][NO_SECRET] ``` -Add `NEXTAUTH_SECRET` to `.env.local`. +Add `NEXTAUTH_SECRET` to `.env.local`. Generate one with: +```bash +# macOS / Linux +openssl rand -base64 32 +# Windows PowerShell +[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Maximum 256 })) +``` + +--- + +### GitHub OAuth `error=github` Redirect Loop + +**Symptom:** After clicking "Sign in with GitHub" and completing the GitHub flow, the browser redirects back to `/auth/signin?error=github` instead of the dashboard. + +Work through this checklist in order: + +#### 1. Missing or placeholder env vars (most common cause) + +Open `.env.local` and confirm these four are set to real values (not `your_...` placeholders): + +```env +GITHUB_ID=Ov23... # from github.com/settings/developers +GITHUB_SECRET=ghp_... # generated in the same OAuth App +NEXTAUTH_SECRET=<32-byte> # run: openssl rand -base64 32 +NEXTAUTH_URL=http://localhost:3000 +``` + +Also required for the database upsert on sign-in: +```env +NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co +SUPABASE_SERVICE_ROLE_KEY=eyJ... +``` + +If `NEXT_PUBLIC_SUPABASE_URL` or `SUPABASE_SERVICE_ROLE_KEY` are missing, the server log will print: +``` +signIn: supabaseAdmin is not configured; skipping DB upsert. +``` +Authentication will still succeed, but no user record will be written to Supabase. + +#### 2. Callback URL mismatch in the GitHub OAuth App + +The **Authorization callback URL** in your GitHub OAuth App must be **exactly**: + +``` +http://localhost:3000/api/auth/callback/github +``` + +Any trailing slash, different port, or HTTPS vs HTTP mismatch will cause `error=github`. Verify at [github.com/settings/developers](https://github.com/settings/developers) → your OAuth App → **Authorization callback URL**. + +#### 3. `ENCRYPTION_KEY` not set + +The `ENCRYPTION_KEY` is required for OAuth token encryption: + +```env +ENCRYPTION_KEY=<64 hex chars> # run: openssl rand -hex 32 +``` + +On Windows PowerShell: +```powershell +-join ((1..32) | ForEach-Object { "{0:x2}" -f (Get-Random -Maximum 256) }) +``` + +#### 4. Restart the dev server after changing env vars + +Next.js reads `.env.local` only at startup. After any change, stop and restart: + +```bash +npm run dev +``` + +#### 5. Check the server console for the real error + +The browser only shows `error=github` — the actual error is printed to the **terminal running `npm run dev`**. Look for lines starting with `[next-auth]` or `signIn:`. --- -### GitHub OAuth callback mismatch +### GitHub OAuth callback URL mismatch ``` The redirect_uri is not associated with this application ``` diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index b89babda..dad7bfc7 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -1,12 +1,80 @@ "use client"; import { signIn } from "next-auth/react"; -import { useEffect, useRef } from "react"; +import { Suspense, useEffect, useRef } from "react"; +import { useSearchParams } from "next/navigation"; + const A = "#818cf8"; +const ERR = "#f87171"; const MONO = "var(--font-jetbrains, ui-monospace, monospace)"; const DISP = "var(--font-syne, system-ui, sans-serif)"; +/** Maps NextAuth error codes → user-facing messages. */ +const AUTH_ERROR_MESSAGES: Record = { + github: + "GitHub sign-in failed. This is usually caused by incorrect OAuth credentials or a mismatched callback URL. Check your GitHub OAuth App settings and try again.", + OAuthCallback: + "The OAuth callback could not be completed. Please try signing in again.", + OAuthSignin: + "Could not start the GitHub sign-in flow. Please try again.", + Configuration: + "There is a server configuration error. Please contact the site administrator.", + AccessDenied: + "Access was denied. You may have cancelled the GitHub authorization.", + Verification: + "The sign-in link has expired or has already been used.", + Default: + "An unexpected authentication error occurred. Please try again.", +}; + +function getErrorMessage(error: string): string { + return AUTH_ERROR_MESSAGES[error] ?? AUTH_ERROR_MESSAGES.Default; +} + +function AuthErrorBanner({ error }: { error: string }) { + return ( +
+

+ ⚠ Sign-in failed +

+

+ {getErrorMessage(error)} +

+
+ ); +} + function MouseSpotlight() { const ref = useRef(null); useEffect(() => { @@ -35,7 +103,25 @@ function MouseSpotlight() { ); } -export default function SignInPage() { +/** + * Inner component that reads search params — must live inside a Suspense + * boundary because useSearchParams() opts the subtree out of static rendering. + */ +function SignInContent() { + const searchParams = useSearchParams(); + const error = searchParams.get("error"); + + // Clear the ?error= param from the URL immediately after reading it so + // that refreshing the page or navigating back doesn't show a stale error + // from a previous sign-in attempt. + useEffect(() => { + if (error && typeof window !== "undefined") { + const url = new URL(window.location.href); + url.searchParams.delete("error"); + window.history.replaceState({}, "", url.toString()); + } + }, [error]); + return (
+ {error && } +
); } + +export default function SignInPage() { + return ( + + + + ); +} diff --git a/src/components/landing/LandingPage.tsx b/src/components/landing/LandingPage.tsx index 2dba147e..e97d9921 100644 --- a/src/components/landing/LandingPage.tsx +++ b/src/components/landing/LandingPage.tsx @@ -156,7 +156,7 @@ function LandingNav() { DEVTRACK - + SIGN IN → @@ -402,7 +402,7 @@ function HeroSection() { {/* CTAs */}
- + @@ -667,7 +667,7 @@ function SetupSection() {
- + Sign in with GitHub ; function createUnavailableSupabaseAdmin(): SupabaseAdminClient {