@xenterprises/fastify-xconfig 0.0.10 → 1.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.
Files changed (99) hide show
  1. package/.github/workflows/ci.yml +19 -0
  2. package/.taprc +3 -0
  3. package/README.md +18 -134
  4. package/index.d.ts +13 -0
  5. package/index.js +9 -0
  6. package/package.json +33 -39
  7. package/test/index.test-d.ts +13 -0
  8. package/test/index.test.js +14 -0
  9. package/test/xConfig.js +115 -0
  10. package/tsconfig.json +9 -0
  11. package/dist/integrations/cloudinary.d.ts +0 -1
  12. package/dist/integrations/cloudinary.js +0 -25
  13. package/dist/integrations/cloudinary.js.map +0 -1
  14. package/dist/integrations/prisma.d.ts +0 -1
  15. package/dist/integrations/prisma.js +0 -13
  16. package/dist/integrations/prisma.js.map +0 -1
  17. package/dist/integrations/sendgrid.d.ts +0 -1
  18. package/dist/integrations/sendgrid.js +0 -22
  19. package/dist/integrations/sendgrid.js.map +0 -1
  20. package/dist/integrations/stripe.d.ts +0 -1
  21. package/dist/integrations/stripe.js +0 -15
  22. package/dist/integrations/stripe.js.map +0 -1
  23. package/dist/integrations/twilio.d.ts +0 -1
  24. package/dist/integrations/twilio.js +0 -17
  25. package/dist/integrations/twilio.js.map +0 -1
  26. package/dist/middleware/bugsnag.d.ts +0 -2
  27. package/dist/middleware/bugsnag.js +0 -9
  28. package/dist/middleware/bugsnag.js.map +0 -1
  29. package/dist/middleware/cors.d.ts +0 -2
  30. package/dist/middleware/cors.js +0 -11
  31. package/dist/middleware/cors.js.map +0 -1
  32. package/dist/middleware/errorHandler.d.ts +0 -2
  33. package/dist/middleware/errorHandler.js +0 -19
  34. package/dist/middleware/errorHandler.js.map +0 -1
  35. package/dist/middleware/multipart.d.ts +0 -2
  36. package/dist/middleware/multipart.js +0 -7
  37. package/dist/middleware/multipart.js.map +0 -1
  38. package/dist/middleware/rateLimit.d.ts +0 -2
  39. package/dist/middleware/rateLimit.js +0 -7
  40. package/dist/middleware/rateLimit.js.map +0 -1
  41. package/dist/middleware/underPressure.d.ts +0 -2
  42. package/dist/middleware/underPressure.js +0 -7
  43. package/dist/middleware/underPressure.js.map +0 -1
  44. package/dist/utils/colorize.d.ts +0 -4
  45. package/dist/utils/colorize.js +0 -33
  46. package/dist/utils/colorize.js.map +0 -1
  47. package/dist/utils/formatBytes.d.ts +0 -1
  48. package/dist/utils/formatBytes.js +0 -10
  49. package/dist/utils/formatBytes.js.map +0 -1
  50. package/dist/utils/randomUUID.d.ts +0 -1
  51. package/dist/utils/randomUUID.js +0 -3
  52. package/dist/utils/randomUUID.js.map +0 -1
  53. package/dist/utils/statAsync.d.ts +0 -2
  54. package/dist/utils/statAsync.js +0 -4
  55. package/dist/utils/statAsync.js.map +0 -1
  56. package/dist/xConfig.d.ts +0 -3
  57. package/dist/xConfig.js +0 -9
  58. package/dist/xConfig.js.map +0 -1
  59. package/server/app.js +0 -92
  60. package/src/auth/admin.js +0 -241
  61. package/src/auth/portal.js +0 -286
  62. package/src/integrations/cloudinary.js +0 -98
  63. package/src/integrations/geocode.js +0 -43
  64. package/src/integrations/prisma.js +0 -30
  65. package/src/integrations/sendgrid.js +0 -58
  66. package/src/integrations/twilio.js +0 -146
  67. package/src/lifecycle/xFastifyAfter.js +0 -27
  68. package/src/middleware/bugsnag.js +0 -10
  69. package/src/middleware/cors.js +0 -10
  70. package/src/middleware/fancyErrors.js +0 -26
  71. package/src/middleware/multipart.js +0 -6
  72. package/src/middleware/rateLimit.js +0 -6
  73. package/src/middleware/underPressure.js +0 -6
  74. package/src/utils/colorize.js +0 -37
  75. package/src/utils/cookie.js +0 -5
  76. package/src/utils/formatBytes.js +0 -16
  77. package/src/utils/health.js +0 -126
  78. package/src/utils/xEcho.js +0 -12
  79. package/src/utils/xSlugify.js +0 -20
  80. package/src/utils/xUUID.js +0 -14
  81. package/src/xConfig.js +0 -117
  82. package/test/index.js +0 -17
  83. package/ts-reference/integrations/cloudinary.ts +0 -26
  84. package/ts-reference/integrations/prisma.ts +0 -13
  85. package/ts-reference/integrations/sendgrid.ts +0 -27
  86. package/ts-reference/integrations/stripe.ts +0 -15
  87. package/ts-reference/integrations/twilio.ts +0 -20
  88. package/ts-reference/middleware/bugsnag.ts +0 -10
  89. package/ts-reference/middleware/cors.ts +0 -13
  90. package/ts-reference/middleware/errorHandler.ts +0 -24
  91. package/ts-reference/middleware/multipart.ts +0 -8
  92. package/ts-reference/middleware/rateLimit.ts +0 -8
  93. package/ts-reference/middleware/underPressure.ts +0 -11
  94. package/ts-reference/utils/colorize.ts +0 -45
  95. package/ts-reference/utils/formatBytes.ts +0 -8
  96. package/ts-reference/utils/randomUUID.ts +0 -3
  97. package/ts-reference/utils/statAsync.ts +0 -4
  98. package/xConfigReference.js +0 -1495
  99. package/xConfigWorkingList.js +0 -720
