git-subtree-dir: estore git-subtree-mainline: 746868d720b9be1003a2f783b7a12d526d8eea60 git-subtree-split: e34dfc397c94025670baa2b73b482c01f3033a6a
61 lines
1.9 KiB
TypeScript
61 lines
1.9 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import type { NextRequest } from 'next/server'
|
|
|
|
const COOKIE = 'admin_token'
|
|
|
|
/** Constant-time string comparison to prevent timing attacks */
|
|
function safeEqual(a: string, b: string): boolean {
|
|
if (a.length !== b.length) return false
|
|
let diff = 0
|
|
for (let i = 0; i < a.length; i++) {
|
|
diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
|
|
}
|
|
return diff === 0
|
|
}
|
|
|
|
/**
|
|
* Derive a session token from the admin password using SHA-256.
|
|
* The raw password is never stored in the cookie.
|
|
*/
|
|
async function deriveSessionToken(password: string): Promise<string> {
|
|
const data = new TextEncoder().encode(`admin-session-v1:${password}`)
|
|
const hash = await crypto.subtle.digest('SHA-256', data)
|
|
return Array.from(new Uint8Array(hash))
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('')
|
|
}
|
|
|
|
export async function middleware(request: NextRequest) {
|
|
const { pathname } = request.nextUrl
|
|
|
|
if (pathname === '/shop/admin/login' || pathname === '/api/admin/login') {
|
|
return NextResponse.next()
|
|
}
|
|
|
|
if (pathname.startsWith('/shop/admin') || pathname.startsWith('/api/admin')) {
|
|
const token = request.cookies.get(COOKIE)?.value
|
|
const password = process.env.ADMIN_PASSWORD
|
|
|
|
if (!password || !token) {
|
|
if (pathname.startsWith('/api/')) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
return NextResponse.redirect(new URL('/shop/admin/login', request.url))
|
|
}
|
|
|
|
const expected = await deriveSessionToken(password)
|
|
if (!safeEqual(token, expected)) {
|
|
if (pathname.startsWith('/api/')) {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
return NextResponse.redirect(new URL('/shop/admin/login', request.url))
|
|
}
|
|
}
|
|
|
|
return NextResponse.next()
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ['/shop/admin/:path*', '/api/admin/:path*'],
|
|
}
|