Some checks failed
Create and publish Docker images / prepare (push) Has been cancelled
Version Docs on Tag / version-docs (push) Has been cancelled
Create and publish Docker images / build (amd64, linux/amd64, ubuntu-24.04) (push) Has been cancelled
Create and publish Docker images / build (arm64, linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images / merge (push) Has been cancelled
146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
/**
|
|
* Better Auth session cookie name (default prefix + session_token).
|
|
* @see https://www.better-auth.com/docs/concepts/session-management
|
|
*/
|
|
const SESSION_COOKIE = 'better-auth.session_token';
|
|
const SECURE_SESSION_COOKIE = '__Secure-better-auth.session_token';
|
|
const SESSION_COOKIE_NAMES = [SESSION_COOKIE, SECURE_SESSION_COOKIE];
|
|
const US_COUNTRY_CODE = 'US';
|
|
const COUNTRY_HEADER_NAMES = [
|
|
'x-vercel-ip-country',
|
|
'cf-ipcountry',
|
|
'x-country-code',
|
|
'x-geo-country',
|
|
];
|
|
|
|
/**
|
|
* Routes that never require a session cookie.
|
|
* Static assets and Next.js internals are excluded via the matcher config below.
|
|
*/
|
|
const PUBLIC_PATH_PREFIXES = [
|
|
'/api/auth', // Better Auth endpoints (sign-in, sign-up, callbacks, etc.)
|
|
'/signin',
|
|
'/signup',
|
|
'/privacy',
|
|
];
|
|
|
|
function isPublicPath(pathname: string): boolean {
|
|
// Root landing page
|
|
if (pathname === '/') return true;
|
|
|
|
return PUBLIC_PATH_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
|
}
|
|
|
|
function isAuthEnabled(): boolean {
|
|
return !!(process.env.AUTH_SECRET && process.env.BASE_URL);
|
|
}
|
|
|
|
function isAnonymousAuthEnabled(): boolean {
|
|
if (!isAuthEnabled()) return false;
|
|
const raw = process.env.USE_ANONYMOUS_AUTH_SESSIONS;
|
|
return raw?.trim().toLowerCase() === 'true';
|
|
}
|
|
|
|
function isRichardrDevProductionInstance(): boolean {
|
|
return process.env.RICHARDRDEV_PRODUCTION?.trim().toLowerCase() === 'true';
|
|
}
|
|
|
|
function getCountryCodeFromHeaders(request: NextRequest): string | null {
|
|
for (const headerName of COUNTRY_HEADER_NAMES) {
|
|
const rawValue = request.headers.get(headerName);
|
|
if (!rawValue) continue;
|
|
|
|
const normalized = rawValue.trim().toUpperCase();
|
|
if (normalized) {
|
|
return normalized;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function middleware(request: NextRequest) {
|
|
if (isRichardrDevProductionInstance()) {
|
|
const countryCode = getCountryCodeFromHeaders(request);
|
|
const isUnitedStatesRequest = countryCode === US_COUNTRY_CODE;
|
|
|
|
// Strict region gate for the official production instance.
|
|
if (!isUnitedStatesRequest) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
if (pathname.startsWith('/api/')) {
|
|
return NextResponse.json(
|
|
{ error: 'OpenReader is only available in the United States.' },
|
|
{ status: 451, headers: { 'Cache-Control': 'no-store' } },
|
|
);
|
|
}
|
|
|
|
return new NextResponse('OpenReader is only available in the United States.', {
|
|
status: 451,
|
|
headers: {
|
|
'Cache-Control': 'no-store',
|
|
'Content-Type': 'text/plain; charset=utf-8',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// When auth is disabled entirely, let everything through.
|
|
if (!isAuthEnabled()) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// Fast-path redirect for signed-in users hitting the public landing page.
|
|
// This avoids extra server work in the landing page render path.
|
|
if (pathname === '/' && request.nextUrl.searchParams.get('redirect') !== 'false') {
|
|
const hasSession = SESSION_COOKIE_NAMES.some((name) => request.cookies.has(name));
|
|
if (hasSession) {
|
|
const appUrl = request.nextUrl.clone();
|
|
appUrl.pathname = '/app';
|
|
appUrl.search = '';
|
|
return NextResponse.redirect(appUrl);
|
|
}
|
|
}
|
|
|
|
// Public routes are always accessible.
|
|
if (isPublicPath(pathname)) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// When anonymous auth is enabled, unauthenticated users need to reach
|
|
// the page so AuthLoader.tsx can bootstrap an anonymous session client-side.
|
|
if (isAnonymousAuthEnabled()) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// Check for the presence of a session cookie.
|
|
const hasSession = SESSION_COOKIE_NAMES.some((name) => request.cookies.has(name));
|
|
|
|
if (!hasSession) {
|
|
// API routes get a 401 instead of a redirect.
|
|
if (pathname.startsWith('/api/')) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
// Page routes redirect to sign-in.
|
|
const signInUrl = request.nextUrl.clone();
|
|
signInUrl.pathname = '/signin';
|
|
return NextResponse.redirect(signInUrl);
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
/**
|
|
* Match all routes except static assets and Next.js internals.
|
|
*/
|
|
export const config = {
|
|
matcher: [
|
|
'/((?!_next/static|_next/image|favicon\\.ico|icon\\.png|icon\\.svg|apple-icon\\.png|manifest\\.json).*)',
|
|
],
|
|
};
|