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