-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add: weglot header to middleware #159
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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' | ||||||||||||||||||||||
} | ||||||||||||||||||||||
bitfalt marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
||||||||||||||||||||||
// 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 | ||||||||||||||||||||||
} | ||||||||||||||||||||||
Comment on lines
+114
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add validation for malformed paths. The function should handle edge cases like empty paths or paths with multiple segments. function getLanguageFromPath(pathname: string): string | null {
+ if (!pathname || pathname === '/') return null
+
const firstSegment = pathname.split('/')[1]
return SUPPORTED_LANGUAGES.includes(firstSegment) ? firstSegment : null
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||
|
||||||||||||||||||||||
// 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*" | ||||||||||||||||||||||
] | ||||||||||||||||||||||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Remove unused import.
The
headers
import from 'next/headers' is not used in the code.-import { headers } from 'next/headers'
📝 Committable suggestion