mirror of
https://github.com/tiennm99/bsk.git
synced 2026-06-17 16:48:17 +00:00
129cbb7bf0
- 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.
96 lines
4.6 KiB
TypeScript
96 lines
4.6 KiB
TypeScript
import createMiddleware from "next-intl/middleware";
|
|
import { NextResponse, type NextRequest } from "next/server";
|
|
import { routing } from "@/i18n/routing";
|
|
import { updateSupabaseSession, PROTECTED_PATH_PREFIXES } from "@/lib/supabase/session";
|
|
import { copyCookies } from "@/lib/proxy/copy-cookies";
|
|
|
|
/**
|
|
* Built once at module load — not per request.
|
|
* next-intl composition pattern: call createMiddleware() here, then invoke the
|
|
* returned function inside proxy() with the actual request.
|
|
* Ref: next-intl docs "Usage without framework integration".
|
|
*/
|
|
const handleI18nRouting = createMiddleware(routing);
|
|
|
|
/**
|
|
* Returns true when the path (locale-stripped) starts with a protected prefix.
|
|
* Input: raw pathname from NextRequest (e.g. "/vi/dashboard/patients").
|
|
* Strips the leading locale segment before comparing so "/vi/dashboard" and
|
|
* "/en/dashboard" both match "/dashboard".
|
|
*/
|
|
function isProtectedPath(pathname: string): boolean {
|
|
// Remove a leading locale segment if present (e.g. "/vi" or "/en").
|
|
const localeSegmentRe = new RegExp(`^\\/(${routing.locales.join("|")})(\\/.*)?(\\?.*)?$`);
|
|
const match = localeSegmentRe.exec(pathname);
|
|
// After stripping: "/vi/dashboard" → "/dashboard"; "/dashboard" stays as-is.
|
|
const stripped = match ? (match[2] ?? "/") : pathname;
|
|
|
|
return PROTECTED_PATH_PREFIXES.some((prefix) => stripped.startsWith(prefix));
|
|
}
|
|
|
|
/**
|
|
* Unified middleware entry point.
|
|
*
|
|
* Request flow:
|
|
* 1. Run Supabase session refresh → writes refreshed sb-* cookies onto BOTH
|
|
* request.cookies (for downstream reads) and a NextResponse (for Set-Cookie).
|
|
* 2. Coarse auth gate: unauthenticated requests to protected paths get a 307
|
|
* redirect to /${locale}/sign-in?next=<original-path>.
|
|
* Supabase cookies are copied onto the redirect response so the browser
|
|
* receives the token delta even on redirect.
|
|
* 3. Hand off to next-intl for locale detection / prefix rewrites.
|
|
* • If next-intl produces a redirect/rewrite (status !== 200 OR x-middleware-rewrite
|
|
* header present), copy Supabase cookies onto it and return it.
|
|
* • Otherwise return the Supabase response directly (cookies already attached).
|
|
*
|
|
* Single response guarantee: only one NextResponse is ever returned per
|
|
* request. Cookie sets from both layers are always merged before returning.
|
|
* Two competing responses would silently drop one set of Set-Cookie headers,
|
|
* appearing as a sign-out on the next request.
|
|
*
|
|
* NOTE: No `export const runtime = 'edge'` — proxy runs on the Node.js runtime
|
|
* in Next.js 16+. Supabase SSR is not certified for the Edge runtime.
|
|
*/
|
|
export default async function proxy(request: NextRequest): Promise<NextResponse> {
|
|
// ── Step 1: Supabase session refresh ─────────────────────────────────────
|
|
const { response: supabaseResponse, user } = await updateSupabaseSession(request);
|
|
|
|
// ── Step 2: Coarse protected-path gate ───────────────────────────────────
|
|
// Always redirects to `/${locale}/sign-in` with NO `?next=` param —
|
|
// the trimmed plan dropped post-login redirect plumbing (original BSK has no
|
|
// URL deep-linking surface; signInAction always lands users on /dashboard).
|
|
if (!user && isProtectedPath(request.nextUrl.pathname)) {
|
|
const locale =
|
|
routing.locales.find((l) => request.nextUrl.pathname.startsWith(`/${l}`)) ??
|
|
routing.defaultLocale;
|
|
|
|
const signInUrl = new URL(`/${locale}/sign-in`, request.url);
|
|
|
|
const redirectResponse = NextResponse.redirect(signInUrl, { status: 307 });
|
|
// Port Supabase cookie deltas onto the redirect so the browser stores them.
|
|
copyCookies(supabaseResponse, redirectResponse);
|
|
return redirectResponse;
|
|
}
|
|
|
|
// ── Step 3: next-intl locale routing ─────────────────────────────────────
|
|
const intlResponse = handleI18nRouting(request);
|
|
|
|
// next-intl produced a redirect (3xx) or a rewrite (x-middleware-rewrite header).
|
|
// In both cases it is a distinct NextResponse — merge Supabase cookies onto it.
|
|
const isRedirect = intlResponse.status >= 300 && intlResponse.status < 400;
|
|
const isRewrite = intlResponse.headers.has("x-middleware-rewrite");
|
|
|
|
if (isRedirect || isRewrite) {
|
|
copyCookies(supabaseResponse, intlResponse);
|
|
return intlResponse;
|
|
}
|
|
|
|
// next-intl returned a plain next() response — the Supabase response already
|
|
// carries the correct cookies (and the request rewrites from step 1).
|
|
return supabaseResponse;
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
|
|
};
|