diff --git a/.env.example b/.env.example index 0b659a3..22ff05f 100644 --- a/.env.example +++ b/.env.example @@ -2,10 +2,17 @@ # Copy this file to .env.local and fill in your values # API Configuration -# Browser-side requests use relative URLs (empty string) automatically. -# API_INTERNAL_URL is only needed for server-side rendering in Docker. -# For local dev, the default (http://localhost:3000) works out of the box. -# API_INTERNAL_URL=http://localhost:3100 +# +# NEXT_PUBLIC_API_URL -- used by the browser for client-side API requests. +# Local dev: set to the API server URL (e.g., http://localhost:3100) +# Production (behind Caddy): leave unset -- relative URLs are routed by the reverse proxy +# +# API_INTERNAL_URL -- used by Next.js SSR for server-side API requests. +# Local dev: set to the API server URL (e.g., http://localhost:3100) +# Docker: set to Docker service name (e.g., http://barazo-api:3000) +# +NEXT_PUBLIC_API_URL=http://localhost:3100 +API_INTERNAL_URL=http://localhost:3100 # Application NEXT_PUBLIC_APP_NAME=Barazo diff --git a/src/app/auth/callback/page.tsx b/src/app/auth/callback/page.tsx index 60be8ff..e51475c 100644 --- a/src/app/auth/callback/page.tsx +++ b/src/app/auth/callback/page.tsx @@ -1,7 +1,7 @@ /** - * OAuth callback page -- processes auth response from PDS. - * URL: /auth/callback?code=...&state=... - * On success: stores token in auth context (memory), redirects to returnTo or /. + * OAuth callback page -- completes auth after API redirect. + * URL: /auth/callback?success=true (redirected from API after PDS OAuth) + * On success: refreshes session via cookie, stores token in memory, redirects to returnTo or /. * On failure: shows error with retry link. * @see specs/prd-web.md Section M3 (Auth Flow) */ @@ -11,7 +11,7 @@ import { Suspense, useEffect, useMemo, useState, useRef } from 'react' import { useSearchParams } from 'next/navigation' import Link from 'next/link' -import { handleCallback } from '@/lib/api/client' +import { refreshSession } from '@/lib/api/client' import { useAuth } from '@/hooks/use-auth' function CallbackContent() { @@ -19,14 +19,15 @@ function CallbackContent() { const { setSessionFromCallback } = useAuth() const processedRef = useRef(false) - const code = searchParams.get('code') - const state = searchParams.get('state') + const success = searchParams.get('success') + const errorParam = searchParams.get('error') // Derive missing-params error synchronously (not in effect) - const missingParamsError = useMemo( - () => (!code || !state ? 'Missing authorization code or state parameter' : null), - [code, state] - ) + const missingParamsError = useMemo(() => { + if (errorParam) return errorParam + if (success) return null + return 'Missing authorization parameters' + }, [errorParam, success]) const [error, setError] = useState(missingParamsError) @@ -36,7 +37,9 @@ function CallbackContent() { async function processCallback() { try { - const session = await handleCallback(code!, state!) + // API already set the HTTP-only refresh cookie during redirect. + // Call refresh to get the access token from the cookie. + const session = await refreshSession() setSessionFromCallback(session) const returnTo = sessionStorage.getItem('auth_returnTo') ?? '/' @@ -48,7 +51,7 @@ function CallbackContent() { } void processCallback() - }, [code, state, missingParamsError, setSessionFromCallback]) + }, [missingParamsError, setSessionFromCallback]) if (error) { return ( diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 9700c56..3be6b80 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -66,7 +66,8 @@ function LoginContent() { alt="Barazo" width={160} height={42} - className="h-10 w-auto dark:hidden" + className="h-10 dark:hidden" + style={{ width: 'auto' }} priority /> Barazo diff --git a/src/components/layout/forum-layout.tsx b/src/components/layout/forum-layout.tsx index e63c780..e632492 100644 --- a/src/components/layout/forum-layout.tsx +++ b/src/components/layout/forum-layout.tsx @@ -34,7 +34,8 @@ export function ForumLayout({ children, sidebar }: ForumLayoutProps) { alt="Barazo" width={120} height={32} - className="h-8 w-auto dark:hidden" + className="h-8 dark:hidden" + style={{ width: 'auto' }} priority /> Barazo diff --git a/src/context/auth-context.tsx b/src/context/auth-context.tsx index 5c10a54..66803fe 100644 --- a/src/context/auth-context.tsx +++ b/src/context/auth-context.tsx @@ -108,8 +108,8 @@ export function AuthProvider({ children }: AuthProviderProps) { }, [setSession]) const login = useCallback(async (handle: string) => { - const { redirectUrl } = await initiateLogin(handle) - window.location.href = redirectUrl + const { url } = await initiateLogin(handle) + window.location.href = url }, []) const logout = useCallback(async () => { diff --git a/src/lib/api/auth-fetch.ts b/src/lib/api/auth-fetch.ts index ab8c35a..766e511 100644 --- a/src/lib/api/auth-fetch.ts +++ b/src/lib/api/auth-fetch.ts @@ -8,7 +8,9 @@ import { refreshSession } from './client' import type { AuthSession } from './types' const API_URL = - typeof window !== 'undefined' ? '' : (process.env.API_INTERNAL_URL ?? 'http://localhost:3000') + typeof window !== 'undefined' + ? (process.env.NEXT_PUBLIC_API_URL ?? '') + : (process.env.API_INTERNAL_URL ?? 'http://localhost:3000') interface AuthFetchOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE' diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index 735d9e0..ea14d81 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -46,7 +46,9 @@ import type { } from './types' const API_URL = - typeof window !== 'undefined' ? '' : (process.env.API_INTERNAL_URL ?? 'http://localhost:3000') + typeof window !== 'undefined' + ? (process.env.NEXT_PUBLIC_API_URL ?? '') + : (process.env.API_INTERNAL_URL ?? 'http://localhost:3000') interface FetchOptions { headers?: Record @@ -95,9 +97,9 @@ function buildQuery(params: Record): string // --- Auth endpoints --- -export function initiateLogin(handle: string): Promise<{ redirectUrl: string }> { +export function initiateLogin(handle: string): Promise<{ url: string }> { const query = buildQuery({ handle }) - return apiFetch<{ redirectUrl: string }>(`/api/auth/login${query}`) + return apiFetch<{ url: string }>(`/api/auth/login${query}`) } export function handleCallback(code: string, state: string): Promise { @@ -110,7 +112,6 @@ export async function refreshSession(): Promise { const response = await fetch(url, { method: 'POST', credentials: 'include', - headers: { 'Content-Type': 'application/json' }, }) if (!response.ok) { diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 0133c4a..10be746 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -42,7 +42,7 @@ export const handlers = [ return HttpResponse.json({ error: 'handle is required' }, { status: 400 }) } return HttpResponse.json({ - redirectUrl: `https://bsky.social/oauth/authorize?handle=${encodeURIComponent(handle)}&state=mock-state-123`, + url: `https://bsky.social/oauth/authorize?handle=${encodeURIComponent(handle)}&state=mock-state-123`, }) }),