-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmiddleware.ts
More file actions
82 lines (67 loc) · 2.76 KB
/
Copy pathmiddleware.ts
File metadata and controls
82 lines (67 loc) · 2.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { NextRequest, NextResponse } from 'next/server';
import { settingsPersistence, verifyPassword } from '@/lib/settingsPersistence';
export const runtime = 'nodejs';
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
const LOCALHOST_ADDRS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1', 'localhost']);
export function middleware(request: NextRequest) {
const settings = settingsPersistence.loadSettingsSync();
// Determine if this is a local request
const ip =
request.headers.get('x-forwarded-for')?.split(',')[0].trim() ||
request.headers.get('x-real-ip') ||
'';
const isLocal = !ip || LOCALHOST_ADDRS.has(ip);
// Localhost requests always pass through
if (isLocal) {
return NextResponse.next();
}
// External request — block if localhostOnly
if (settings.localhostOnly) {
return new NextResponse('Forbidden', { status: 403 });
}
// No credentials configured — block external access even if localhostOnly is off
if (!settings.authUsername || !settings.authPasswordHash) {
return new NextResponse('Forbidden', { status: 403 });
}
// External connections allowed with credentials — require BASIC auth
const pathname = request.nextUrl.pathname;
// Auth utility endpoints and login page bypass auth
if (pathname.startsWith('/api/auth/') || pathname === '/login') {
return NextResponse.next();
}
// All other routes — validate BASIC auth
const authHeader = request.headers.get('authorization');
if (!authHeader || !authHeader.startsWith('Basic ')) {
const accept = request.headers.get('accept') || '';
if (accept.includes('text/html')) {
// Browser page navigation → redirect to login page
return NextResponse.redirect(new URL('/login', request.url));
}
// XHR/fetch → 401 with WWW-Authenticate so the browser's challenge-response
// can cache credentials at this path level (critical for root-level caching)
return new NextResponse('Unauthorized', {
status: 401,
headers: {
'WWW-Authenticate': 'Basic realm="Fury"',
'Cache-Control': 'no-cache, no-transform',
},
});
}
const decoded = Buffer.from(authHeader.slice(6), 'base64').toString();
const separatorIndex = decoded.indexOf(':');
if (separatorIndex === -1) {
return new NextResponse('Unauthorized', { status: 401 });
}
const username = decoded.slice(0, separatorIndex);
const password = decoded.slice(separatorIndex + 1);
if (
username.toLowerCase() !== settings.authUsername.toLowerCase() ||
!verifyPassword(password, settings.authPasswordHash)
) {
// Wrong credentials — no WWW-Authenticate (don't trigger native dialog)
return new NextResponse('Unauthorized', { status: 403 });
}
return NextResponse.next();
}