-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmiddleware.ts
More file actions
206 lines (181 loc) · 7.86 KB
/
middleware.ts
File metadata and controls
206 lines (181 loc) · 7.86 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import { NextResponse, URLPattern } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from '@/lib/utils';
import apiError from '@/lib/apiError';
import initFirebaseAdmin from '@/lib/fireadmin-init';
import { DecodedIdToken, FirebaseAdmin, FirebaseAuthError } from './lib/firebase-admin';
import db from '@/lib/db';
import { appConfig, OWNER_SETUP_KEY } from '@/drizzle/schema/config';
import { eq } from 'drizzle-orm';
function redirect(req: NextRequest, path: string) {
// see: https://nextjs.org/docs/messages/middleware-relative-urls
return NextResponse.redirect(new URL(path, req.url));
}
const localHostnames = ['localhost', '0.0.0.0', '127.0.0.1'];
let firebaseAdmin: FirebaseAdmin | undefined;
// Helper to check D1 Setup Status
async function checkSetupComplete(): Promise<boolean> {
try {
const dbInstance = await db();
const config = await dbInstance.select({ value: appConfig.value })
.from(appConfig)
.where(eq(appConfig.key, OWNER_SETUP_KEY))
.limit(1);
return config.length > 0 && config[0].value === 'true';
} catch (error) {
console.error("[Middleware] Error checking D1 setup status:", error);
// If the DB check fails (e.g., D1 not provisioned/accessible), assume setup is NOT complete
// to allow the user to potentially reach the setup page.
// Log the error for investigation.
return false;
}
}
export async function middleware(req: NextRequest) {
// Because cloud run terminate TLS to container service which means we can't use https://localhost:{port}. So we need to use `http`. Upto next v13.4.12, it's all good and we can directly use `req.nextUrl.origin` for baseUrl. But after that, next.js has unsolved bug which if we used via proxy (via cloudflare tunnel or in production), it out `req.url` with `https://`.
// I've mentioned this bug here (closed now but not fixed): https://github.com/vercel/next.js/issues/54961
// Watch for these issues:
// <https://github.com/vercel/next.js/issues/54450>
const hostname = req.nextUrl.hostname;
const isLocal = localHostnames.includes(hostname);
const baseUrl = isLocal ? `http://${req.nextUrl.host}` : req.nextUrl.origin;
const pathname = req.nextUrl.pathname;
// production checklist: <https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy>
// Disable iframe (better alt to X-Frame-Options)
const cspHeader = `
frame-ancestors 'self';
`;
// Replace newline characters and spaces
const contentSecurityPolicyHeaderValue = cspHeader.replace(/\s{2,}/g, ' ').trim();
const res = NextResponse.next();
// set security headers
res.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue);
const excludedRoutes = ['/api/hc'].map(
(r) => new URLPattern({ pathname: r, baseURL: baseUrl, search: '*', hash: '*' })
);
// skip these routes.
// Don't use r.test(req.url), otherwise we will not be able to use cloudflare tunnel for testing webhook locally.
// check here: console.log('Pathname: ', pathname, baseUrl, req.url, req.nextUrl.origin);
if (excludedRoutes.some((r) => r.test(pathname, baseUrl))) return res;
console.log(`<Called Middleware: ${req.url}>`);
// --- Initialize Firebase Admin --- Try/catch block for robust error handling
try {
firebaseAdmin = initFirebaseAdmin();
} catch (initError: unknown) {
const message = initError instanceof Error ? initError.message : 'Initialization failed';
return NextResponse.json(
{ error: 'Server configuration error', details: message },
{ status: 500 }
);
}
// token verification
const token = getToken(req.headers);
let decoded: DecodedIdToken | null = null;
let verificationError: Error | null = null;
if (token && firebaseAdmin) {
// Check if admin is initialized
try {
decoded = await firebaseAdmin.verifyIdToken(token); // Set checkRevoked if needed
} catch (error: unknown) {
console.error('[Middleware] Token verification failed:', error);
if (error instanceof FirebaseAuthError) {
verificationError = error;
} else {
verificationError = new Error('Unknown verification error');
}
}
} else if (!token) {
verificationError = new Error('No token provided');
} else {
// firebaseAdmin not initialized
verificationError = new Error('Firebase Admin not initialized');
}
const isTokenValid = !!decoded && !verificationError;
if (pathname.startsWith('/api')) {
if (!isTokenValid) {
const status =
verificationError instanceof FirebaseAuthError &&
(verificationError.code === 'id-token-expired' ||
verificationError.code === 'id-token-revoked')
? 401
: 403;
return apiError({ status: status, message: verificationError?.message || 'Unauthorized' });
}
// skipping email verification for now.
// if (!isEmailVerified) {
// console.log('[Middleware] Token revoked[api] (email not verified): ', decodedToken.uid);
// // Directly revoke tokens if email is not verified
// try {
// await firebaseAdmin.revokeRefreshTokens(decodedToken.uid);
// } catch (revokeErr) {
// console.error('[Middleware] Failed to revoke token during API email check:', revokeErr);
// // Decide if failure to revoke should block the request - likely not
// }
// return apiError({ status: 401, message: 'Email not verified' });
// }
// Add uid
if(decoded) {
res.headers.set('x-uid', decoded.uid);
}
return res;
} else {
// Page routes
const isSetupComplete = await checkSetupComplete(); // Check DB if initial owner setup is done
const isAdminPath = pathname.startsWith('/admin');
const isLoginPage = pathname === '/admin/login';
const isSetupPage = pathname === '/admin/setup';
// --- Handle Setup Incomplete ---
if (!isSetupComplete) {
if (isSetupPage) {
// Allow access to the setup page if setup is not complete
return res;
} else {
// Redirect *any* other request (admin or not) to the setup page
console.log(`[Middleware] Setup incomplete, redirecting ${pathname} to /admin/setup`);
return redirect(req, '/admin/setup');
}
}
// --- Handle Setup Complete ---
// At this point, isSetupComplete is true
// Redirect away from setup page if accessed directly after completion
if (isSetupPage) {
console.log('[Middleware] Setup complete, redirecting away from /admin/setup');
return redirect(req, '/admin');
}
// Handle Authentication for Admin Routes
if (isAdminPath) {
if (isTokenValid) {
// Logged in: Redirect away from login page
if (isLoginPage) {
console.log('[Middleware] Logged in, redirecting away from /admin/login');
return redirect(req, '/admin');
}
// Logged in: Allow access to other admin pages
console.log(`[Middleware] Logged in, allowing access to ${pathname}`);
if (decoded) { // Add uid header if decoded token exists
res.headers.set('x-uid', decoded.uid);
}
return res;
} else {
// Not logged in: Redirect non-login admin paths to login page
if (!isLoginPage) {
console.log(`[Middleware] Not logged in, redirecting ${pathname} to /admin/login`);
return redirect(req, '/admin/login');
}
// Not logged in: Allow access to login page
console.log('[Middleware] Not logged in, allowing access to /admin/login');
return res;
}
}
// Non-admin routes are allowed regardless of login state (if setup is complete)
console.log(`[Middleware] Allowing access to non-admin route: ${pathname}`);
return res;
}
}
export const config = {
matcher: [
'/admin',
'/admin/(.*)',
// see this for double parentheses: https://nextjs.org/docs/messages/invalid-route-source
'/api/(.*)',
],
};