Eliminate all code paths, configuration, and documentation related to running without authentication. Require AUTH_SECRET and BASE_URL at startup, updating middleware, server logic, and runtime checks to assume auth is always enabled. Simplify onboarding, settings, and test helpers to reflect mandatory auth. Update environment examples, Docker and deployment docs, and CI/test configs. Remove no-auth-specific UI flows, test cases, and feature toggles.
140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
import { getRequiredAuthEnv } from '@/lib/server/auth/config';
|
|
|
|
/**
|
|
* 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 isAnonymousAuthEnabled(): boolean {
|
|
getRequiredAuthEnv();
|
|
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',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
getRequiredAuthEnv();
|
|
|
|
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).*)',
|
|
],
|
|
};
|