-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy.ts
More file actions
159 lines (136 loc) · 5.29 KB
/
proxy.ts
File metadata and controls
159 lines (136 loc) · 5.29 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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { auth } from "@/auth";
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { NextRequest, NextResponse } from 'next/server';
// Routes that don't require authentication
const publicRoutes = ["/", "/login", "/register", "/forgot-password", "/reset-password", "/api/auth", "/api/register"];
// Locale configuration
const locales = ['en', 'cs'] as const;
const defaultLocale = 'en';
const cookieName = 'NEXT_LOCALE';
function getLocale(request: NextRequest): string {
// 1. Check cookie first (user preference)
const cookieLocale = request.cookies.get(cookieName)?.value;
if (cookieLocale && locales.includes(cookieLocale as typeof locales[number])) {
return cookieLocale;
}
// 2. Check Accept-Language header (browser/system preference)
const acceptLanguage = request.headers.get('accept-language');
if (acceptLanguage) {
try {
const headers = { 'accept-language': acceptLanguage };
const languages = new Negotiator({ headers }).languages();
return match(languages, [...locales], defaultLocale);
} catch {
// If matching fails, fall through to default
}
}
// 3. Default to English
return defaultLocale;
}
export default auth((req) => {
const { pathname } = req.nextUrl;
const isPublicRoute = publicRoutes.some((route) => pathname.startsWith(route));
// Protect admin routes - only superadmins can access /admin/*
if (pathname.startsWith('/admin')) {
if (!req.auth?.user?.isSuperadmin) {
const url = req.nextUrl.clone();
url.pathname = "/login";
url.searchParams.set('callbackUrl', pathname);
return Response.redirect(url);
}
}
// League routes: require authentication (membership check in layout)
const leagueRouteMatch = pathname.match(/^\/(\d+)(\/|$)/);
if (leagueRouteMatch) {
if (!req.auth?.user?.id) {
const url = req.nextUrl.clone();
url.pathname = "/login";
url.searchParams.set('callbackUrl', pathname);
return Response.redirect(url);
}
}
// Root route handling - let the page handle redirect to league or available leagues
// Admins can access user view via the root route, and can switch to admin via the menu
// Default: require authentication for non-public routes
if (!req.auth && !isPublicRoute) {
const url = req.nextUrl.clone();
url.pathname = "/login";
url.searchParams.set('callbackUrl', pathname);
return Response.redirect(url);
}
// ===== CSRF PROTECTION =====
// Verify origin for state-changing requests (POST, PUT, DELETE, PATCH)
// Skip CSRF for cron API routes (they authenticate via Bearer token)
const isCronRoute = pathname.startsWith('/api/cron/')
if (
!isCronRoute &&
(req.method === 'POST' ||
req.method === 'PUT' ||
req.method === 'DELETE' ||
req.method === 'PATCH')
) {
const origin = req.headers.get('origin');
const referer = req.headers.get('referer');
const host = req.headers.get('host');
if (host) {
const allowedOrigins = [
`https://${host}`,
`http://${host}`,
...(process.env.ALLOWED_ORIGINS?.split(',').map(o => o.trim()) || []),
];
// Check origin header (sent by modern browsers)
if (origin && !allowedOrigins.some((allowed) => allowed === origin)) {
console.warn(`[CSRF] Blocked request from origin: ${origin}`);
return new Response('CSRF validation failed', { status: 403 });
}
// Check referer header as additional validation
if (referer) {
try {
const refererUrl = new URL(referer);
const refererOrigin = `${refererUrl.protocol}//${refererUrl.host}`;
if (!allowedOrigins.some((allowed) => allowed === refererOrigin)) {
console.warn(`[CSRF] Blocked request from referer: ${referer}`);
return new Response('CSRF validation failed', { status: 403 });
}
} catch {
console.warn(`[CSRF] Invalid referer header: ${referer}`);
return new Response('CSRF validation failed', { status: 403 });
}
}
// Block state-changing requests that have neither origin nor referer
if (!origin && !referer) {
console.warn('[CSRF] Blocked request without origin or referer');
return new Response('CSRF validation failed', { status: 403 });
}
}
}
// ===== LOCALE DETECTION =====
// Detect user's preferred locale
const locale = getLocale(req as unknown as NextRequest);
// Set locale in request headers for Server Components to access
const requestHeaders = new Headers(req.headers);
requestHeaders.set('x-locale', locale);
// Create response with modified headers
const response = NextResponse.next({
request: {
headers: requestHeaders,
},
});
// Set locale cookie if not already set or different
const currentCookie = req.cookies.get(cookieName)?.value;
if (currentCookie !== locale) {
response.cookies.set(cookieName, locale, {
maxAge: 365 * 24 * 60 * 60, // 1 year
path: '/',
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
});
}
return response;
});
export const config = {
// Exclude static files and PWA assets from middleware processing
matcher: ["/((?!_next/static|_next/image|favicon.ico|sw.js|manifest.json|icons/).*)"],
};
export const preferredRegion = "auto";