UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

257 lines (212 loc) 8.17 kB
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'; import debug from 'debug'; import { NextRequest, NextResponse } from 'next/server'; import { UAParser } from 'ua-parser-js'; import urlJoin from 'url-join'; import { authEnv } from '@/config/auth'; import { LOBE_LOCALE_COOKIE } from '@/const/locale'; import { LOBE_THEME_APPEARANCE } from '@/const/theme'; import { appEnv } from '@/envs/app'; import NextAuthEdge from '@/libs/next-auth/edge'; import { Locales } from '@/locales/resources'; import { parseBrowserLanguage } from '@/utils/locale'; import { parseDefaultThemeFromCountry } from '@/utils/server/geo'; import { RouteVariants } from '@/utils/server/routeVariants'; import { OAUTH_AUTHORIZED } from './const/auth'; import { oidcEnv } from './envs/oidc'; // Create debug logger instances const logDefault = debug('lobe-middleware:default'); const logNextAuth = debug('lobe-middleware:next-auth'); const logClerk = debug('lobe-middleware:clerk'); // OIDC session pre-sync constant const OIDC_SESSION_HEADER = 'x-oidc-session-sync'; export const config = { matcher: [ // include any files in the api or trpc folders that might have an extension '/(api|trpc|webapi)(.*)', // include the / '/', '/discover', '/discover(.*)', '/chat', '/chat(.*)', '/changelog(.*)', '/settings(.*)', '/files', '/files(.*)', '/repos(.*)', '/profile(.*)', '/me', '/me(.*)', '/login(.*)', '/signup(.*)', '/next-auth/(.*)', '/oauth(.*)', '/oidc(.*)', // ↓ cloud ↓ ], }; const backendApiEndpoints = ['/api', '/trpc', '/webapi', '/oidc']; const defaultMiddleware = (request: NextRequest) => { const url = new URL(request.url); logDefault('Processing request: %s %s', request.method, request.url); // skip all api requests if (backendApiEndpoints.some((path) => url.pathname.startsWith(path))) { logDefault('Skipping API request: %s', url.pathname); return NextResponse.next(); } // 1. Read user preferences from cookies const theme = request.cookies.get(LOBE_THEME_APPEARANCE)?.value || parseDefaultThemeFromCountry(request); // if it's a new user, there's no cookie // So we need to use the fallback language parsed by accept-language const browserLanguage = parseBrowserLanguage(request.headers); const locale = (request.cookies.get(LOBE_LOCALE_COOKIE)?.value || browserLanguage) as Locales; const ua = request.headers.get('user-agent'); const device = new UAParser(ua || '').getDevice(); logDefault('User preferences: %O', { browserLanguage, deviceType: device.type, hasCookies: { locale: !!request.cookies.get(LOBE_LOCALE_COOKIE)?.value, theme: !!request.cookies.get(LOBE_THEME_APPEARANCE)?.value, }, locale, theme, }); // 2. Create normalized preference values const route = RouteVariants.serializeVariants({ isMobile: device.type === 'mobile', locale, theme, }); logDefault('Serialized route variant: %s', route); // if app is in docker, rewrite to self container // https://github.com/lobehub/lobe-chat/issues/5876 if (appEnv.MIDDLEWARE_REWRITE_THROUGH_LOCAL) { logDefault('Local container rewrite enabled: %O', { host: '127.0.0.1', original: url.toString(), port: process.env.PORT || '3210', protocol: 'http', }); url.protocol = 'http'; url.host = '127.0.0.1'; url.port = process.env.PORT || '3210'; } // refs: https://github.com/lobehub/lobe-chat/pull/5866 // new handle segment rewrite: /${route}${originalPathname} // / -> /zh-CN__0__dark // /discover -> /zh-CN__0__dark/discover const nextPathname = `/${route}` + (url.pathname === '/' ? '' : url.pathname); const nextURL = appEnv.MIDDLEWARE_REWRITE_THROUGH_LOCAL ? urlJoin(url.origin, nextPathname) : nextPathname; logDefault('URL rewrite: %O', { isLocalRewrite: appEnv.MIDDLEWARE_REWRITE_THROUGH_LOCAL, nextPathname: nextPathname, nextURL: nextURL, originalPathname: url.pathname, }); url.pathname = nextPathname; return NextResponse.rewrite(url, { status: 200 }); }; const isPublicRoute = createRouteMatcher([ '/api/auth(.*)', '/trpc(.*)', // next auth '/next-auth/(.*)', // clerk '/login', '/signup', ]); const isProtectedRoute = createRouteMatcher([ '/settings(.*)', '/files(.*)', '/onboard(.*)', '/oauth(.*)', // ↓ cloud ↓ ]); // Initialize an Edge compatible NextAuth middleware const nextAuthMiddleware = NextAuthEdge.auth((req) => { logNextAuth('NextAuth middleware processing request: %s %s', req.method, req.url); const response = defaultMiddleware(req); // when enable auth protection, only public route is not protected, others are all protected const isProtected = appEnv.ENABLE_AUTH_PROTECTION ? !isPublicRoute(req) : isProtectedRoute(req); logNextAuth('Route protection status: %s, %s', req.url, isProtected ? 'protected' : 'public'); // Just check if session exists const session = req.auth; // Check if next-auth throws errors // refs: https://github.com/lobehub/lobe-chat/pull/1323 const isLoggedIn = !!session?.expires; logNextAuth('NextAuth session status: %O', { expires: session?.expires, isLoggedIn, userId: session?.user?.id, }); // Remove & amend OAuth authorized header response.headers.delete(OAUTH_AUTHORIZED); if (isLoggedIn) { logNextAuth('Setting auth header: %s = %s', OAUTH_AUTHORIZED, 'true'); response.headers.set(OAUTH_AUTHORIZED, 'true'); // If OIDC is enabled and user is logged in, add OIDC session pre-sync header if (oidcEnv.ENABLE_OIDC && session?.user?.id) { logNextAuth('OIDC session pre-sync: Setting %s = %s', OIDC_SESSION_HEADER, session.user.id); response.headers.set(OIDC_SESSION_HEADER, session.user.id); } } else { // If request a protected route, redirect to sign-in page // ref: https://authjs.dev/getting-started/session-management/protecting if (isProtected) { logNextAuth('Request a protected route, redirecting to sign-in page'); const nextLoginUrl = new URL('/next-auth/signin', req.nextUrl.origin); nextLoginUrl.searchParams.set('callbackUrl', req.nextUrl.href); return Response.redirect(nextLoginUrl); } logNextAuth('Request a free route but not login, allow visit without auth header'); } return response; }); const clerkAuthMiddleware = clerkMiddleware( async (auth, req) => { logClerk('Clerk middleware processing request: %s %s', req.method, req.url); // when enable auth protection, only public route is not protected, others are all protected const isProtected = appEnv.ENABLE_AUTH_PROTECTION ? !isPublicRoute(req) : isProtectedRoute(req); logClerk('Route protection status: %s, %s', req.url, isProtected ? 'protected' : 'public'); if (isProtected) { logClerk('Protecting route: %s', req.url); await auth.protect(); } const response = defaultMiddleware(req); const data = await auth(); logClerk('Clerk auth status: %O', { isSignedIn: !!data.userId, userId: data.userId, }); // If OIDC is enabled and Clerk user is logged in, add OIDC session pre-sync header if (oidcEnv.ENABLE_OIDC && data.userId) { logClerk('OIDC session pre-sync: Setting %s = %s', OIDC_SESSION_HEADER, data.userId); response.headers.set(OIDC_SESSION_HEADER, data.userId); } else if (oidcEnv.ENABLE_OIDC) { logClerk('No Clerk user detected, not setting OIDC session sync header'); } return response; }, { // https://github.com/lobehub/lobe-chat/pull/3084 clockSkewInMs: 60 * 60 * 1000, signInUrl: '/login', signUpUrl: '/signup', }, ); logDefault('Middleware configuration: %O', { enableAuthProtection: appEnv.ENABLE_AUTH_PROTECTION, enableClerk: authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH, enableNextAuth: authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH, enableOIDC: oidcEnv.ENABLE_OIDC, }); export default authEnv.NEXT_PUBLIC_ENABLE_CLERK_AUTH ? clerkAuthMiddleware : authEnv.NEXT_PUBLIC_ENABLE_NEXT_AUTH ? nextAuthMiddleware : defaultMiddleware;