@@ -1,1495 +0,0 @@
1
- /**
2
- * *xConfig - Fastify configuration plugin for setting up middleware, services, and route handling.*
3
- *
4
- * This plugin provides a comprehensive setup for configuring various services such as Prisma, SendGrid,
5
- * Twilio, Cloudinary, CORS, rate limiting, authentication, and more within a Fastify instance.
6
- * It centralizes configuration, allowing for easy customization and scaling.
7
- *
8
- * *Key Features:*
9
- * - Handles CORS, rate-limiting, multipart handling, and error handling out of the box.
10
- * - Integrates with third-party services such as SendGrid, Twilio, Cloudinary, Stripe, and Bugsnag.
11
- * - Provides authentication setup for both Admin and User, with JWT-based token handling.
12
- * - Includes Prisma database connection and gracefully handles disconnecting on server shutdown.
13
- * - Adds health check routes and resource usage monitoring, with environment validation.
14
- * - Customizes routes listing and applies various performance and security options.
15
- *
16
- * *Usage:*
17
- * This plugin should be registered in your Fastify instance with custom options provided
18
- * for each service. It ensures proper initialization of services like SendGrid, Twilio, and Prisma.
19
- *
20
- * *Example:*
21
- * ```typescript
22
- * import Fastify from 'fastify';
23
- * import xConfig from './path/to/xConfig';
24
- *
25
- * const fastify = Fastify();
26
- * fastify.register(xConfig, {
27
- * prisma: { active: true },
28
- * sendGrid: { apiKey: 'your-sendgrid-api-key' },
29
- * twilio: { accountSid: 'your-account-sid', authToken: 'your-auth-token', phoneNumber: 'your-twilio-number' },
30
- * cloudinary: { cloudName: 'your-cloud-name', apiKey: 'your-api-key', apiSecret: 'your-api-secret' },
31
- * auth: {
32
- * admin: { secret: 'admin-jwt-secret', expiresIn: '1h' },
33
- * user: { secret: 'user-jwt-secret', expiresIn: '1h' },
34
- * },
35
- * cors: { origin: ['https://your-frontend.com'], credentials: true },
36
- * });
37
- *
38
- * fastify.listen({ port: 3000 });
39
- * ```
40
- *
41
- * *Parameters:*
42
- * @param {Object} options - The options for configuring various plugins and services.
43
- * - prisma: Prisma Client configuration (optional).
44
- * - sendGrid: SendGrid configuration for email services (optional).
45
- * - twilio: Twilio configuration for SMS services (optional).
46
- * - cloudinary: Cloudinary configuration for media upload services (optional).
47
- * - auth: Authentication configuration for Admin and User JWT tokens (optional).
48
- * - cors: CORS configuration (optional).
49
- * - rateLimit: Rate-limiting options (optional).
50
- * - multipart: Multipart handling options for file uploads (optional).
51
- * - bugsnag: Bugsnag error reporting configuration (optional).
52
- * - underPressure: Under Pressure plugin options for monitoring and throttling under load (optional).
53
- *
54
- * *Authentication:*
55
- * The plugin provides both Admin and User authentication with JWT support. It sets secure cookies,
56
- * manages token refresh, and validates tokens on protected routes. For example:
57
- * - `/admin/auth/login` for admin login.
58
- * - `/portal/auth/login` for user login.
59
- * - `/admin/auth/me` to check authentication status for admins.
60
- * - `/portal/auth/me` to check authentication status for users.
61
- *
62
- * *Health Check:*
63
- * The `/health` route provides a health check with details about uptime, memory usage, CPU load,
64
- * database and Redis status, and environment variable validation.
65
- *
66
- * *Services:*
67
- * - **Prisma**: Initializes Prisma Client and decorates the Fastify instance for database queries.
68
- * - **SendGrid**: Provides an email sending service through SendGrid, integrated with the Fastify instance.
69
- * - **Twilio**: Provides SMS sending and phone number validation services.
70
- * - **Cloudinary**: Handles file uploads to Cloudinary and deletion of media files.
71
- * - **Authentication**: JWT-based authentication for Admin and User with token-based sessions.
72
- *
73
- * *Hooks:*
74
- * - `onRequest`: Validates JWT tokens for protected routes.
75
- * - `onClose`: Gracefully disconnects Prisma and other services on server shutdown.
76
- *
77
- * *Error Handling:*
78
- * Fancy error handling is enabled by default, showing enhanced error messages during development.
79
- * Bugsnag integration is optional for real-time error reporting.
80
- *
81
- * *Route Listing:*
82
- * The plugin prints all routes after registration, color-coded by HTTP method, unless disabled.
83
- *
84
- * *Dependencies:*
85
- * - Prisma Client, SendGrid, Twilio, Cloudinary, Fastify CORS, Fastify Rate Limit, Fastify Multipart,
86
- * Fastify Under Pressure, and Fastify Bugsnag are used for various services and integrations.
87
- *
88
- * *Notes:*
89
- * - Environment variables such as `DATABASE_URL`, `ADMIN_JWT_SECRET`, and `USER_JWT_SECRET` are expected to be set.
90
- * - Services like SendGrid, Twilio, and Cloudinary require API keys to be passed via the options.
91
- * - This plugin is highly customizable, with options to enable/disable each feature.
92
- *
93
- * *Example Configuration:*
94
- * ```javascript
95
- * fastify.register(xConfig, {
96
- * prisma: { active: true },
97
- * sendGrid: { apiKey: 'SG.your_api_key' },
98
- * twilio: {
99
- * accountSid: 'ACxxxxxxxxxxxxxxxx',
100
- * authToken: 'your_auth_token',
101
- * phoneNumber: '+1234567890',
102
- * },
103
- * cloudinary: {
104
- * cloudName: 'your-cloud-name',
105
- * apiKey: 'your-api-key',
106
- * apiSecret: 'your-api-secret',
107
- * },
108
- * auth: {
109
- * admin: {
110
- * secret: 'admin-secret',
111
- * expiresIn: '1h',
112
- * cookieOptions: { name: 'adminCookie', refreshTokenName: 'adminRefreshToken' },
113
- * },
114
- * user: {
115
- * secret: 'user-secret',
116
- * expiresIn: '1h',
117
- * cookieOptions: { name: 'userCookie', refreshTokenName: 'userRefreshToken' },
118
- * },
119
- * },
120
- * cors: { origin: ['https://your-frontend.com'], credentials: true },
121
- * });
122
- * ```
123
- */
124
-
125
- import fp from "fastify-plugin";
126
- import jwt from "@fastify/jwt";
127
- import bcrypt from "bcrypt";
128
- import { PrismaClient } from "@prisma/client";
129
- import Sendgrid from '@sendgrid/mail';
130
- import sgClient from '@sendgrid/client';
131
- import Twilio from "twilio";
132
- import { v2 as Cloudinary } from "cloudinary";
133
- import { randomUUID } from "uncrypto"; // Import randomString from uncrypto
134
- const isProduction = process.env.NODE_ENV === 'production';
135
- import Stripe from 'stripe';
136
- /*
137
- ===== SET VARS =====
138
- Setting variables for the config
139
- */
140
- const COLORS = { POST: 33, GET: 32, PUT: 34, DELETE: 31, PATCH: 90, clear: 39 };
141
- const colorize = (m, t) =>
142
- `\u001b[${COLORS[m] || COLORS.clear}m${t}\u001b[${COLORS.clear}m`;
143
- const printRoutes = (routes, colors = true) =>
144
- routes
145
- .sort((a, b) => a.url.localeCompare(b.url))
146
- .forEach(({ method, url }) => {
147
- // Ensure method is always an array
148
- const methodsArray = Array.isArray(method) ? method : [method];
149
-
150
- // Filter out 'HEAD' methods
151
- methodsArray
152
- .filter((m) => m !== 'HEAD')
153
- .forEach((m) =>
154
- console.info(
155
- `${colors ? colorize(m, m) : m}\t${colors ? colorize(m, url) : url}`
156
- )
157
- );
158
- });
159
-
160
- // Import necessary modules
161
- import os from "os";
162
- import process from "process";
163
- import fs from "fs";
164
- import util from "util";
165
-
166
- // Promisify fs functions
167
- const statAsync = util.promisify(fs.stat);
168
-
169
- // Record the server start time
170
- const serverStartTime = Date.now();
171
-
172
- // Helper function to format bytes into human-readable format
173
- function formatBytes(bytes, decimals = 2) {
174
- if (bytes === 0) return "0 Bytes";
175
- const k = 1024;
176
- const dm = decimals < 0 ? 0 : decimals;
177
- const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
178
- const i = Math.floor(Math.log(bytes) / Math.log(k));
179
- const formattedNumber = parseFloat((bytes / Math.pow(k, i)).toFixed(dm));
180
- return `${formattedNumber} ${sizes[i]}`;
181
- }
182
-
183
- /*
184
- ===== SET MAIN FUNCTION AND OPTIONS =====
185
- Setting variables for the config
186
- */
187
- async function xConfig(fastify, options) {
188
- const {
189
- professional = false,
190
- fancyErrors = true,
191
- prisma: prismaOptions = {},
192
- bugsnag: bugsnagOptions = {},
193
- stripe: stripeOptions = {},
194
- sendGrid: sendGridOptions = {},
195
- twilio: twilioOptions = {},
196
- cloudinary: cloudinaryOptions = {},
197
- auth: authOptions = {},
198
- cors: corsOptions = {},
199
- underPressure: underPressureOptions = {},
200
- multipart: multipartOptions = {},
201
- rateLimit: rateLimitOptions = {},
202
- geocode: geocodeOptions = {},
203
- } = options;
204
-
205
- // add starting console with emoji
206
- console.info("\n 🌮 Starting xConfig...\n");
207
-
208
- /*
209
- ===== LIST ROUTES =====
210
- Moved the onRoute hook to the top to capture all routes.
211
- */
212
- const routes = [];
213
- fastify.addHook("onRoute", (r) => routes.push(r));
214
-
215
- /*
216
- ===== CORS =====
217
- */
218
- if (corsOptions.active !== false) {
219
- await fastify.register(import("@fastify/cors"), {
220
- origin: corsOptions.origin || [
221
- "https://getx.io",
222
- "http://localhost:3000",
223
- ],
224
- credentials:
225
- corsOptions.credentials !== undefined ? corsOptions.credentials : true,
226
- methods: corsOptions.methods || [
227
- "GET",
228
- "POST",
229
- "PUT",
230
- "DELETE",
231
- "OPTIONS",
232
- ],
233
- strictPreflight: false,
234
- hideOptionsRoute: true,
235
- allowedHeaders: [
236
- "Content-Type", // Standard content-type headers
237
- "Authorization", // Needed for JWT or OAuth authentication
238
- "Accept", // Accept headers for specifying response type
239
- "DNT", // Do Not Track header
240
- "Referer" // Referrer header for tracking
241
- ],
242
- exposedHeaders: [
243
- "Authorization", // For accessing token headers after login
244
- "Set-Cookie" // Expose Set-Cookie header to client
245
- ],
246
- });
247
- console.info(" ✅ CORS Enabled");
248
- }
249
-
250
- /*
251
- ===== SENSIBLE =====
252
- */
253
- fastify.register(import('@fastify/sensible'))
254
-
255
- /*
256
- ===== UNDER PRESSURE =====
257
- */
258
- if (underPressureOptions.active !== false) {
259
- fastify.register(import("@fastify/under-pressure"), underPressureOptions);
260
- console.info(" ✅ Under Pressure Enabled");
261
- }
262
-
263
- /*
264
- ===== RATE LIMIT =====
265
- */
266
- if (rateLimitOptions.active !== false) {
267
- fastify.register(import("@fastify/rate-limit"), rateLimitOptions);
268
- console.info(" ✅ Rate Limiting Enabled");
269
- }
270
-
271
- /*
272
- ===== MULTIPART =====
273
- */
274
- if (multipartOptions.active !== false) {
275
- fastify.register(import("@fastify/multipart"), multipartOptions);
276
- console.info(" ✅ Multipart Enabled");
277
- }
278
-
279
- /*
280
- ===== BUGSNAG =====
281
- */
282
- if (bugsnagOptions.active !== false) {
283
- if (!bugsnagOptions.apiKey)
284
- throw new Error("Bugsnag API key must be provided.");
285
- fastify.register(import("fastify-bugsnag"), {
286
- apiKey: bugsnagOptions.apiKey,
287
- });
288
- console.info(" ✅ BugSnag Enabled");
289
- }
290
-
291
- /*
292
- ===== EXTEND ERRORS =====
293
- */
294
- if (fancyErrors !== false) {
295
- fastify.setErrorHandler((error, request, reply) => {
296
- const statusCode = error.statusCode || 500;
297
- const response = {
298
- status: statusCode,
299
- message: error.message || "Internal Server Error",
300
- // Only show stack in development mode
301
- stack: process.env.NODE_ENV === "development" ? error.stack : undefined,
302
- };
303
-
304
- // Optional Bugsnag error reporting
305
- if (fastify.bugsnag) {
306
- fastify.bugsnag.notify(error);
307
- }
308
-
309
- fastify.log.error(response);
310
- reply.status(statusCode).send(response);
311
- });
312
- console.info(" ✅ Fancy Errors Enabled");
313
- }
314
-
315
- /*
316
- ===== PRISMA =====
317
- */
318
- if (prismaOptions.active !== false) {
319
- delete prismaOptions.active;
320
- const prisma = new PrismaClient(prismaOptions);
321
-
322
- // Connect to the database
323
- await prisma.$connect();
324
-
325
- // Decorate Fastify instance with Prisma Client
326
- fastify.decorate("prisma", prisma);
327
-
328
- // Disconnect Prisma Client when Fastify closes
329
- fastify.addHook("onClose", async () => {
330
- await fastify.prisma.$disconnect();
331
- });
332
-
333
- console.info(" ✅ Prisma Enabled");
334
- }
335
-
336
- /*
337
- ===== STRIPE =====
338
- */
339
- if (stripeOptions.active === true) {
340
- if (!stripeOptions.apiKey) throw new Error("Stripe API key must be provided.");
341
-
342
-
343
-
344
- const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY, {
345
- apiVersion: '2022-11-15',
346
- });
347
-
348
- const stripePlugin = async function (fastify, options) {
349
- fastify.decorate('stripe', {
350
- // Create a Payment Intent (for processing payments)
351
- createPaymentIntent: async (amount, currency = 'usd', customerId) => {
352
- try {
353
- const paymentIntent = await stripeClient.paymentIntents.create({
354
- amount,
355
- currency,
356
- customer: customerId,
357
- });
358
- return paymentIntent;
359
- } catch (error) {
360
- fastify.log.error('Failed to create payment intent:', error);
361
- throw new Error('Failed to create payment intent.');
362
- }
363
- },
364
-
365
- // Create a Customer
366
- createCustomer: async (email, name, paymentMethodId) => {
367
- try {
368
- const customer = await stripeClient.customers.create({
369
- email,
370
- name,
371
- payment_method: paymentMethodId,
372
- invoice_settings: {
373
- default_payment_method: paymentMethodId,
374
- },
375
- });
376
- return customer;
377
- } catch (error) {
378
- fastify.log.error('Failed to create customer:', error);
379
- throw new Error('Failed to create customer.');
380
- }
381
- },
382
-
383
- // Create a Subscription (recurring payments)
384
- createSubscription: async (customerId, priceId) => {
385
- try {
386
- const subscription = await stripeClient.subscriptions.create({
387
- customer: customerId,
388
- items: [{ price: priceId }],
389
- expand: ['latest_invoice.payment_intent'],
390
- });
391
- return subscription;
392
- } catch (error) {
393
- fastify.log.error('Failed to create subscription:', error);
394
- throw new Error('Failed to create subscription.');
395
- }
396
- },
397
-
398
- // Retrieve Payment Details
399
- retrievePayment: async (paymentIntentId) => {
400
- try {
401
- const paymentIntent = await stripeClient.paymentIntents.retrieve(paymentIntentId);
402
- return paymentIntent;
403
- } catch (error) {
404
- fastify.log.error('Failed to retrieve payment intent:', error);
405
- throw new Error('Failed to retrieve payment intent.');
406
- }
407
- },
408
-
409
- // Create an Invoice
410
- createInvoice: async (customerId, items) => {
411
- try {
412
- const invoiceItemPromises = items.map(async item => {
413
- return stripeClient.invoiceItems.create({
414
- customer: customerId,
415
- price: item.priceId, // Assuming you have the price ID
416
- });
417
- });
418
- await Promise.all(invoiceItemPromises);
419
-
420
- const invoice = await stripeClient.invoices.create({
421
- customer: customerId,
422
- auto_advance: true, // Automatically finalize the invoice
423
- });
424
- return invoice;
425
- } catch (error) {
426
- fastify.log.error('Failed to create invoice:', error);
427
- throw new Error('Failed to create invoice.');
428
- }
429
- },
430
-
431
- // Handle Webhooks (e.g., payment success, failed, etc.)
432
- handleWebhook: async (req, res, signature, endpointSecret) => {
433
- let event;
434
- try {
435
- event = stripeClient.webhooks.constructEvent(
436
- req.rawBody, // Stripe requires the raw body for webhooks
437
- signature,
438
- endpointSecret
439
- );
440
- } catch (error) {
441
- fastify.log.error('Failed to verify webhook signature:', error);
442
- throw new Error('Webhook signature verification failed.');
443
- }
444
-
445
- // Process the event (e.g., payment success, subscription created)
446
- return event;
447
- },
448
- });
449
-
450
- console.info(" ✅ Stripe Enabled");
451
- }
452
- }
453
-
454
- /*
455
- ===== SENDGRID =====
456
- */
457
- if (sendGridOptions.active !== false) {
458
- if (!sendGridOptions.apiKey)
459
- throw new Error("SendGrid API key must be provided.");
460
-
461
- Sendgrid.setApiKey(sendGridOptions.apiKey);
462
- sgClient.setApiKey(sendGridOptions.apiKeyEmailValidation);
463
- // Decorator to group SendGrid-related methods under fastify.sendGrid
464
- fastify.decorate('sendgrid', {
465
- sendEmail: async (to, subject, templateId, dynamicTemplateData) => {
466
- try {
467
- const msg = {
468
- to,
469
- from: sendGridOptions.fromEmail,
470
- subject,
471
- templateId,
472
- dynamicTemplateData,
473
- };
474
- await Sendgrid.send(msg);
475
- return true;
476
- } catch (error) {
477
- fastify.log.error("SendGrid send email failed:", error);
478
- throw new Error("Failed to send email.");
479
- }
480
- },
481
- validateEmail: async (email) => {
482
- const data = {
483
- email: email,
484
- // source: 'signup',
485
- };
486
-
487
- const request = {
488
- url: `/v3/validations/email`,
489
- method: 'POST',
490
- body: data,
491
- };
492
-
493
- try {
494
- const [response, body] = await sgClient.request(request);
495
- console.log(body)
496
- if (response.statusCode === 200) {
497
- return body?.result; // Return the validation result
498
- } else {
499
- throw new Error(body.errors ? body.errors.map(err => err.message).join(', ') : 'Failed to validate email.');
500
- }
501
- } catch (error) {
502
- fastify.log.error('SendGrid email validation failed:', error);
503
- throw new Error('Failed to validate email.');
504
- }
505
- }
506
- });
507
-
508
- console.info(" ✅ SendGrid Enabled");
509
- }
510
-
511
- /*
512
- ===== TWILIO =====
513
- */
514
- if (twilioOptions.active !== false) {
515
- // Return if missing Twilio credentials
516
- if (
517
- !twilioOptions.accountSid ||
518
- !twilioOptions.authToken ||
519
- !twilioOptions.phoneNumber
520
- )
521
- throw new Error(
522
- "Twilio accountSid, authToken, and phoneNumber must be provided."
523
- );
524
-
525
- const twilioClient = Twilio(
526
- twilioOptions.accountSid,
527
- twilioOptions.authToken
528
- );
529
-
530
- // Decorator to group Twilio-related methods under fastify.twilio
531
- fastify.decorate('twilio', {
532
- sendSMS: async (to, body) => {
533
- try {
534
- const message = await twilioClient.messages.create({
535
- body,
536
- to,
537
- from: twilioOptions.phoneNumber, // Use the Twilio phone number from options
538
- });
539
- return message;
540
- } catch (error) {
541
- fastify.log.error("Twilio send SMS failed:", error);
542
- throw new Error("Failed to send SMS.");
543
- }
544
- },
545
- sendMMS: async (to, body, mediaUrl) => {
546
- try {
547
- const message = await twilioClient.messages.create({
548
- body,
549
- to,
550
- from: twilioOptions.phoneNumber,
551
- mediaUrl: [mediaUrl], // Array of media URLs
552
- });
553
- return message;
554
- } catch (error) {
555
- fastify.log.error("Twilio send MMS failed:", error);
556
- throw new Error("Failed to send MMS.");
557
- }
558
- },
559
- makeVoiceCall: async (to, twimlUrl) => {
560
- try {
561
- const call = await twilioClient.calls.create({
562
- to,
563
- from: twilioOptions.phoneNumber,
564
- url: twimlUrl, // URL that provides TwiML instructions
565
- });
566
- return call;
567
- } catch (error) {
568
- fastify.log.error("Twilio make voice call failed:", error);
569
- throw new Error("Failed to make voice call.");
570
- }
571
- },
572
- sendVerificationCode: async (to, channel = 'sms') => {
573
- try {
574
- const verification = await twilioClient.verify.services(twilioOptions.verifyServiceSid)
575
- .verifications
576
- .create({ to, channel }); // channel can be 'sms' or 'call'
577
- return verification;
578
- } catch (error) {
579
- fastify.log.error("Twilio send verification code failed:", error);
580
- throw new Error("Failed to send verification code.");
581
- }
582
- },
583
- verifyCode: async (to, code) => {
584
- try {
585
- const verificationCheck = await twilioClient.verify.services(twilioOptions.verifyServiceSid)
586
- .verificationChecks
587
- .create({ to, code });
588
- return verificationCheck;
589
- } catch (error) {
590
- fastify.log.error("Twilio verify code failed:", error);
591
- throw new Error("Failed to verify code.");
592
- }
593
- },
594
- fetchSMSHistory: async (fromDate, toDate) => {
595
- try {
596
- const messages = await twilioClient.messages.list({
597
- dateSentAfter: fromDate,
598
- dateSentBefore: toDate,
599
- limit: 100,
600
- });
601
- return messages;
602
- } catch (error) {
603
- fastify.log.error("Twilio fetch SMS history failed:", error);
604
- throw new Error("Failed to fetch SMS history.");
605
- }
606
- },
607
- handleIncomingSMS: (req, reply) => {
608
- const { Body, From } = req.body;
609
-
610
- // Process incoming message
611
- console.log(`Message received from ${From}: ${Body}`);
612
-
613
- reply.type('text/xml');
614
- reply.send('<Response><Message>Thanks for your message!</Message></Response>');
615
- },
616
- checkCallStatus: async (callSid) => {
617
- try {
618
- const call = await twilioClient.calls(callSid).fetch();
619
- return call;
620
- } catch (error) {
621
- fastify.log.error("Twilio check call status failed:", error);
622
- throw new Error("Failed to check call status.");
623
- }
624
- },
625
- validatePhoneNumber: async (phoneNumber) => {
626
- try {
627
- // Perform phone number validation with Twilio
628
- const validation = await twilioClient.lookups.v2
629
- .phoneNumbers(phoneNumber)
630
- .fetch();
631
-
632
- // Check if the phone number is valid (Twilio may return "valid": true/false)
633
- const isValid = validation.valid !== undefined ? validation.valid : true;
634
-
635
- // Return the required structure
636
- return {
637
- sms: phoneNumber, // The phone number
638
- smsValidate: validation, // The full validation payload from Twilio
639
- isSmsValidated: isValid // Set true/false based on the validation
640
- };
641
- } catch (error) {
642
- fastify.log.error("Twilio phone number validation failed:", error);
643
-
644
- // Return the structure with failed validation
645
- return {
646
- sms: phoneNumber, // The phone number
647
- smsValidate: {}, // Empty object as validation failed
648
- isSmsValidated: false // Validation failed
649
- };
650
- }
651
- }
652
- });
653
-
654
- console.info(" ✅ Twilio Enabled");
655
- }
656
-
657
- /*
658
- ===== CLOUDINARY =====
659
- */
660
- if (cloudinaryOptions.active !== false) {
661
- if (
662
- !cloudinaryOptions.cloudName ||
663
- !cloudinaryOptions.apiKey ||
664
- !cloudinaryOptions.apiSecret
665
- ) {
666
- throw new Error(
667
- "Cloudinary cloudName, apiKey, and apiSecret must be provided."
668
- );
669
- }
670
-
671
- Cloudinary.config({
672
- cloud_name: cloudinaryOptions.cloudName,
673
- api_key: cloudinaryOptions.apiKey,
674
- api_secret: cloudinaryOptions.apiSecret,
675
- },
676
- );
677
-
678
- fastify.decorate('cloudinary', {
679
- upload: async (fileStream, options = {}) => {
680
- return new Promise((resolve, reject) => {
681
- if (cloudinaryOptions.folder) {
682
- options.folder = options.folder || cloudinaryOptions.folder;
683
- }
684
- options.resource_type = options.resource_type || 'auto';
685
- options.timestamp = Math.floor(Date.now() / 1000); // Add timestamp to options
686
- options.use_filename = options.use_filename !== undefined ? options.use_filename : true;
687
- options.unique_filename = options.unique_filename !== undefined ? options.unique_filename : true;
688
-
689
- const uploadStream = Cloudinary.uploader.upload_stream(options, (error, result) => {
690
- if (error) {
691
- fastify.log.error("Cloudinary upload failed:", error);
692
- reject(new Error(`Failed to upload to Cloudinary: ${JSON.stringify(error)}`));
693
- } else {
694
- resolve(result); // Resolve with the result from Cloudinary
695
- }
696
- });
697
-
698
- fileStream.pipe(uploadStream)
699
- .on('error', (streamError) => {
700
- fastify.log.error("Stream error:", streamError);
701
- reject(new Error(`Stream error during Cloudinary upload: ${streamError.message}`));
702
- })
703
- .on('end', () => {
704
- fastify.log.info("Stream ended successfully.");
705
- });
706
- });
707
- },
708
-
709
- uploadLarge: async (fileStream, options = {}) => {
710
- return new Promise((resolve, reject) => {
711
- if (cloudinaryOptions.folder) {
712
- options.folder = options.folder || cloudinaryOptions.folder;
713
- }
714
- options.resource_type = options.resource_type || 'auto';
715
- options.timestamp = Math.floor(Date.now() / 1000);
716
- options.use_filename = options.use_filename !== undefined ? options.use_filename : true;
717
- options.unique_filename = options.unique_filename !== undefined ? options.unique_filename : true;
718
- options.overwrite = options.overwrite !== undefined ? options.overwrite : false;
719
-
720
- const uploadStream = Cloudinary.uploader.upload_stream(options, (error, result) => {
721
- if (error) {
722
- fastify.log.error("Cloudinary upload failed:", error);
723
- reject(new Error(`Failed to upload to Cloudinary: ${JSON.stringify(error)}`));
724
- } else {
725
- resolve(result);
726
- }
727
- });
728
-
729
- fileStream.pipe(uploadStream)
730
- .on('error', (streamError) => {
731
- fastify.log.error("Stream error:", streamError);
732
- reject(new Error(`Stream error during Cloudinary upload: ${streamError.message}`));
733
- })
734
- .on('end', () => {
735
- fastify.log.info("Stream ended successfully.");
736
- });
737
- });
738
- },
739
-
740
- delete: async (publicId) => {
741
- try {
742
- const response = await Cloudinary.uploader.destroy(publicId);
743
- return response;
744
- } catch (error) {
745
- fastify.log.error("Cloudinary delete failed:", error);
746
- throw new Error("Failed to delete from Cloudinary.");
747
- }
748
- }
749
- });
750
-
751
- console.info(" ✅ Cloudinary Enabled");
752
- }
753
-
754
- /*
755
- ===== AUTHENTICATION =====
756
- Admin and User authentication are handled separately and redundantly,
757
- sharing no code between them.
758
- */
759
-
760
- // Register @fastify/cookie plugin
761
- fastify.register(import("@fastify/cookie"));
762
-
763
- console.info(" ✅ @fastify/cookie Enabled");
764
-
765
- /*
766
- ===== Admin Authentication =====
767
- */
768
- /*
769
- ===== Admin Authentication =====
770
- */
771
- if (authOptions.admin?.active !== false) {
772
-
773
- // Ensure the admin JWT secret is provided
774
- if (!authOptions.admin.secret) {
775
- throw new Error("Admin JWT secret must be provided.");
776
- }
777
-
778
- const adminAuthOptions = authOptions.admin;
779
- const adminCookieName =
780
- adminAuthOptions.cookieOptions?.name || "adminToken";
781
- const adminRefreshCookieName =
782
- adminAuthOptions.cookieOptions?.refreshTokenName || "adminRefreshToken";
783
- const adminCookieOptions = {
784
- httpOnly: true, // Ensures the cookie is not accessible via JavaScript
785
- secure: isProduction, // true in production (HTTPS), false in development (HTTP)
786
- sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
787
- path: '/', // Ensure cookies are valid for the entire site
788
- };
789
- const adminExcludedPaths = adminAuthOptions.excludedPaths || [
790
- "/admin/auth/login",
791
- "/admin/auth/logout",
792
- ];
793
-
794
- // Decorator to hash admin passwords
795
- async function hashAdminPassword(password) {
796
- const saltRounds = 10; // Number of salt rounds for bcrypt (10 is generally a good default)
797
- try {
798
- const hashedPassword = await bcrypt.hash(password, saltRounds);
799
- return hashedPassword;
800
- } catch (error) {
801
- throw new Error("Failed to hash password: " + error.message);
802
- }
803
- }
804
-
805
- fastify.decorate("hashAdminPassword", hashAdminPassword);
806
-
807
- // Register JWT for admin
808
- await fastify.register(jwt, {
809
- secret: adminAuthOptions.secret,
810
- sign: { algorithm: 'HS256', expiresIn: adminAuthOptions.expiresIn || "15m" },
811
- cookie: {
812
- cookieName: adminCookieName,
813
- signed: false,
814
- },
815
- namespace: "adminJwt",
816
- jwtVerify: "adminJwtVerify",
817
- jwtSign: "adminJwtSign",
818
- });
819
-
820
- // Common function to set tokens as cookies
821
- const setAdminAuthCookies = (reply, accessToken, refreshToken) => {
822
- reply.setCookie(adminCookieName, accessToken, adminCookieOptions);
823
- reply.setCookie(adminRefreshCookieName, refreshToken, {
824
- // ...adminCookieOptions,
825
- httpOnly: true, // Ensures the cookie is not accessible via JavaScript
826
- secure: isProduction, // true in production (HTTPS), false in development (HTTP)
827
- sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
828
- path: '/', // Ensure cookies are valid for the entire site
829
- });
830
- };
831
-
832
- // Admin authentication hook
833
- fastify.addHook("onRequest", async (request, reply) => {
834
- const url = request.url;
835
-
836
- // Skip authentication for excluded paths
837
- if (adminExcludedPaths.some((path) => url.startsWith(path))) {
838
- return;
839
- }
840
-
841
- if (url.startsWith("/admin")) {
842
- try {
843
- // Extract token from cookie or Authorization header
844
- const authHeader = request.headers.authorization;
845
- const authToken =
846
- authHeader && authHeader.startsWith("Bearer ")
847
- ? authHeader.slice(7)
848
- : null;
849
- const token = request.cookies[adminCookieName] || authToken;
850
-
851
- if (!token) {
852
- throw fastify.httpErrors.unauthorized(
853
- "Admin access token not provided"
854
- );
855
- }
856
-
857
- // Verify access token
858
- const decoded = await request.adminJwtVerify(token);
859
- request.adminAuth = decoded; // Attach admin auth context
860
- } catch (err) {
861
- // Use built-in HTTP error handling
862
- reply.send(
863
- fastify.httpErrors.unauthorized("Invalid or expired access token")
864
- );
865
- }
866
- }
867
- });
868
-
869
- // Admin login route
870
- fastify.post("/admin/auth/login", async (req, reply) => {
871
- try {
872
- const { email, password } = req.body;
873
-
874
- // Validate input
875
- if (!email || !password) {
876
- throw fastify.httpErrors.badRequest(
877
- "Email and password are required"
878
- );
879
- }
880
-
881
- // Fetch admin from the database
882
- const admin = await fastify.prisma.admins.findUnique({
883
- where: { email },
884
- });
885
- if (!admin) {
886
- throw fastify.httpErrors.unauthorized("Invalid credentials");
887
- }
888
-
889
- // Compare passwords using bcrypt
890
- const isValidPassword = await bcrypt.compare(password, admin.password);
891
- if (!isValidPassword) {
892
- throw fastify.httpErrors.unauthorized("Invalid credentials");
893
- }
894
-
895
- // Issue access token
896
- const accessToken = await reply.adminJwtSign({ id: admin.id });
897
-
898
- // Generate refresh token
899
- const refreshToken = randomUUID();
900
- const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
901
-
902
- // Store hashed refresh token in the database
903
- await fastify.prisma.admins.update({
904
- where: { id: admin.id },
905
- data: { refreshToken: hashedRefreshToken },
906
- });
907
-
908
- // Set tokens as cookies
909
- setAdminAuthCookies(reply, accessToken, refreshToken);
910
-
911
- reply.send({ accessToken });
912
- } catch (err) {
913
- reply.send(err);
914
- }
915
- });
916
-
917
- // Admin refresh token route
918
- fastify.post("/admin/auth/refresh", async (req, reply) => {
919
- try {
920
- const adminAuth = req.adminAuth;
921
- const refreshToken = req.cookies[adminRefreshCookieName];
922
- if (!refreshToken) {
923
- throw fastify.httpErrors.unauthorized("Refresh token not provided");
924
- }
925
-
926
- // Fetch admin from the database using the refresh token
927
- const admin = await fastify.prisma.admins.findFirst({
928
- where: { id: adminAuth.id, refreshToken: { not: null } },
929
- });
930
- if (!admin) {
931
- throw fastify.httpErrors.unauthorized("Invalid refresh token");
932
- }
933
-
934
- // Verify the refresh token
935
- const isValid = await bcrypt.compare(refreshToken, admin.refreshToken);
936
- if (!isValid) {
937
- throw fastify.httpErrors.unauthorized("Invalid refresh token");
938
- }
939
-
940
- // Issue new access token
941
- const accessToken = await reply.adminJwtSign({ id: admin.id });
942
-
943
- // Generate new refresh token
944
- const newRefreshToken = randomUUID();
945
- const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
946
-
947
- // Update refresh token in the database
948
- await fastify.prisma.admins.update({
949
- where: { id: admin.id },
950
- data: { refreshToken: hashedNewRefreshToken },
951
- });
952
-
953
- // Set new tokens as cookies
954
- setAdminAuthCookies(reply, accessToken, newRefreshToken);
955
-
956
- reply.send({ accessToken });
957
- } catch (err) {
958
- reply.send(err);
959
- }
960
- });
961
-
962
- // Admin logout route
963
- fastify.post("/admin/auth/logout", async (req, reply) => {
964
- try {
965
- const adminAuth = req.adminAuth;
966
- if (adminAuth) {
967
- // Delete refresh token from the database
968
- await fastify.prisma.admins.update({
969
- where: { id: adminAuth.id },
970
- data: { refreshToken: null },
971
- });
972
- }
973
-
974
- // Clear cookies
975
- reply.clearCookie(adminCookieName, { path: "/" });
976
- reply.clearCookie(adminRefreshCookieName, { path: "/" });
977
-
978
- reply.send({ message: "Logged out successfully" });
979
- } catch (err) {
980
- reply.send(err);
981
- }
982
- });
983
-
984
- // Admin authentication status route
985
- fastify.get("/admin/auth/me", async (req, reply) => {
986
- try {
987
- const adminAuth = req.adminAuth;
988
-
989
- // Fetch admin details from database
990
- const admin = await fastify.prisma.admins.findUnique({
991
- where: { id: adminAuth.id },
992
- select: { id: true, firstName: true, lastName: true, email: true },
993
- });
994
-
995
- if (!admin) {
996
- throw fastify.httpErrors.notFound("Admin not found");
997
- }
998
-
999
- reply.send(admin);
1000
- } catch (err) {
1001
- reply.send(err);
1002
- }
1003
- });
1004
-
1005
- console.info(" ✅ Auth Admin Enabled");
1006
- }
1007
-
1008
- /*
1009
- ===== User Authentication =====
1010
- */
1011
- if (authOptions.user?.active !== false) {
1012
- // Ensure the user JWT secret is provided
1013
- if (!authOptions.user.secret) {
1014
- throw new Error("User JWT secret must be provided.");
1015
- }
1016
-
1017
- const userAuthOptions = authOptions.user;
1018
- const userCookieName = userAuthOptions.cookieOptions?.name || "userToken";
1019
- const userRefreshCookieName =
1020
- userAuthOptions.cookieOptions?.refreshTokenName || "userRefreshToken";
1021
- const userCookieOptions = {
1022
- httpOnly: true, // Ensures the cookie is not accessible via JavaScript
1023
- secure: isProduction, // true in production (HTTPS), false in development (HTTP)
1024
- sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
1025
- path: '/', // Ensure cookies are valid for the entire site
1026
- };
1027
- const userExcludedPaths = userAuthOptions.excludedPaths || [
1028
- "/portal/auth/login",
1029
- "/portal/auth/logout",
1030
- "/portal/auth/register",
1031
- ];
1032
-
1033
- // Register JWT for user
1034
- await fastify.register(jwt, {
1035
- secret: userAuthOptions.secret,
1036
- sign: { algorithm: 'HS256', expiresIn: userAuthOptions.expiresIn || "15m" },
1037
- cookie: { cookieName: userCookieName, signed: false },
1038
- namespace: "userJwt",
1039
- jwtVerify: "userJwtVerify",
1040
- jwtSign: "userJwtSign",
1041
- });
1042
-
1043
- // Common function to set tokens as cookies
1044
- const setAuthCookies = (reply, accessToken, refreshToken) => {
1045
- reply.setCookie(userCookieName, accessToken, userCookieOptions);
1046
- reply.setCookie(userRefreshCookieName, refreshToken, {
1047
- // ...userCookieOptions,
1048
- httpOnly: true, // Ensures the cookie is not accessible via JavaScript
1049
- secure: isProduction, // true in production (HTTPS), false in development (HTTP)
1050
- sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
1051
- path: '/', // Ensure cookies are valid for the entire site
1052
- });
1053
- };
1054
-
1055
- // User authentication hook
1056
- fastify.addHook("onRequest", async (request, reply) => {
1057
- const url = request.url;
1058
-
1059
- // Skip authentication for excluded paths
1060
- if (userExcludedPaths.some((path) => url.startsWith(path))) {
1061
- return;
1062
- }
1063
-
1064
- if (url.startsWith("/portal")) {
1065
- try {
1066
- // Extract token from cookie or Authorization header
1067
- const authHeader = request.headers.authorization;
1068
- const authToken =
1069
- authHeader && authHeader.startsWith("Bearer ")
1070
- ? authHeader.slice(7)
1071
- : null;
1072
- const token = request.cookies[userCookieName] || authToken;
1073
-
1074
- if (!token) {
1075
- throw fastify.httpErrors.unauthorized(
1076
- "User access token not provided"
1077
- );
1078
- }
1079
-
1080
- // Verify access token using the namespaced verify method
1081
- const decoded = await request.userJwtVerify(token);
1082
- request.userAuth = decoded; // Attach user auth context
1083
- } catch (err) {
1084
- // Use built-in HTTP error handling
1085
- reply.send(
1086
- fastify.httpErrors.unauthorized("Invalid or expired access token")
1087
- );
1088
- }
1089
- }
1090
- });
1091
-
1092
- // User registration route
1093
- fastify.post("/portal/auth/register", async (req, reply) => {
1094
- try {
1095
- const { email, password, firstName, lastName } = req.body;
1096
-
1097
- // Validate input
1098
- if (!email || !password || !firstName || !lastName) {
1099
- throw fastify.httpErrors.badRequest("Missing required fields");
1100
- }
1101
-
1102
- // Check if user already exists
1103
- const existingUser = await fastify.prisma.users.findUnique({
1104
- where: { email },
1105
- });
1106
- if (existingUser) {
1107
- throw fastify.httpErrors.conflict("Email already in use");
1108
- }
1109
-
1110
- // Hash the password
1111
- const hashedPassword = await bcrypt.hash(password, 10);
1112
-
1113
- // Create the user
1114
- const user = await fastify.prisma.users.create({
1115
- data: {
1116
- email,
1117
- password: hashedPassword,
1118
- firstName,
1119
- lastName,
1120
- },
1121
- });
1122
-
1123
- // Send welcome email
1124
- await fastify.sendGrid.sendEmail(email, auth?.user?.registerEmail?.subject, auth?.user?.registerEmail?.templateId, { name: firstName, email });
1125
-
1126
- // Issue access token
1127
- const accessToken = await reply.userJwtSign({ id: user.id });
1128
-
1129
- // Generate refresh token
1130
- const refreshToken = randomUUID();
1131
- const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
1132
-
1133
- // Store hashed refresh token in the database
1134
- await fastify.prisma.users.update({
1135
- where: { id: user.id },
1136
- data: { refreshToken: hashedRefreshToken },
1137
- });
1138
-
1139
- // Set tokens as cookies
1140
- setAuthCookies(reply, accessToken, refreshToken);
1141
-
1142
- reply.send({ accessToken });
1143
- } catch (err) {
1144
- reply.send(err);
1145
- }
1146
- });
1147
-
1148
- // User login route
1149
- fastify.post("/portal/auth/login", async (req, reply) => {
1150
- try {
1151
- const { email, password } = req.body;
1152
-
1153
- if (!email || !password) {
1154
- throw fastify.httpErrors.badRequest(
1155
- "Email and password are required"
1156
- );
1157
- }
1158
-
1159
- // Fetch user from the database
1160
- const user = await fastify.prisma.users.findUnique({
1161
- where: { email },
1162
- });
1163
- if (!user) {
1164
- throw fastify.httpErrors.unauthorized("Invalid credentials");
1165
- }
1166
-
1167
- // Compare passwords using bcrypt
1168
- const isValidPassword = await bcrypt.compare(password, user.password);
1169
- if (!isValidPassword) {
1170
- throw fastify.httpErrors.unauthorized("Invalid credentials");
1171
- }
1172
-
1173
- // Issue access token
1174
- const accessToken = await reply.userJwtSign({ id: user.id });
1175
-
1176
- // Generate refresh token
1177
- const refreshToken = randomUUID();
1178
- const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
1179
-
1180
- // Store hashed refresh token in the database
1181
- await fastify.prisma.users.update({
1182
- where: { id: user.id },
1183
- data: { refreshToken: hashedRefreshToken },
1184
- });
1185
-
1186
- // Set tokens as cookies
1187
- setAuthCookies(reply, accessToken, refreshToken);
1188
-
1189
- reply.send({ accessToken });
1190
- } catch (err) {
1191
- reply.send(err);
1192
- }
1193
- });
1194
-
1195
- // User refresh token route
1196
- fastify.post("/portal/auth/refresh", async (req, reply) => {
1197
- try {
1198
- const userAuth = req.userAuth;
1199
- const refreshToken = req.cookies[userRefreshCookieName];
1200
- if (!refreshToken) {
1201
- throw fastify.httpErrors.unauthorized("Refresh token not provided");
1202
- }
1203
-
1204
- // Fetch user from the database using the refresh token
1205
- const user = await fastify.prisma.users.findFirst({
1206
- where: { id: userAuth?.id, refreshToken: { not: null } },
1207
- });
1208
-
1209
- if (!user) {
1210
- throw fastify.httpErrors.unauthorized("Invalid refresh token");
1211
- }
1212
-
1213
- // Verify the refresh token
1214
- const isValid = await bcrypt.compare(refreshToken, user.refreshToken);
1215
- if (!isValid) {
1216
- throw fastify.httpErrors.unauthorized("Invalid refresh token");
1217
- }
1218
-
1219
- // Issue new access token
1220
- const accessToken = await reply.userJwtSign({ id: user.id });
1221
-
1222
- // Generate new refresh token
1223
- const newRefreshToken = randomUUID();
1224
- const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
1225
-
1226
- // Update refresh token in the database
1227
- await fastify.prisma.users.update({
1228
- where: { id: user.id },
1229
- data: { refreshToken: hashedNewRefreshToken },
1230
- });
1231
-
1232
- // Set new tokens as cookies
1233
- setAuthCookies(reply, accessToken, newRefreshToken);
1234
-
1235
- reply.send({ accessToken });
1236
- } catch (err) {
1237
- reply.send(err);
1238
- }
1239
- });
1240
-
1241
- // User logout route
1242
- fastify.post("/portal/auth/logout", async (req, reply) => {
1243
- try {
1244
- const userAuth = req.userAuth;
1245
- if (userAuth) {
1246
- // Delete refresh token from the database
1247
- await fastify.prisma.users.update({
1248
- where: { id: userAuth.id },
1249
- data: { refreshToken: null },
1250
- });
1251
- }
1252
-
1253
- // Clear cookies
1254
- reply.clearCookie(userCookieName, { path: "/" });
1255
- reply.clearCookie(userRefreshCookieName, { path: "/" });
1256
-
1257
- reply.send({ message: "Logged out successfully" });
1258
- } catch (err) {
1259
- reply.send(err);
1260
- }
1261
- });
1262
-
1263
- // User authentication status route
1264
- fastify.get("/portal/auth/me", async (req, reply) => {
1265
- try {
1266
- const userAuth = req.userAuth;
1267
-
1268
- // Fetch user details from database
1269
- const user = await fastify.prisma.users.findUnique({
1270
- where: { id: userAuth.id },
1271
- select: { id: true, email: true, firstName: true, lastName: true, ...authOptions.user.me },
1272
- });
1273
-
1274
- if (!user) {
1275
- throw fastify.httpErrors.notFound("User not found");
1276
- }
1277
-
1278
- reply.send(user);
1279
- } catch (err) {
1280
- reply.send(err);
1281
- }
1282
- });
1283
-
1284
- console.info(" ✅ Auth User Enabled");
1285
- }
1286
-
1287
- /*
1288
- ===== GEOCODE =====
1289
- */
1290
- if (geocodeOptions.active !== false) {
1291
- if (!geocodeOptions.apiKey) {
1292
- throw new Error("Geocode API key must be provided.");
1293
- }
1294
- const geocodioEndpoint = 'https://api.geocod.io/v1.7/geocode';
1295
- const geocodioApiKey = geocodeOptions.apiKey;
1296
-
1297
- fastify.decorate('geocode', {
1298
- // Method to get lat/long and county from a zip code
1299
- async getLatLongByZip(zipCode) {
1300
- try {
1301
- const url = `${geocodioEndpoint}?q=${zipCode}&fields=cd,stateleg&api_key=${geocodioApiKey}`;
1302
- const response = await fetch(url);
1303
- const result = await response.json();
1304
-
1305
- if (result.results && result.results.length > 0) {
1306
- const location = result.results[0].location;
1307
- const addressComponents = result.results[0].address_components;
1308
-
1309
- return {
1310
- zip: zipCode,
1311
- lat: location.lat,
1312
- lng: location.lng,
1313
- city: addressComponents.city,
1314
- county: addressComponents.county,
1315
- country: addressComponents.country,
1316
- state: addressComponents.state,
1317
- addressComponents: addressComponents
1318
- };
1319
- } else {
1320
- throw new Error('No results found for the provided zip code.');
1321
- }
1322
- } catch (error) {
1323
- fastify.log.error('Failed to fetch geolocation data:', error);
1324
- throw new Error('Failed to fetch geolocation data.');
1325
- }
1326
- }
1327
- });
1328
-
1329
- console.info(' ✅ Geocodio Enabled');
1330
- }
1331
-
1332
-
1333
-
1334
- /*
1335
- ===== LIST ROUTES AFTER ALL PLUGINS =====
1336
- Use the after() method to ensure this runs after all plugins are registered.
1337
- */
1338
- fastify.after(() => {
1339
- if (professional !== true) {
1340
- console.info(" ✅ Listing Routes:");
1341
- fastify.ready(() => {
1342
- printRoutes(routes, options.colors !== false);
1343
- // Add rocket emoji
1344
- console.info(
1345
- `🚀 Server is ready on port ${process.env.PORT || 3000}\n\n`
1346
- );
1347
- // Add goodbye emoji for server shutting down
1348
- fastify.addHook("onClose", () =>
1349
- console.info("Server shutting down... Goodbye 👋")
1350
- );
1351
- });
1352
- }
1353
- });
1354
-
1355
- /*
1356
- * ===== HEALTH CHECK ROUTE =====
1357
- */
1358
-
1359
- // Health Check Route
1360
- fastify.get("/health", async (request, reply) => {
1361
- const status = {
1362
- status: "healthy",
1363
- timestamp: new Date().toISOString(),
1364
- uptime: process.uptime(), // Uptime in seconds
1365
- version: "1.0.0", // Fetch dynamically if possible
1366
- environment: process.env.NODE_ENV || "development",
1367
- dependencies: {},
1368
- resources: {},
1369
- details: {},
1370
- };
1371
-
1372
- try {
1373
- // Database connectivity check
1374
- if (fastify.prisma) {
1375
- await fastify.prisma.$queryRaw`SELECT 1`;
1376
- status.dependencies.database = "up";
1377
- } else {
1378
- status.dependencies.database = "not configured";
1379
- }
1380
- } catch (error) {
1381
- status.dependencies.database = "down";
1382
- status.status = "degraded";
1383
- status.details.databaseError = error.message;
1384
- }
1385
-
1386
- try {
1387
- // Redis connectivity check
1388
- if (fastify.redis) {
1389
- await fastify.redis.ping();
1390
- status.dependencies.redis = "up";
1391
- } else {
1392
- status.dependencies.redis = "not configured";
1393
- }
1394
- } catch (error) {
1395
- status.dependencies.redis = "down";
1396
- status.status = "degraded";
1397
- status.details.redisError = error.message;
1398
- }
1399
-
1400
- // Resource usage
1401
- const memoryUsage = process.memoryUsage();
1402
- const loadAverage = os.loadavg();
1403
-
1404
- status.resources.memory = {
1405
- rss: formatBytes(memoryUsage.rss),
1406
- heapTotal: formatBytes(memoryUsage.heapTotal),
1407
- heapUsed: formatBytes(memoryUsage.heapUsed),
1408
- external: formatBytes(memoryUsage.external),
1409
- arrayBuffers: formatBytes(memoryUsage.arrayBuffers),
1410
- };
1411
-
1412
- status.resources.cpu = {
1413
- loadAverage, // 1, 5, and 15 minute load averages
1414
- cpus: os.cpus().length,
1415
- };
1416
-
1417
- // Disk space availability
1418
- try {
1419
- // Implement disk space check using 'check-disk-space' package
1420
- // Install it via 'npm install check-disk-space'
1421
- const checkDiskSpace = (await import("check-disk-space")).default;
1422
- const diskSpace = await checkDiskSpace("/"); // Replace '/' with your drive or mount point
1423
- status.resources.disk = {
1424
- free: formatBytes(diskSpace.free),
1425
- size: formatBytes(diskSpace.size),
1426
- };
1427
- } catch (error) {
1428
- status.resources.disk = "unknown";
1429
- status.details.diskError = error.message;
1430
- }
1431
-
1432
- // Application-specific checks
1433
- // Example: Check if a scheduled job is running
1434
- if (typeof isJobRunning === "function" && !isJobRunning()) {
1435
- status.status = "degraded";
1436
- status.details.jobStatus = "Scheduled job is not running";
1437
- }
1438
-
1439
- // Configuration validation
1440
- const requiredEnvVars = [
1441
- "DATABASE_URL",
1442
- "ADMIN_JWT_SECRET",
1443
- "USER_JWT_SECRET",
1444
- ];
1445
- const missingEnvVars = requiredEnvVars.filter(
1446
- (varName) => !process.env[varName]
1447
- );
1448
- if (missingEnvVars.length > 0) {
1449
- status.status = "degraded";
1450
- status.details.missingEnvVars = missingEnvVars;
1451
- }
1452
-
1453
- // Determine overall health
1454
- if (status.status === "healthy") {
1455
- reply.send(status);
1456
- } else {
1457
- reply.status(500).send(status);
1458
- }
1459
- });
1460
-
1461
- /*
1462
- ===== Slugify =====
1463
- */
1464
- fastify.decorate('slugify', (string) => {
1465
- return string
1466
- .toString()
1467
- .toLowerCase() // Convert to lowercase
1468
- .trim() // Trim leading and trailing spaces
1469
- .replace(/\s+/g, '-') // Replace spaces with -
1470
- .replace(/[^\w\-]+/g, '') // Remove all non-word characters (allow only letters, numbers, and dashes)
1471
- .replace(/\-\-+/g, '-') // Replace multiple dashes with a single dash
1472
- .replace(/^-+/, '') // Trim leading dashes
1473
- .replace(/-+$/, ''); // Trim trailing dashes
1474
- });
1475
- /*
1476
- ===== Generate UUID =====
1477
- */
1478
- fastify.decorate('UUID', () => {
1479
- return randomUUID(); // Generate a UUID using uncrypto's randomUUID
1480
- });
1481
-
1482
- /*
1483
- ===== Ensure Proper Cleanup on Server Shutdown =====
1484
- */
1485
- fastify.addHook("onClose", async () => {
1486
- if (fastify.prisma) {
1487
- await fastify.prisma.$disconnect();
1488
- }
1489
- // Add additional cleanup for other services if necessary
1490
- });
1491
- }
1492
-
1493
- export default fp(xConfig, {
1494
- name: "xConfig",
1495
- });