Skip to content
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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 95 additions & 23 deletions apps/web/src/middleware.ts
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'
Copy link
Contributor

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getLanguageFromPath(pathname: string): string | null {
const firstSegment = pathname.split('/')[1]
return SUPPORTED_LANGUAGES.includes(firstSegment) ? firstSegment : null
}
function getLanguageFromPath(pathname: string): string | null {
if (!pathname || pathname === '/') return 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;
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}`}`
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const addLanguagePrefix = (url: string): string => {
if (urlLanguage || SUPPORTED_LANGUAGES.some(lang => url.startsWith(`/${lang}/`))) {
return url
}
return `/${preferredLanguage}${url.startsWith('/') ? url : `/${url}`}`
}
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}`}`
}


// 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
Expand All @@ -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*"
]
};
Loading