Files
tiennm99 129cbb7bf0 feat(phase-1): auth session wiring (proxy + layout + helpers)
- proxy.ts: composes Supabase session refresh + next-intl middleware
  into a single NextResponse via copyCookies helper. Coarse auth gate
  on /dashboard + /admin prefixes redirects unauth users to
  /[locale]/sign-in (no ?next= per trimmed plan).
- lib/supabase/session.ts: implements updateSupabaseSession() returning
  { response, user }. Cookies written onto both request.cookies (for
  downstream reads) and response.cookies (for browser). PROTECTED_PATH_PREFIXES
  exported as the gate list.
- lib/proxy/copy-cookies.ts: small helper that ports Set-Cookie entries
  between two NextResponses.
- lib/auth/get-server-session.ts: getServerSession() returning
  { user, role } | null. Derives User type from the factory's return
  type so @supabase/supabase-js stays out of allow-listed lib/auth/*
  per ESLint no-restricted-imports.
- lib/auth/session-provider.tsx: client-side context exposing user to
  client components via useSession() — populated once per request in
  the locale layout.
- app/[locale]/layout.tsx: reads user via getUser() outside any
  'use cache' scope; wraps children in SessionProvider; explicit
  'use cache' warning comment.
2026-05-25 17:30:41 +07:00

69 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "server-only";
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
import { serverEnv, SUPABASE_SCHEMA } from "@/lib/env/server";
import type { User } from "@supabase/supabase-js";
/**
* Coarse list of path prefixes that require authentication.
* Checked by proxy.ts AFTER stripping the locale segment.
* Extend here as new protected route groups are added (phases 0506).
* `/sign-in` is intentionally absent — it must be reachable while unauth'd.
*/
export const PROTECTED_PATH_PREFIXES: ReadonlyArray<string> = ["/dashboard", "/admin"];
/**
* Refreshes the Supabase auth session for an incoming request.
*
* Call this BEFORE handing the request to `next-intl/middleware`. Merge the
* returned `response`'s Set-Cookie headers onto whichever response next-intl
* ultimately returns — do NOT build two independent NextResponse instances or
* one set of cookies will be silently dropped (use `copyCookies` helper).
*
* Returns `{ response, user }` where `user` is `null` on auth failure or when
* no session exists. The `response` always carries refreshed (or unchanged)
* Supabase cookie deltas.
*
* Error-swallow on `getUser()` is intentional: a transient Supabase Auth
* outage should not hard-fail every request. The caller treats `user: null` as
* unauthenticated and applies the protected-path redirect; Supabase cookies are
* preserved so the next request retries. (Confirmed acceptable — code-reviewer N6.)
*/
export async function updateSupabaseSession(
request: NextRequest,
): Promise<{ response: NextResponse; user: User | null }> {
const response = NextResponse.next({ request });
const supabase = createServerClient(
serverEnv.NEXT_PUBLIC_SUPABASE_URL,
serverEnv.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
{
db: { schema: SUPABASE_SCHEMA },
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
for (const { name, value, options } of cookiesToSet) {
// Write onto the request so downstream middleware see the fresh token.
request.cookies.set(name, value);
// Write onto the response so the browser receives the refreshed cookie.
response.cookies.set(name, value, options);
}
},
},
},
);
let user: User | null = null;
try {
const { data } = await supabase.auth.getUser();
user = data.user;
} catch {
// Transient Supabase Auth outage: keep stale cookies; next request retries.
}
return { response, user };
}