@xenterprises/fastify-xconfig 1.1.9 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +189 -0
- package/README.md +127 -135
- package/package.json +16 -24
- package/server/app.js +9 -46
- package/src/lifecycle/xFastifyAfter.js +4 -4
- package/src/middleware/cors.js +6 -1
- package/src/utils/health.js +10 -11
- package/src/xConfig.js +1 -29
- package/test/index.js +6 -6
- package/test/xConfig.test.js +278 -0
- package/xConfigReference.js +103 -1505
- package/src/auth/admin.js +0 -185
- package/src/auth/portal.js +0 -179
- package/src/integrations/cloudinary.js +0 -98
- package/src/integrations/geocode.js +0 -43
- package/src/integrations/sendgrid.js +0 -58
- package/src/integrations/twilio.js +0 -146
- package/xConfigWorkingList.js +0 -720
package/xConfigWorkingList.js
DELETED
|
@@ -1,720 +0,0 @@
|
|
|
1
|
-
import fp from "fastify-plugin";
|
|
2
|
-
import jwt from "@fastify/jwt";
|
|
3
|
-
import bcrypt from "bcrypt";
|
|
4
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
5
|
-
import Stripe from 'stripe';
|
|
6
|
-
/*
|
|
7
|
-
===== SET VARS =====
|
|
8
|
-
Setting variables for the config
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/*
|
|
13
|
-
===== SET MAIN FUNCTION AND OPTIONS =====
|
|
14
|
-
Setting variables for the config
|
|
15
|
-
*/
|
|
16
|
-
async function xConfig(fastify, options) {
|
|
17
|
-
const {
|
|
18
|
-
professional = false,
|
|
19
|
-
fancyErrors = true,
|
|
20
|
-
prisma: prismaOptions = {},
|
|
21
|
-
bugsnag: bugsnagOptions = {},
|
|
22
|
-
stripe: stripeOptions = {},
|
|
23
|
-
sendGrid: sendGridOptions = {},
|
|
24
|
-
twilio: twilioOptions = {},
|
|
25
|
-
cloudinary: cloudinaryOptions = {},
|
|
26
|
-
auth: authOptions = {},
|
|
27
|
-
cors: corsOptions = {},
|
|
28
|
-
underPressure: underPressureOptions = {},
|
|
29
|
-
multipart: multipartOptions = {},
|
|
30
|
-
rateLimit: rateLimitOptions = {},
|
|
31
|
-
geocode: geocodeOptions = {},
|
|
32
|
-
} = options;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// /*
|
|
39
|
-
// ===== STRIPE =====
|
|
40
|
-
// */
|
|
41
|
-
// if (stripeOptions.active === true) {
|
|
42
|
-
// if (!stripeOptions.apiKey) throw new Error("Stripe API key must be provided.");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
47
|
-
// apiVersion: '2022-11-15',
|
|
48
|
-
// });
|
|
49
|
-
|
|
50
|
-
// const stripePlugin = async function (fastify, options) {
|
|
51
|
-
// fastify.decorate('stripe', {
|
|
52
|
-
// // Create a Payment Intent (for processing payments)
|
|
53
|
-
// createPaymentIntent: async (amount, currency = 'usd', customerId) => {
|
|
54
|
-
// try {
|
|
55
|
-
// const paymentIntent = await stripeClient.paymentIntents.create({
|
|
56
|
-
// amount,
|
|
57
|
-
// currency,
|
|
58
|
-
// customer: customerId,
|
|
59
|
-
// });
|
|
60
|
-
// return paymentIntent;
|
|
61
|
-
// } catch (error) {
|
|
62
|
-
// fastify.log.error('Failed to create payment intent:', error);
|
|
63
|
-
// throw new Error('Failed to create payment intent.');
|
|
64
|
-
// }
|
|
65
|
-
// },
|
|
66
|
-
|
|
67
|
-
// // Create a Customer
|
|
68
|
-
// createCustomer: async (email, name, paymentMethodId) => {
|
|
69
|
-
// try {
|
|
70
|
-
// const customer = await stripeClient.customers.create({
|
|
71
|
-
// email,
|
|
72
|
-
// name,
|
|
73
|
-
// payment_method: paymentMethodId,
|
|
74
|
-
// invoice_settings: {
|
|
75
|
-
// default_payment_method: paymentMethodId,
|
|
76
|
-
// },
|
|
77
|
-
// });
|
|
78
|
-
// return customer;
|
|
79
|
-
// } catch (error) {
|
|
80
|
-
// fastify.log.error('Failed to create customer:', error);
|
|
81
|
-
// throw new Error('Failed to create customer.');
|
|
82
|
-
// }
|
|
83
|
-
// },
|
|
84
|
-
|
|
85
|
-
// // Create a Subscription (recurring payments)
|
|
86
|
-
// createSubscription: async (customerId, priceId) => {
|
|
87
|
-
// try {
|
|
88
|
-
// const subscription = await stripeClient.subscriptions.create({
|
|
89
|
-
// customer: customerId,
|
|
90
|
-
// items: [{ price: priceId }],
|
|
91
|
-
// expand: ['latest_invoice.payment_intent'],
|
|
92
|
-
// });
|
|
93
|
-
// return subscription;
|
|
94
|
-
// } catch (error) {
|
|
95
|
-
// fastify.log.error('Failed to create subscription:', error);
|
|
96
|
-
// throw new Error('Failed to create subscription.');
|
|
97
|
-
// }
|
|
98
|
-
// },
|
|
99
|
-
|
|
100
|
-
// // Retrieve Payment Details
|
|
101
|
-
// retrievePayment: async (paymentIntentId) => {
|
|
102
|
-
// try {
|
|
103
|
-
// const paymentIntent = await stripeClient.paymentIntents.retrieve(paymentIntentId);
|
|
104
|
-
// return paymentIntent;
|
|
105
|
-
// } catch (error) {
|
|
106
|
-
// fastify.log.error('Failed to retrieve payment intent:', error);
|
|
107
|
-
// throw new Error('Failed to retrieve payment intent.');
|
|
108
|
-
// }
|
|
109
|
-
// },
|
|
110
|
-
|
|
111
|
-
// // Create an Invoice
|
|
112
|
-
// createInvoice: async (customerId, items) => {
|
|
113
|
-
// try {
|
|
114
|
-
// const invoiceItemPromises = items.map(async item => {
|
|
115
|
-
// return stripeClient.invoiceItems.create({
|
|
116
|
-
// customer: customerId,
|
|
117
|
-
// price: item.priceId, // Assuming you have the price ID
|
|
118
|
-
// });
|
|
119
|
-
// });
|
|
120
|
-
// await Promise.all(invoiceItemPromises);
|
|
121
|
-
|
|
122
|
-
// const invoice = await stripeClient.invoices.create({
|
|
123
|
-
// customer: customerId,
|
|
124
|
-
// auto_advance: true, // Automatically finalize the invoice
|
|
125
|
-
// });
|
|
126
|
-
// return invoice;
|
|
127
|
-
// } catch (error) {
|
|
128
|
-
// fastify.log.error('Failed to create invoice:', error);
|
|
129
|
-
// throw new Error('Failed to create invoice.');
|
|
130
|
-
// }
|
|
131
|
-
// },
|
|
132
|
-
|
|
133
|
-
// // Handle Webhooks (e.g., payment success, failed, etc.)
|
|
134
|
-
// handleWebhook: async (req, res, signature, endpointSecret) => {
|
|
135
|
-
// let event;
|
|
136
|
-
// try {
|
|
137
|
-
// event = stripeClient.webhooks.constructEvent(
|
|
138
|
-
// req.rawBody, // Stripe requires the raw body for webhooks
|
|
139
|
-
// signature,
|
|
140
|
-
// endpointSecret
|
|
141
|
-
// );
|
|
142
|
-
// } catch (error) {
|
|
143
|
-
// fastify.log.error('Failed to verify webhook signature:', error);
|
|
144
|
-
// throw new Error('Webhook signature verification failed.');
|
|
145
|
-
// }
|
|
146
|
-
|
|
147
|
-
// // Process the event (e.g., payment success, subscription created)
|
|
148
|
-
// return event;
|
|
149
|
-
// },
|
|
150
|
-
// });
|
|
151
|
-
|
|
152
|
-
// console.info(" ✅ Stripe Enabled");
|
|
153
|
-
// }
|
|
154
|
-
// }
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
/*
|
|
158
|
-
===== AUTHENTICATION =====
|
|
159
|
-
Admin and User authentication are handled separately and redundantly,
|
|
160
|
-
sharing no code between them.
|
|
161
|
-
*/
|
|
162
|
-
|
|
163
|
-
/*
|
|
164
|
-
===== Admin Authentication =====
|
|
165
|
-
*/
|
|
166
|
-
/*
|
|
167
|
-
===== Admin Authentication =====
|
|
168
|
-
*/
|
|
169
|
-
if (authOptions.admin?.active !== false) {
|
|
170
|
-
|
|
171
|
-
// Ensure the admin JWT secret is provided
|
|
172
|
-
if (!authOptions.admin.secret) {
|
|
173
|
-
throw new Error("Admin JWT secret must be provided.");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const adminAuthOptions = authOptions.admin;
|
|
177
|
-
const adminCookieName =
|
|
178
|
-
adminAuthOptions.cookieOptions?.name || "adminToken";
|
|
179
|
-
const adminRefreshCookieName =
|
|
180
|
-
adminAuthOptions.cookieOptions?.refreshTokenName || "adminRefreshToken";
|
|
181
|
-
const adminCookieOptions = {
|
|
182
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
183
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
184
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
185
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
186
|
-
};
|
|
187
|
-
const adminExcludedPaths = adminAuthOptions.excludedPaths || [
|
|
188
|
-
"/admin/auth/login",
|
|
189
|
-
"/admin/auth/logout",
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
// Decorator to hash admin passwords
|
|
193
|
-
async function hashAdminPassword(password) {
|
|
194
|
-
const saltRounds = 10; // Number of salt rounds for bcrypt (10 is generally a good default)
|
|
195
|
-
try {
|
|
196
|
-
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
|
197
|
-
return hashedPassword;
|
|
198
|
-
} catch (error) {
|
|
199
|
-
throw new Error("Failed to hash password: " + error.message);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
fastify.decorate("hashAdminPassword", hashAdminPassword);
|
|
204
|
-
|
|
205
|
-
// Register JWT for admin
|
|
206
|
-
await fastify.register(jwt, {
|
|
207
|
-
secret: adminAuthOptions.secret,
|
|
208
|
-
sign: { algorithm: 'HS256', expiresIn: adminAuthOptions.expiresIn || "15m" },
|
|
209
|
-
cookie: {
|
|
210
|
-
cookieName: adminCookieName,
|
|
211
|
-
signed: false,
|
|
212
|
-
},
|
|
213
|
-
namespace: "adminJwt",
|
|
214
|
-
jwtVerify: "adminJwtVerify",
|
|
215
|
-
jwtSign: "adminJwtSign",
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Common function to set tokens as cookies
|
|
219
|
-
const setAdminAuthCookies = (reply, accessToken, refreshToken) => {
|
|
220
|
-
reply.setCookie(adminCookieName, accessToken, adminCookieOptions);
|
|
221
|
-
reply.setCookie(adminRefreshCookieName, refreshToken, {
|
|
222
|
-
// ...adminCookieOptions,
|
|
223
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
224
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
225
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
226
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
227
|
-
});
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Admin authentication hook
|
|
231
|
-
fastify.addHook("onRequest", async (request, reply) => {
|
|
232
|
-
const url = request.url;
|
|
233
|
-
|
|
234
|
-
// Skip authentication for excluded paths
|
|
235
|
-
if (adminExcludedPaths.some((path) => url.startsWith(path))) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (url.startsWith("/admin")) {
|
|
240
|
-
try {
|
|
241
|
-
// Extract token from cookie or Authorization header
|
|
242
|
-
const authHeader = request.headers.authorization;
|
|
243
|
-
const authToken =
|
|
244
|
-
authHeader && authHeader.startsWith("Bearer ")
|
|
245
|
-
? authHeader.slice(7)
|
|
246
|
-
: null;
|
|
247
|
-
const token = request.cookies[adminCookieName] || authToken;
|
|
248
|
-
|
|
249
|
-
if (!token) {
|
|
250
|
-
throw fastify.httpErrors.unauthorized(
|
|
251
|
-
"Admin access token not provided"
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Verify access token
|
|
256
|
-
const decoded = await request.adminJwtVerify(token);
|
|
257
|
-
request.adminAuth = decoded; // Attach admin auth context
|
|
258
|
-
} catch (err) {
|
|
259
|
-
// Use built-in HTTP error handling
|
|
260
|
-
reply.send(
|
|
261
|
-
fastify.httpErrors.unauthorized("Invalid or expired access token")
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
// Admin login route
|
|
268
|
-
fastify.post("/admin/auth/login", async (req, reply) => {
|
|
269
|
-
try {
|
|
270
|
-
const { email, password } = req.body;
|
|
271
|
-
|
|
272
|
-
// Validate input
|
|
273
|
-
if (!email || !password) {
|
|
274
|
-
throw fastify.httpErrors.badRequest(
|
|
275
|
-
"Email and password are required"
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Fetch admin from the database
|
|
280
|
-
const admin = await fastify.prisma.admins.findUnique({
|
|
281
|
-
where: { email },
|
|
282
|
-
});
|
|
283
|
-
if (!admin) {
|
|
284
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Compare passwords using bcrypt
|
|
288
|
-
const isValidPassword = await bcrypt.compare(password, admin.password);
|
|
289
|
-
if (!isValidPassword) {
|
|
290
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Issue access token
|
|
294
|
-
const accessToken = await reply.adminJwtSign({ id: admin.id });
|
|
295
|
-
|
|
296
|
-
// Generate refresh token
|
|
297
|
-
const refreshToken = randomUUID();
|
|
298
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
299
|
-
|
|
300
|
-
// Store hashed refresh token in the database
|
|
301
|
-
await fastify.prisma.admins.update({
|
|
302
|
-
where: { id: admin.id },
|
|
303
|
-
data: { refreshToken: hashedRefreshToken },
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
// Set tokens as cookies
|
|
307
|
-
setAdminAuthCookies(reply, accessToken, refreshToken);
|
|
308
|
-
|
|
309
|
-
reply.send({ accessToken });
|
|
310
|
-
} catch (err) {
|
|
311
|
-
reply.send(err);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// Admin refresh token route
|
|
316
|
-
fastify.post("/admin/auth/refresh", async (req, reply) => {
|
|
317
|
-
try {
|
|
318
|
-
const adminAuth = req.adminAuth;
|
|
319
|
-
const refreshToken = req.cookies[adminRefreshCookieName];
|
|
320
|
-
if (!refreshToken) {
|
|
321
|
-
throw fastify.httpErrors.unauthorized("Refresh token not provided");
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Fetch admin from the database using the refresh token
|
|
325
|
-
const admin = await fastify.prisma.admins.findFirst({
|
|
326
|
-
where: { id: adminAuth.id, refreshToken: { not: null } },
|
|
327
|
-
});
|
|
328
|
-
if (!admin) {
|
|
329
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Verify the refresh token
|
|
333
|
-
const isValid = await bcrypt.compare(refreshToken, admin.refreshToken);
|
|
334
|
-
if (!isValid) {
|
|
335
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Issue new access token
|
|
339
|
-
const accessToken = await reply.adminJwtSign({ id: admin.id });
|
|
340
|
-
|
|
341
|
-
// Generate new refresh token
|
|
342
|
-
const newRefreshToken = randomUUID();
|
|
343
|
-
const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
|
|
344
|
-
|
|
345
|
-
// Update refresh token in the database
|
|
346
|
-
await fastify.prisma.admins.update({
|
|
347
|
-
where: { id: admin.id },
|
|
348
|
-
data: { refreshToken: hashedNewRefreshToken },
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// Set new tokens as cookies
|
|
352
|
-
setAdminAuthCookies(reply, accessToken, newRefreshToken);
|
|
353
|
-
|
|
354
|
-
reply.send({ accessToken });
|
|
355
|
-
} catch (err) {
|
|
356
|
-
reply.send(err);
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Admin logout route
|
|
361
|
-
fastify.post("/admin/auth/logout", async (req, reply) => {
|
|
362
|
-
try {
|
|
363
|
-
const adminAuth = req.adminAuth;
|
|
364
|
-
if (adminAuth) {
|
|
365
|
-
// Delete refresh token from the database
|
|
366
|
-
await fastify.prisma.admins.update({
|
|
367
|
-
where: { id: adminAuth.id },
|
|
368
|
-
data: { refreshToken: null },
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Clear cookies
|
|
373
|
-
reply.clearCookie(adminCookieName, { path: "/" });
|
|
374
|
-
reply.clearCookie(adminRefreshCookieName, { path: "/" });
|
|
375
|
-
|
|
376
|
-
reply.send({ message: "Logged out successfully" });
|
|
377
|
-
} catch (err) {
|
|
378
|
-
reply.send(err);
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// Admin authentication status route
|
|
383
|
-
fastify.get("/admin/auth/me", async (req, reply) => {
|
|
384
|
-
try {
|
|
385
|
-
const adminAuth = req.adminAuth;
|
|
386
|
-
|
|
387
|
-
// Fetch admin details from database
|
|
388
|
-
const admin = await fastify.prisma.admins.findUnique({
|
|
389
|
-
where: { id: adminAuth.id },
|
|
390
|
-
select: { id: true, firstName: true, lastName: true, email: true },
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
if (!admin) {
|
|
394
|
-
throw fastify.httpErrors.notFound("Admin not found");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
reply.send(admin);
|
|
398
|
-
} catch (err) {
|
|
399
|
-
reply.send(err);
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
console.info(" ✅ Auth Admin Enabled");
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/*
|
|
407
|
-
===== User Authentication =====
|
|
408
|
-
*/
|
|
409
|
-
if (authOptions.user?.active !== false) {
|
|
410
|
-
// Ensure the user JWT secret is provided
|
|
411
|
-
if (!authOptions.user.secret) {
|
|
412
|
-
throw new Error("User JWT secret must be provided.");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const userAuthOptions = authOptions.user;
|
|
416
|
-
const userCookieName = userAuthOptions.cookieOptions?.name || "userToken";
|
|
417
|
-
const userRefreshCookieName =
|
|
418
|
-
userAuthOptions.cookieOptions?.refreshTokenName || "userRefreshToken";
|
|
419
|
-
const userCookieOptions = {
|
|
420
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
421
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
422
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
423
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
424
|
-
};
|
|
425
|
-
const userExcludedPaths = userAuthOptions.excludedPaths || [
|
|
426
|
-
"/portal/auth/login",
|
|
427
|
-
"/portal/auth/logout",
|
|
428
|
-
"/portal/auth/register",
|
|
429
|
-
];
|
|
430
|
-
|
|
431
|
-
// Register JWT for user
|
|
432
|
-
await fastify.register(jwt, {
|
|
433
|
-
secret: userAuthOptions.secret,
|
|
434
|
-
sign: { algorithm: 'HS256', expiresIn: userAuthOptions.expiresIn || "15m" },
|
|
435
|
-
cookie: { cookieName: userCookieName, signed: false },
|
|
436
|
-
namespace: "userJwt",
|
|
437
|
-
jwtVerify: "userJwtVerify",
|
|
438
|
-
jwtSign: "userJwtSign",
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
// Common function to set tokens as cookies
|
|
442
|
-
const setAuthCookies = (reply, accessToken, refreshToken) => {
|
|
443
|
-
reply.setCookie(userCookieName, accessToken, userCookieOptions);
|
|
444
|
-
reply.setCookie(userRefreshCookieName, refreshToken, {
|
|
445
|
-
// ...userCookieOptions,
|
|
446
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
447
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
448
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
449
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
450
|
-
});
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
// User authentication hook
|
|
454
|
-
fastify.addHook("onRequest", async (request, reply) => {
|
|
455
|
-
const url = request.url;
|
|
456
|
-
|
|
457
|
-
// Skip authentication for excluded paths
|
|
458
|
-
if (userExcludedPaths.some((path) => url.startsWith(path))) {
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if (url.startsWith("/portal")) {
|
|
463
|
-
try {
|
|
464
|
-
// Extract token from cookie or Authorization header
|
|
465
|
-
const authHeader = request.headers.authorization;
|
|
466
|
-
const authToken =
|
|
467
|
-
authHeader && authHeader.startsWith("Bearer ")
|
|
468
|
-
? authHeader.slice(7)
|
|
469
|
-
: null;
|
|
470
|
-
const token = request.cookies[userCookieName] || authToken;
|
|
471
|
-
|
|
472
|
-
if (!token) {
|
|
473
|
-
throw fastify.httpErrors.unauthorized(
|
|
474
|
-
"User access token not provided"
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Verify access token using the namespaced verify method
|
|
479
|
-
const decoded = await request.userJwtVerify(token);
|
|
480
|
-
request.userAuth = decoded; // Attach user auth context
|
|
481
|
-
} catch (err) {
|
|
482
|
-
// Use built-in HTTP error handling
|
|
483
|
-
reply.send(
|
|
484
|
-
fastify.httpErrors.unauthorized("Invalid or expired access token")
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
// User registration route
|
|
491
|
-
fastify.post("/portal/auth/register", async (req, reply) => {
|
|
492
|
-
try {
|
|
493
|
-
const { email, password, firstName, lastName } = req.body;
|
|
494
|
-
|
|
495
|
-
// Validate input
|
|
496
|
-
if (!email || !password || !firstName || !lastName) {
|
|
497
|
-
throw fastify.httpErrors.badRequest("Missing required fields");
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Check if user already exists
|
|
501
|
-
const existingUser = await fastify.prisma.users.findUnique({
|
|
502
|
-
where: { email },
|
|
503
|
-
});
|
|
504
|
-
if (existingUser) {
|
|
505
|
-
throw fastify.httpErrors.conflict("Email already in use");
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Hash the password
|
|
509
|
-
const hashedPassword = await bcrypt.hash(password, 10);
|
|
510
|
-
|
|
511
|
-
// Create the user
|
|
512
|
-
const user = await fastify.prisma.users.create({
|
|
513
|
-
data: {
|
|
514
|
-
email,
|
|
515
|
-
password: hashedPassword,
|
|
516
|
-
firstName,
|
|
517
|
-
lastName,
|
|
518
|
-
},
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
// Send welcome email
|
|
522
|
-
await fastify.sendGrid.sendEmail(email, auth?.user?.registerEmail?.subject, auth?.user?.registerEmail?.templateId, { name: firstName, email });
|
|
523
|
-
|
|
524
|
-
// Issue access token
|
|
525
|
-
const accessToken = await reply.userJwtSign({ id: user.id });
|
|
526
|
-
|
|
527
|
-
// Generate refresh token
|
|
528
|
-
const refreshToken = randomUUID();
|
|
529
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
530
|
-
|
|
531
|
-
// Store hashed refresh token in the database
|
|
532
|
-
await fastify.prisma.users.update({
|
|
533
|
-
where: { id: user.id },
|
|
534
|
-
data: { refreshToken: hashedRefreshToken },
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
// Set tokens as cookies
|
|
538
|
-
setAuthCookies(reply, accessToken, refreshToken);
|
|
539
|
-
|
|
540
|
-
reply.send({ accessToken });
|
|
541
|
-
} catch (err) {
|
|
542
|
-
reply.send(err);
|
|
543
|
-
}
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
// User login route
|
|
547
|
-
fastify.post("/portal/auth/login", async (req, reply) => {
|
|
548
|
-
try {
|
|
549
|
-
const { email, password } = req.body;
|
|
550
|
-
|
|
551
|
-
if (!email || !password) {
|
|
552
|
-
throw fastify.httpErrors.badRequest(
|
|
553
|
-
"Email and password are required"
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Fetch user from the database
|
|
558
|
-
const user = await fastify.prisma.users.findUnique({
|
|
559
|
-
where: { email },
|
|
560
|
-
});
|
|
561
|
-
if (!user) {
|
|
562
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Compare passwords using bcrypt
|
|
566
|
-
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
567
|
-
if (!isValidPassword) {
|
|
568
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Issue access token
|
|
572
|
-
const accessToken = await reply.userJwtSign({ id: user.id });
|
|
573
|
-
|
|
574
|
-
// Generate refresh token
|
|
575
|
-
const refreshToken = randomUUID();
|
|
576
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
577
|
-
|
|
578
|
-
// Store hashed refresh token in the database
|
|
579
|
-
await fastify.prisma.users.update({
|
|
580
|
-
where: { id: user.id },
|
|
581
|
-
data: { refreshToken: hashedRefreshToken },
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Set tokens as cookies
|
|
585
|
-
setAuthCookies(reply, accessToken, refreshToken);
|
|
586
|
-
|
|
587
|
-
reply.send({ accessToken });
|
|
588
|
-
} catch (err) {
|
|
589
|
-
reply.send(err);
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
// User refresh token route
|
|
594
|
-
fastify.post("/portal/auth/refresh", async (req, reply) => {
|
|
595
|
-
try {
|
|
596
|
-
const userAuth = req.userAuth;
|
|
597
|
-
const refreshToken = req.cookies[userRefreshCookieName];
|
|
598
|
-
if (!refreshToken) {
|
|
599
|
-
throw fastify.httpErrors.unauthorized("Refresh token not provided");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Fetch user from the database using the refresh token
|
|
603
|
-
const user = await fastify.prisma.users.findFirst({
|
|
604
|
-
where: { id: userAuth?.id, refreshToken: { not: null } },
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
if (!user) {
|
|
608
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Verify the refresh token
|
|
612
|
-
const isValid = await bcrypt.compare(refreshToken, user.refreshToken);
|
|
613
|
-
if (!isValid) {
|
|
614
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Issue new access token
|
|
618
|
-
const accessToken = await reply.userJwtSign({ id: user.id });
|
|
619
|
-
|
|
620
|
-
// Generate new refresh token
|
|
621
|
-
const newRefreshToken = randomUUID();
|
|
622
|
-
const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
|
|
623
|
-
|
|
624
|
-
// Update refresh token in the database
|
|
625
|
-
await fastify.prisma.users.update({
|
|
626
|
-
where: { id: user.id },
|
|
627
|
-
data: { refreshToken: hashedNewRefreshToken },
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
// Set new tokens as cookies
|
|
631
|
-
setAuthCookies(reply, accessToken, newRefreshToken);
|
|
632
|
-
|
|
633
|
-
reply.send({ accessToken });
|
|
634
|
-
} catch (err) {
|
|
635
|
-
reply.send(err);
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
// User logout route
|
|
640
|
-
fastify.post("/portal/auth/logout", async (req, reply) => {
|
|
641
|
-
try {
|
|
642
|
-
const userAuth = req.userAuth;
|
|
643
|
-
if (userAuth) {
|
|
644
|
-
// Delete refresh token from the database
|
|
645
|
-
await fastify.prisma.users.update({
|
|
646
|
-
where: { id: userAuth.id },
|
|
647
|
-
data: { refreshToken: null },
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Clear cookies
|
|
652
|
-
reply.clearCookie(userCookieName, { path: "/" });
|
|
653
|
-
reply.clearCookie(userRefreshCookieName, { path: "/" });
|
|
654
|
-
|
|
655
|
-
reply.send({ message: "Logged out successfully" });
|
|
656
|
-
} catch (err) {
|
|
657
|
-
reply.send(err);
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
// User authentication status route
|
|
662
|
-
fastify.get("/portal/auth/me", async (req, reply) => {
|
|
663
|
-
try {
|
|
664
|
-
const userAuth = req.userAuth;
|
|
665
|
-
|
|
666
|
-
// Fetch user details from database
|
|
667
|
-
const user = await fastify.prisma.users.findUnique({
|
|
668
|
-
where: { id: userAuth.id },
|
|
669
|
-
select: { id: true, email: true, firstName: true, lastName: true, ...authOptions.user.me },
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
if (!user) {
|
|
673
|
-
throw fastify.httpErrors.notFound("User not found");
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
reply.send(user);
|
|
677
|
-
} catch (err) {
|
|
678
|
-
reply.send(err);
|
|
679
|
-
}
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
console.info(" ✅ Auth User Enabled");
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/*
|
|
686
|
-
===== GEOCODE =====
|
|
687
|
-
*/
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
/*
|
|
693
|
-
===== LIST ROUTES AFTER ALL PLUGINS =====
|
|
694
|
-
Use the after() method to ensure this runs after all plugins are registered.
|
|
695
|
-
*/
|
|
696
|
-
fastify.after(() => {
|
|
697
|
-
if (professional !== true) {
|
|
698
|
-
console.info(" ✅ Listing Routes:");
|
|
699
|
-
fastify.ready(() => {
|
|
700
|
-
printRoutes(routes, options.colors !== false);
|
|
701
|
-
// Add rocket emoji
|
|
702
|
-
console.info(
|
|
703
|
-
`🚀 Server is ready on port ${process.env.PORT || 3000}\n\n`
|
|
704
|
-
);
|
|
705
|
-
// Add goodbye emoji for server shutting down
|
|
706
|
-
fastify.addHook("onClose", () =>
|
|
707
|
-
console.info("Server shutting down... Goodbye 👋")
|
|
708
|
-
);
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
export default fp(xConfig, {
|
|
719
|
-
name: "xConfig",
|
|
720
|
-
});
|