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 all 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
146 changes: 120 additions & 26 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,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
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;

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