import { NextRequest, NextResponse } from 'next/server' import { config as appConfig } from './lib/config' const AUTH_COOKIE_NAME = 'auth_token' const SECRET_KEY = appConfig.auth.secret // Helper to generate a random nonce function generateNonce() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) } // Helper to create a signed token (simplified JWT-like structure) async function createToken(username: string) { const header = { alg: 'HS256', typ: 'JWT' } const payload = { sub: username, iat: Date.now(), exp: Date.now() + 24 * 60 * 60 * 1000, // 24 hours nonce: generateNonce() // Ensure token is different every time } const encodedHeader = btoa(JSON.stringify(header)) const encodedPayload = btoa(JSON.stringify(payload)) const data = `${encodedHeader}.${encodedPayload}` const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(appConfig.auth.secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] ) const signature = await crypto.subtle.sign( 'HMAC', key, new TextEncoder().encode(data) ) // Convert signature to base64 const signatureArray = Array.from(new Uint8Array(signature)) const encodedSignature = btoa(String.fromCharCode.apply(null, signatureArray)) return `${data}.${encodedSignature}` } // Helper to verify token async function verifyToken(token: string) { try { const parts = token.split('.') if (parts.length !== 3) return false const [encodedHeader, encodedPayload, encodedSignature] = parts const data = `${encodedHeader}.${encodedPayload}` const key = await crypto.subtle.importKey( 'raw', new TextEncoder().encode(SECRET_KEY), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'] ) const signature = new Uint8Array( atob(encodedSignature).split('').map(c => c.charCodeAt(0)) ) const isValid = await crypto.subtle.verify( 'HMAC', key, signature, new TextEncoder().encode(data) ) if (!isValid) return false const payload = JSON.parse(atob(encodedPayload)) if (payload.exp < Date.now()) return false return true } catch (e) { return false } } export async function middleware(req: NextRequest) { const url = new URL(req.url) // Skip auth for certain paths (same logic as original Express app) if ( url.pathname.startsWith('/api/') || url.pathname.startsWith('/screenshots/') || url.pathname.startsWith('/downloads/') || url.pathname.startsWith('/manifest.json') || url.pathname.startsWith('/sw.js') || url.pathname.includes('favicon.png') || url.pathname.includes('install') || url.pathname.includes('WinupdateCore') || req.method === 'POST' || url.pathname.startsWith('/_next/') || // Next.js static files url.pathname.startsWith('/api-test') // API test page (for development) ) { return NextResponse.next() } // 1. Try Cookie Authentication const authCookie = req.cookies.get(AUTH_COOKIE_NAME) if (authCookie) { const isValid = await verifyToken(authCookie.value) if (isValid) { return NextResponse.next() } } // 2. Fallback to Basic Auth const authHeader = req.headers.get('authorization') if (!authHeader) { return new NextResponse('Authentication required', { status: 401, headers: { 'WWW-Authenticate': 'Basic realm="Restricted Access"' } }) } try { // Use atob for Edge compatibility const authValue = authHeader.split(' ')[1] const auth = atob(authValue) const [username, password] = auth.split(':') if (username === appConfig.auth.username && password === appConfig.auth.password) { const response = NextResponse.next() // Set auth cookie on successful Basic Auth const token = await createToken(username) response.cookies.set(AUTH_COOKIE_NAME, token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24, // 1 day path: '/' }) return response } else { return new NextResponse('Authentication failed', { status: 401, headers: { 'WWW-Authenticate': 'Basic realm="Restricted Access"' } }) } } catch { return new NextResponse('Invalid authentication', { status: 401, headers: { 'WWW-Authenticate': 'Basic realm="Restricted Access"' } }) } } // 配置中间件匹配的路径 export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - api (API routes) * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) */ '/((?!_next/static|_next/image|favicon.ico|manifest.json|sw.js).*)', ], }