-
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 3 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,13 +74,55 @@ async function verifyToken(token: string) { | |||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Helper to check if path is public (including language prefixes) | ||||||||||||||||||||||||||||||
function isPublicPath(path: string): boolean { | ||||||||||||||||||||||||||||||
// Remove language prefix if it exists | ||||||||||||||||||||||||||||||
const pathWithoutLang = SUPPORTED_LANGUAGES.some(lang => path.startsWith(`/${lang}/`)) | ||||||||||||||||||||||||||||||
? path.substring(3) // Remove /{lang}/ prefix | ||||||||||||||||||||||||||||||
: path | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return PUBLIC_PATHS.some(publicPath => pathWithoutLang.startsWith(publicPath)) | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// 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; | ||||||||||||||||||||||||||||||
const { pathname, search } = request.nextUrl; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// 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) | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Function to add language prefix to URL if needed | ||||||||||||||||||||||||||||||
const addLanguagePrefix = (url: string): string => { | ||||||||||||||||||||||||||||||
if (urlLanguage || SUPPORTED_LANGUAGES.some(lang => url.startsWith(`/${lang}/`))) { | ||||||||||||||||||||||||||||||
return url | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
return `/${preferredLanguage}${url.startsWith('/') ? url : `/${url}`}` | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
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 URL validation in language prefix function. The function should validate URLs before manipulation to prevent errors with malformed URLs. const addLanguagePrefix = (url: string): string => {
+ if (!url) return `/${preferredLanguage}`
+
if (urlLanguage || SUPPORTED_LANGUAGES.some(lang => url.startsWith(`/${lang}/`))) {
return url
}
return `/${preferredLanguage}${url.startsWith('/') ? url : `/${url}`}`
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Redirect to language-prefixed URL if no language prefix exists | ||||||||||||||||||||||||||||||
if (!urlLanguage && !pathname.startsWith('/api/')) { | ||||||||||||||||||||||||||||||
const url = new URL(request.url) | ||||||||||||||||||||||||||||||
url.pathname = addLanguagePrefix(pathname) | ||||||||||||||||||||||||||||||
return NextResponse.redirect(url) | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Allow public paths | ||||||||||||||||||||||||||||||
if (publicPaths.some((path) => pathname.startsWith(path))) { | ||||||||||||||||||||||||||||||
return NextResponse.next(); | ||||||||||||||||||||||||||||||
if (isPublicPath(pathname)) { | ||||||||||||||||||||||||||||||
return response | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// Get session token and registration status | ||||||||||||||||||||||||||||||
|
@@ -74,50 +131,65 @@ export async function middleware(request: NextRequest) { | |||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
// For all protected routes | ||||||||||||||||||||||||||||||
if (!sessionToken) { | ||||||||||||||||||||||||||||||
return NextResponse.redirect(new URL("/sign-in", request.url)); | ||||||||||||||||||||||||||||||
const signInUrl = new URL(addLanguagePrefix("/sign-in"), 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"), 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"), 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"), 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