-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy.ts
More file actions
139 lines (122 loc) · 4.84 KB
/
Copy pathproxy.ts
File metadata and controls
139 lines (122 loc) · 4.84 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
// =============================================================================
// NEXT.JS ROUTE PROTECTION PROXY
// =============================================================================
// Intercepts requests to protected routes and enforces authentication and
// admin-role requirements server-side.
//
// Protected routes:
// /dashboard/* — authenticated users only
// /profile/* — authenticated users only
// /admin/* — authenticated admin users only
// /dashboard/users — authenticated admin users only
//
// Session is stored as an httpOnly cookie. Authentication is verified by
// calling internal API routes (outside the matcher so no circular routing).
// Admin RBAC uses GET /api/auth/session-role so this file avoids direct data
// layer imports and keeps middleware edge-safe via fetch-only I/O.
// =============================================================================
import { NextRequest, NextResponse } from 'next/server';
import { getInternalAppOrigin } from '@/lib/app-port';
import { getSessionCookieName } from '@/lib/auth-session-cookie';
/**
* Verify the session by calling the /api/auth/session route.
* Forwards the incoming cookies so the route can read the session cookie.
* Returns the user object if valid, null otherwise.
*/
async function getSessionUser(request: NextRequest): Promise<{ $id: string } | null> {
try {
const res = await fetch(new URL('/api/auth/session', getInternalAppOrigin()), {
headers: { cookie: request.headers.get('cookie') ?? '' },
});
if (!res.ok) return null;
const user = await res.json();
return user && typeof user.$id === 'string' ? user : null;
} catch {
return null;
}
}
/**
* Session + user_profiles.role in one round trip (for /admin/* only).
* Avoids importing the Tables SDK in middleware.
*/
async function getSessionRoleForAdminGate(
request: NextRequest
): Promise<'admin' | 'user' | 'unauthenticated' | 'error'> {
try {
const res = await fetch(new URL('/api/auth/session-role', getInternalAppOrigin()), {
headers: { cookie: request.headers.get('cookie') ?? '' },
});
if (res.status === 401) return 'unauthenticated';
if (!res.ok) return 'error';
const data = (await res.json()) as { role?: string };
return data.role === 'admin' ? 'admin' : 'user';
} catch {
// Network / JSON parse failures — not a confirmed 401; avoid treating as logged-out
return 'error';
}
}
/**
* Build the original path including query string so redirects preserve
* parameters like ?upgrade=success after login.
*/
function getFullPath(request: NextRequest): string {
const { pathname, search } = request.nextUrl;
return search ? `${pathname}${search}` : pathname;
}
function getSessionTokenFromCookies(request: NextRequest): string | null {
return request.cookies.get(getSessionCookieName())?.value ?? null;
}
function isAdminOnlyDashboardPath(pathname: string): boolean {
return pathname === '/dashboard/users' || pathname.startsWith('/dashboard/users/');
}
export async function proxy(request: NextRequest) {
try {
const { pathname } = request.nextUrl;
const fullPath = getFullPath(request);
const sessionToken = getSessionTokenFromCookies(request);
if (pathname === '/') {
if (!sessionToken) {
return NextResponse.next();
}
const user = await getSessionUser(request);
if (user) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
// No session cookie — redirect to login
if (!sessionToken) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', fullPath);
return NextResponse.redirect(loginUrl);
}
if (pathname.startsWith('/admin') || isAdminOnlyDashboardPath(pathname)) {
const gate = await getSessionRoleForAdminGate(request);
if (gate === 'unauthenticated') {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', fullPath);
return NextResponse.redirect(loginUrl);
}
if (gate !== 'admin') {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
return NextResponse.next();
}
const user = await getSessionUser(request);
if (!user) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', fullPath);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
} catch (error) {
// Fail closed: on error, redirect to login instead of allowing through
const loginUrl = new URL('/login', request.url);
const fullPath = getFullPath(request);
loginUrl.searchParams.set('redirect', fullPath);
return NextResponse.redirect(loginUrl);
}
}
export const config = {
matcher: ['/', '/dashboard/:path*', '/profile/:path*', '/admin/:path*'],
};