@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.
@@ -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
- });