Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
70 lines
2.6 KiB
TypeScript
70 lines
2.6 KiB
TypeScript
// apps/admin/src/middleware.ts
|
|
// D3RO Voice — Industrial Grade Admin Route & Security Guard
|
|
|
|
import { NextResponse, type NextRequest } from 'next/server'
|
|
|
|
const PUBLIC_PATHS = ['/login', '/auth/callback', '/api/auth/login', '/api/auth/logout', '/favicon.ico', '/robots.txt']
|
|
|
|
export async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
const { pathname } = request.nextUrl
|
|
|
|
// 1. Check if path is public (e.g. login, static assets)
|
|
const isPublic = PUBLIC_PATHS.some((path) => pathname === path || pathname.startsWith(path + '/'))
|
|
const isStatic = pathname.startsWith('/_next') || pathname.startsWith('/static') || pathname.includes('.')
|
|
|
|
// 2. Validate session cookie
|
|
const sessionCookie = request.cookies.get('d3ro_admin_session')?.value
|
|
let isAuthenticated = false
|
|
|
|
if (sessionCookie) {
|
|
try {
|
|
const decoded = JSON.parse(Buffer.from(sessionCookie, 'base64').toString('utf-8'))
|
|
if (decoded && decoded.expiresAt && decoded.expiresAt > Date.now()) {
|
|
isAuthenticated = true
|
|
}
|
|
} catch {
|
|
isAuthenticated = false
|
|
}
|
|
}
|
|
|
|
// 3. Unauthenticated access to protected route -> Redirect to /login
|
|
if (!isAuthenticated && !isPublic && !isStatic) {
|
|
const loginUrl = new URL('/login', request.url)
|
|
if (pathname !== '/') {
|
|
loginUrl.searchParams.set('redirect', pathname)
|
|
}
|
|
const redirectResponse = NextResponse.redirect(loginUrl)
|
|
addSecurityHeaders(redirectResponse)
|
|
return redirectResponse
|
|
}
|
|
|
|
// 4. Authenticated user visiting /login -> Redirect to Dashboard /
|
|
if (isAuthenticated && pathname === '/login') {
|
|
const dashboardUrl = new URL('/', request.url)
|
|
const redirectResponse = NextResponse.redirect(dashboardUrl)
|
|
addSecurityHeaders(redirectResponse)
|
|
return redirectResponse
|
|
}
|
|
|
|
// 5. Proceed with Security Headers attached
|
|
const response = NextResponse.next({ request: { headers: request.headers } })
|
|
addSecurityHeaders(response)
|
|
return response
|
|
}
|
|
|
|
function addSecurityHeaders(response: NextResponse): void {
|
|
// Anti-Crawling & Anti-Reconnaissance (Shodan, Google, Bing, AI scrapers)
|
|
response.headers.set('X-Robots-Tag', 'noindex, nofollow, noarchive, nosnippet, noimageindex')
|
|
// Clickjacking Prevention
|
|
response.headers.set('X-Frame-Options', 'DENY')
|
|
// MIME Sniffing Prevention
|
|
response.headers.set('X-Content-Type-Options', 'nosniff')
|
|
// Referrer Privacy
|
|
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
|
|
// Feature Policy
|
|
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()')
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
|
|
}
|