diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index a9b056a..5d51d1b 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,9 +1,10 @@ import { jwtVerify } from "jose"; import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; +import { headers } from 'next/headers' // List of public paths that don't require authentication -const publicPaths = [ +const PUBLIC_PATHS = [ "/sign-in", "/api/auth/session", "/api/user", @@ -31,6 +32,20 @@ interface JWTPayload { exp?: number; } +// Add supported languages +const SUPPORTED_LANGUAGES = ['en', 'fr', 'es', 'de'] + +// Helper to extract primary language from Accept-Language header +function getPrimaryLanguage(acceptLanguage: string | null): string { + if (!acceptLanguage) return 'en' + + // Get first language code (e.g. 'fr-FR,fr;q=0.9,en;q=0.8' -> 'fr') + const primaryLang = acceptLanguage.split(',')[0].split('-')[0].toLowerCase() + + // Return primary language if supported, otherwise default to 'en' + return SUPPORTED_LANGUAGES.includes(primaryLang) ? primaryLang : 'en' +} + // Function to verify JWT token async function verifyToken(token: string) { try { @@ -59,65 +74,144 @@ async function verifyToken(token: string) { } } +// Update the addLanguagePrefix function to be more precise +function addLanguagePrefix(pathname: string, preferredLanguage: string): string { + // Don't add prefix if it's an API route + if (pathname.startsWith('/api/')) { + return pathname + } + + // Don't add prefix if it already has a valid language prefix + for (const lang of SUPPORTED_LANGUAGES) { + if (pathname.startsWith(`/${lang}/`)) { + return pathname + } + } + + // Add the prefix + return `/${preferredLanguage}${pathname.startsWith('/') ? pathname : `/${pathname}`}` +} + +// Update the isPublicPath function to handle both prefixed and unprefixed paths +function isPublicPath(path: string): boolean { + // First check if the path is directly in PUBLIC_PATHS + if (PUBLIC_PATHS.some(publicPath => path.startsWith(publicPath))) { + return true + } + + // Then check if the path without language prefix is in PUBLIC_PATHS + for (const lang of SUPPORTED_LANGUAGES) { + if (path.startsWith(`/${lang}/`)) { + const pathWithoutLang = path.substring(3) + return PUBLIC_PATHS.some(publicPath => pathWithoutLang.startsWith(publicPath)) + } + } + + return false +} + +// Helper to get language from URL or Accept-Language header +function getLanguageFromPath(pathname: string): string | null { + const firstSegment = pathname.split('/')[1] + return SUPPORTED_LANGUAGES.includes(firstSegment) ? firstSegment : null +} + // Middleware function export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; - // Allow public paths - if (publicPaths.some((path) => pathname.startsWith(path))) { - return NextResponse.next(); + // Get language from URL or Accept-Language header + const urlLanguage = getLanguageFromPath(pathname) + const acceptLanguage = request.headers.get('Accept-Language') + const preferredLanguage = urlLanguage || getPrimaryLanguage(acceptLanguage) + + // Create response object that we'll modify + let response = NextResponse.next() + + // Set Weglot language header + response.headers.set('Weglot-Language-Preference', preferredLanguage) + + // Skip language prefix redirect for API routes and already prefixed paths + if (!pathname.startsWith('/api/') && !urlLanguage) { + const newPathname = addLanguagePrefix(pathname, preferredLanguage) + + // Only redirect if the path actually changed + if (newPathname !== pathname) { + const url = new URL(newPathname, request.url) + response = NextResponse.redirect(url) + response.headers.set('Weglot-Language-Preference', preferredLanguage) + return response + } } - // Get session token and registration status + // Allow public paths (both with and without language prefix) + if (isPublicPath(pathname)) { + return response + } + + // Rest of your middleware logic... const sessionToken = request.cookies.get("session")?.value; const registrationStatus = request.cookies.get("registration_status")?.value; - // For all protected routes if (!sessionToken) { - return NextResponse.redirect(new URL("/sign-in", request.url)); + const signInUrl = new URL(addLanguagePrefix("/sign-in", preferredLanguage), request.url) + response = NextResponse.redirect(signInUrl) + response.headers.set('Weglot-Language-Preference', preferredLanguage) + return response } try { - const decoded = await verifyToken(sessionToken); + const decoded = await verifyToken(sessionToken) if (!decoded) { - const response = NextResponse.redirect(new URL("/sign-in", request.url)); - response.cookies.delete("session"); - response.cookies.delete("registration_status"); - return response; + const signInUrl = new URL(addLanguagePrefix("/sign-in", preferredLanguage), request.url) + response = NextResponse.redirect(signInUrl) + response.cookies.delete("session") + response.cookies.delete("registration_status") + response.headers.set('Weglot-Language-Preference', preferredLanguage) + return response } // Handle registration flow if (pathname !== "/register" && registrationStatus !== "complete") { - const url = new URL("/register", request.url); + const registerUrl = new URL(addLanguagePrefix("/register", preferredLanguage), request.url) if (decoded.walletAddress) { - url.searchParams.set("walletAddress", decoded.walletAddress); + registerUrl.searchParams.set("walletAddress", decoded.walletAddress) } - return NextResponse.redirect(url); + response = NextResponse.redirect(registerUrl) + response.headers.set('Weglot-Language-Preference', preferredLanguage) + return response } // Add user info to request headers for API routes if (pathname.startsWith("/api/")) { - const requestHeaders = new Headers(request.headers); - requestHeaders.set("x-user-id", decoded.sub as string); - requestHeaders.set("x-wallet-address", decoded.walletAddress as string); + const requestHeaders = new Headers(request.headers) + requestHeaders.set("x-user-id", decoded.sub as string) + requestHeaders.set("x-wallet-address", decoded.walletAddress as string) + requestHeaders.set("x-preferred-language", preferredLanguage) return NextResponse.next({ request: { headers: requestHeaders, }, - }); + }) } - return NextResponse.next(); + return response + } catch { - // Clear invalid session - const response = NextResponse.redirect(new URL("/sign-in", request.url)); - response.cookies.delete("session"); - response.cookies.delete("registration_status"); - return response; + const signInUrl = new URL(addLanguagePrefix("/sign-in", preferredLanguage), request.url) + response = NextResponse.redirect(signInUrl) + response.cookies.delete("session") + response.cookies.delete("registration_status") + response.headers.set('Weglot-Language-Preference', preferredLanguage) + return response } } +// Update matcher to include language prefixes export const config = { - matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"], + matcher: [ + "/((?!_next/static|_next/image|favicon.ico).*)", + "/:lang(en|fr|es|de)/:path*" + ] };