84 lines
3.2 KiB
TypeScript
84 lines
3.2 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
import { jwtVerify } from 'jose';
|
|
|
|
const CUSTOMER_SESSION_COOKIE = 'customer_session';
|
|
const PUBLIC_ACCOUNT_PATHS = ['/account/login', '/account/register', '/account/forgot-password', '/account/reset-password'];
|
|
|
|
const ADMIN_SESSION_COOKIE = 'admin_session';
|
|
const PUBLIC_ADMIN_PATHS = ['/admin/login'];
|
|
|
|
// Protects everything under /admin with the shop owner's own login session —
|
|
// entirely separate cookie and secret from the customer auth below, so
|
|
// logging into one never grants access to the other.
|
|
async function checkAdminAuth(request: NextRequest, requestHeaders: Headers) {
|
|
if (PUBLIC_ADMIN_PATHS.includes(request.nextUrl.pathname)) {
|
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
}
|
|
|
|
const token = request.cookies.get(ADMIN_SESSION_COOKIE)?.value;
|
|
const secret = process.env.ADMIN_SESSION_SECRET;
|
|
|
|
if (token && secret) {
|
|
try {
|
|
await jwtVerify(token, new TextEncoder().encode(secret));
|
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
} catch {
|
|
// fall through to redirect
|
|
}
|
|
}
|
|
|
|
const loginUrl = new URL('/admin/login', request.url);
|
|
loginUrl.searchParams.set('next', request.nextUrl.pathname);
|
|
return NextResponse.redirect(loginUrl);
|
|
}
|
|
|
|
// Protects everything under /account (except the login/register/reset pages
|
|
// themselves) with a customer's own login session — entirely separate cookie
|
|
// and secret from the admin auth above, so one never grants access to the other.
|
|
async function checkCustomerAuth(request: NextRequest, requestHeaders: Headers) {
|
|
if (PUBLIC_ACCOUNT_PATHS.includes(request.nextUrl.pathname)) {
|
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
}
|
|
|
|
const token = request.cookies.get(CUSTOMER_SESSION_COOKIE)?.value;
|
|
const secret = process.env.SESSION_SECRET;
|
|
|
|
if (token && secret) {
|
|
try {
|
|
await jwtVerify(token, new TextEncoder().encode(secret));
|
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
} catch {
|
|
// fall through to redirect
|
|
}
|
|
}
|
|
|
|
const loginUrl = new URL('/account/login', request.url);
|
|
loginUrl.searchParams.set('next', request.nextUrl.pathname);
|
|
return NextResponse.redirect(loginUrl);
|
|
}
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// Expose the path to server components (the root layout reads x-pathname to
|
|
// decide whether to show the maintenance page). Edge middleware can't read the
|
|
// DB, so the maintenance flag itself is checked in the layout, not here.
|
|
const requestHeaders = new Headers(request.headers);
|
|
requestHeaders.set('x-pathname', pathname);
|
|
|
|
if (pathname.startsWith('/admin')) {
|
|
return checkAdminAuth(request, requestHeaders);
|
|
}
|
|
if (pathname.startsWith('/account')) {
|
|
return checkCustomerAuth(request, requestHeaders);
|
|
}
|
|
return NextResponse.next({ request: { headers: requestHeaders } });
|
|
}
|
|
|
|
export const config = {
|
|
// Run on everything except static assets, so the x-pathname header is set on
|
|
// every page. The auth branches above still only gate /admin and /account.
|
|
matcher: ['/((?!_next/static|_next/image|favicon.ico|logo.png).*)'],
|
|
};
|