@xenterprises/fastify-xconfig 1.0.0 → 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.
- package/.github/workflows/ci.yml +19 -0
- package/.taprc +3 -0
- package/README.md +18 -171
- package/index.d.ts +13 -0
- package/index.js +9 -0
- package/package.json +33 -39
- package/test/index.test-d.ts +13 -0
- package/test/index.test.js +14 -0
- package/test/xConfig.js +115 -0
- package/tsconfig.json +9 -0
- package/dist/integrations/cloudinary.d.ts +0 -1
- package/dist/integrations/cloudinary.js +0 -25
- package/dist/integrations/cloudinary.js.map +0 -1
- package/dist/integrations/prisma.d.ts +0 -1
- package/dist/integrations/prisma.js +0 -13
- package/dist/integrations/prisma.js.map +0 -1
- package/dist/integrations/sendgrid.d.ts +0 -1
- package/dist/integrations/sendgrid.js +0 -22
- package/dist/integrations/sendgrid.js.map +0 -1
- package/dist/integrations/stripe.d.ts +0 -1
- package/dist/integrations/stripe.js +0 -15
- package/dist/integrations/stripe.js.map +0 -1
- package/dist/integrations/twilio.d.ts +0 -1
- package/dist/integrations/twilio.js +0 -17
- package/dist/integrations/twilio.js.map +0 -1
- package/dist/middleware/bugsnag.d.ts +0 -2
- package/dist/middleware/bugsnag.js +0 -9
- package/dist/middleware/bugsnag.js.map +0 -1
- package/dist/middleware/cors.d.ts +0 -2
- package/dist/middleware/cors.js +0 -11
- package/dist/middleware/cors.js.map +0 -1
- package/dist/middleware/errorHandler.d.ts +0 -2
- package/dist/middleware/errorHandler.js +0 -19
- package/dist/middleware/errorHandler.js.map +0 -1
- package/dist/middleware/multipart.d.ts +0 -2
- package/dist/middleware/multipart.js +0 -7
- package/dist/middleware/multipart.js.map +0 -1
- package/dist/middleware/rateLimit.d.ts +0 -2
- package/dist/middleware/rateLimit.js +0 -7
- package/dist/middleware/rateLimit.js.map +0 -1
- package/dist/middleware/underPressure.d.ts +0 -2
- package/dist/middleware/underPressure.js +0 -7
- package/dist/middleware/underPressure.js.map +0 -1
- package/dist/utils/colorize.d.ts +0 -4
- package/dist/utils/colorize.js +0 -33
- package/dist/utils/colorize.js.map +0 -1
- package/dist/utils/formatBytes.d.ts +0 -1
- package/dist/utils/formatBytes.js +0 -10
- package/dist/utils/formatBytes.js.map +0 -1
- package/dist/utils/randomUUID.d.ts +0 -1
- package/dist/utils/randomUUID.js +0 -3
- package/dist/utils/randomUUID.js.map +0 -1
- package/dist/utils/statAsync.d.ts +0 -2
- package/dist/utils/statAsync.js +0 -4
- package/dist/utils/statAsync.js.map +0 -1
- package/dist/xConfig.d.ts +0 -3
- package/dist/xConfig.js +0 -9
- package/dist/xConfig.js.map +0 -1
- package/server/app.js +0 -85
- package/src/auth/admin.js +0 -181
- package/src/auth/portal.js +0 -177
- package/src/integrations/cloudinary.js +0 -98
- package/src/integrations/geocode.js +0 -43
- package/src/integrations/prisma.js +0 -30
- package/src/integrations/sendgrid.js +0 -58
- package/src/integrations/twilio.js +0 -146
- package/src/lifecycle/xFastifyAfter.js +0 -27
- package/src/middleware/bugsnag.js +0 -10
- package/src/middleware/cors.js +0 -10
- package/src/middleware/fancyErrors.js +0 -26
- package/src/middleware/multipart.js +0 -6
- package/src/middleware/rateLimit.js +0 -6
- package/src/middleware/underPressure.js +0 -6
- package/src/utils/colorize.js +0 -37
- package/src/utils/cookie.js +0 -5
- package/src/utils/formatBytes.js +0 -16
- package/src/utils/health.js +0 -126
- package/src/utils/xEcho.js +0 -12
- package/src/utils/xSlugify.js +0 -20
- package/src/utils/xUUID.js +0 -14
- package/src/xConfig.js +0 -117
- package/test/index.js +0 -17
- package/ts-reference/integrations/cloudinary.ts +0 -26
- package/ts-reference/integrations/prisma.ts +0 -13
- package/ts-reference/integrations/sendgrid.ts +0 -27
- package/ts-reference/integrations/stripe.ts +0 -15
- package/ts-reference/integrations/twilio.ts +0 -20
- package/ts-reference/middleware/bugsnag.ts +0 -10
- package/ts-reference/middleware/cors.ts +0 -13
- package/ts-reference/middleware/errorHandler.ts +0 -24
- package/ts-reference/middleware/multipart.ts +0 -8
- package/ts-reference/middleware/rateLimit.ts +0 -8
- package/ts-reference/middleware/underPressure.ts +0 -11
- package/ts-reference/utils/colorize.ts +0 -45
- package/ts-reference/utils/formatBytes.ts +0 -8
- package/ts-reference/utils/randomUUID.ts +0 -3
- package/ts-reference/utils/statAsync.ts +0 -4
- package/xConfigReference.js +0 -1526
- package/xConfigWorkingList.js +0 -720
package/xConfigReference.js
DELETED
|
@@ -1,1526 +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 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
|
|
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
|
-
* 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
|
-
* },
|
|
45
|
-
* },
|
|
46
|
-
* cors: { origin: ['https://your-frontend.com'], credentials: true },
|
|
47
|
-
* });
|
|
48
|
-
*
|
|
49
|
-
* fastify.listen({ port: 3000 });
|
|
50
|
-
* ```
|
|
51
|
-
*
|
|
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:*
|
|
100
|
-
* 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
|
-
*
|
|
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.
|
|
109
|
-
*
|
|
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
|
-
* ```
|
|
156
|
-
*/
|
|
157
|
-
|
|
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
|
-
// Register @fastify/cookie plugin
|
|
792
|
-
fastify.register(import("@fastify/cookie"));
|
|
793
|
-
|
|
794
|
-
console.info(" ✅ @fastify/cookie Enabled");
|
|
795
|
-
|
|
796
|
-
/*
|
|
797
|
-
===== Admin Authentication =====
|
|
798
|
-
*/
|
|
799
|
-
/*
|
|
800
|
-
===== Admin Authentication =====
|
|
801
|
-
*/
|
|
802
|
-
if (authOptions.admin?.active !== false) {
|
|
803
|
-
|
|
804
|
-
// Ensure the admin JWT secret is provided
|
|
805
|
-
if (!authOptions.admin.secret) {
|
|
806
|
-
throw new Error("Admin JWT secret must be provided.");
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
const adminAuthOptions = authOptions.admin;
|
|
810
|
-
const adminCookieName =
|
|
811
|
-
adminAuthOptions.cookieOptions?.name || "adminToken";
|
|
812
|
-
const adminRefreshCookieName =
|
|
813
|
-
adminAuthOptions.cookieOptions?.refreshTokenName || "adminRefreshToken";
|
|
814
|
-
const adminCookieOptions = {
|
|
815
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
816
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
817
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
818
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
819
|
-
};
|
|
820
|
-
const adminExcludedPaths = adminAuthOptions.excludedPaths || [
|
|
821
|
-
"/admin/auth/login",
|
|
822
|
-
"/admin/auth/logout",
|
|
823
|
-
];
|
|
824
|
-
|
|
825
|
-
// Decorator to hash admin passwords
|
|
826
|
-
async function hashAdminPassword(password) {
|
|
827
|
-
const saltRounds = 10; // Number of salt rounds for bcrypt (10 is generally a good default)
|
|
828
|
-
try {
|
|
829
|
-
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
|
830
|
-
return hashedPassword;
|
|
831
|
-
} catch (error) {
|
|
832
|
-
throw new Error("Failed to hash password: " + error.message);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
fastify.decorate("hashAdminPassword", hashAdminPassword);
|
|
837
|
-
|
|
838
|
-
// Register JWT for admin
|
|
839
|
-
await fastify.register(jwt, {
|
|
840
|
-
secret: adminAuthOptions.secret,
|
|
841
|
-
sign: { algorithm: 'HS256', expiresIn: adminAuthOptions.expiresIn || "15m" },
|
|
842
|
-
cookie: {
|
|
843
|
-
cookieName: adminCookieName,
|
|
844
|
-
signed: false,
|
|
845
|
-
},
|
|
846
|
-
namespace: "adminJwt",
|
|
847
|
-
jwtVerify: "adminJwtVerify",
|
|
848
|
-
jwtSign: "adminJwtSign",
|
|
849
|
-
});
|
|
850
|
-
|
|
851
|
-
// Common function to set tokens as cookies
|
|
852
|
-
const setAdminAuthCookies = (reply, accessToken, refreshToken) => {
|
|
853
|
-
reply.setCookie(adminCookieName, accessToken, adminCookieOptions);
|
|
854
|
-
reply.setCookie(adminRefreshCookieName, refreshToken, {
|
|
855
|
-
// ...adminCookieOptions,
|
|
856
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
857
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
858
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
859
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
860
|
-
});
|
|
861
|
-
};
|
|
862
|
-
|
|
863
|
-
// Admin authentication hook
|
|
864
|
-
fastify.addHook("onRequest", async (request, reply) => {
|
|
865
|
-
const url = request.url;
|
|
866
|
-
|
|
867
|
-
// Skip authentication for excluded paths
|
|
868
|
-
if (adminExcludedPaths.some((path) => url.startsWith(path))) {
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
if (url.startsWith("/admin")) {
|
|
873
|
-
try {
|
|
874
|
-
// Extract token from cookie or Authorization header
|
|
875
|
-
const authHeader = request.headers.authorization;
|
|
876
|
-
const authToken =
|
|
877
|
-
authHeader && authHeader.startsWith("Bearer ")
|
|
878
|
-
? authHeader.slice(7)
|
|
879
|
-
: null;
|
|
880
|
-
const token = request.cookies[adminCookieName] || authToken;
|
|
881
|
-
|
|
882
|
-
if (!token) {
|
|
883
|
-
throw fastify.httpErrors.unauthorized(
|
|
884
|
-
"Admin access token not provided"
|
|
885
|
-
);
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
// Verify access token
|
|
889
|
-
const decoded = await request.adminJwtVerify(token);
|
|
890
|
-
request.adminAuth = decoded; // Attach admin auth context
|
|
891
|
-
} catch (err) {
|
|
892
|
-
// Use built-in HTTP error handling
|
|
893
|
-
reply.send(
|
|
894
|
-
fastify.httpErrors.unauthorized("Invalid or expired access token")
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
});
|
|
899
|
-
|
|
900
|
-
// Admin login route
|
|
901
|
-
fastify.post("/admin/auth/login", async (req, reply) => {
|
|
902
|
-
try {
|
|
903
|
-
const { email, password } = req.body;
|
|
904
|
-
|
|
905
|
-
// Validate input
|
|
906
|
-
if (!email || !password) {
|
|
907
|
-
throw fastify.httpErrors.badRequest(
|
|
908
|
-
"Email and password are required"
|
|
909
|
-
);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Fetch admin from the database
|
|
913
|
-
const admin = await fastify.prisma.admins.findUnique({
|
|
914
|
-
where: { email },
|
|
915
|
-
});
|
|
916
|
-
if (!admin) {
|
|
917
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
// Compare passwords using bcrypt
|
|
921
|
-
const isValidPassword = await bcrypt.compare(password, admin.password);
|
|
922
|
-
if (!isValidPassword) {
|
|
923
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// Issue access token
|
|
927
|
-
const accessToken = await reply.adminJwtSign({ id: admin.id });
|
|
928
|
-
|
|
929
|
-
// Generate refresh token
|
|
930
|
-
const refreshToken = randomUUID();
|
|
931
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
932
|
-
|
|
933
|
-
// Store hashed refresh token in the database
|
|
934
|
-
await fastify.prisma.admins.update({
|
|
935
|
-
where: { id: admin.id },
|
|
936
|
-
data: { refreshToken: hashedRefreshToken },
|
|
937
|
-
});
|
|
938
|
-
|
|
939
|
-
// Set tokens as cookies
|
|
940
|
-
setAdminAuthCookies(reply, accessToken, refreshToken);
|
|
941
|
-
|
|
942
|
-
reply.send({ accessToken });
|
|
943
|
-
} catch (err) {
|
|
944
|
-
reply.send(err);
|
|
945
|
-
}
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
// Admin refresh token route
|
|
949
|
-
fastify.post("/admin/auth/refresh", async (req, reply) => {
|
|
950
|
-
try {
|
|
951
|
-
const adminAuth = req.adminAuth;
|
|
952
|
-
const refreshToken = req.cookies[adminRefreshCookieName];
|
|
953
|
-
if (!refreshToken) {
|
|
954
|
-
throw fastify.httpErrors.unauthorized("Refresh token not provided");
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Fetch admin from the database using the refresh token
|
|
958
|
-
const admin = await fastify.prisma.admins.findFirst({
|
|
959
|
-
where: { id: adminAuth.id, refreshToken: { not: null } },
|
|
960
|
-
});
|
|
961
|
-
if (!admin) {
|
|
962
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// Verify the refresh token
|
|
966
|
-
const isValid = await bcrypt.compare(refreshToken, admin.refreshToken);
|
|
967
|
-
if (!isValid) {
|
|
968
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Issue new access token
|
|
972
|
-
const accessToken = await reply.adminJwtSign({ id: admin.id });
|
|
973
|
-
|
|
974
|
-
// Generate new refresh token
|
|
975
|
-
const newRefreshToken = randomUUID();
|
|
976
|
-
const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
|
|
977
|
-
|
|
978
|
-
// Update refresh token in the database
|
|
979
|
-
await fastify.prisma.admins.update({
|
|
980
|
-
where: { id: admin.id },
|
|
981
|
-
data: { refreshToken: hashedNewRefreshToken },
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
// Set new tokens as cookies
|
|
985
|
-
setAdminAuthCookies(reply, accessToken, newRefreshToken);
|
|
986
|
-
|
|
987
|
-
reply.send({ accessToken });
|
|
988
|
-
} catch (err) {
|
|
989
|
-
reply.send(err);
|
|
990
|
-
}
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
// Admin logout route
|
|
994
|
-
fastify.post("/admin/auth/logout", async (req, reply) => {
|
|
995
|
-
try {
|
|
996
|
-
const adminAuth = req.adminAuth;
|
|
997
|
-
if (adminAuth) {
|
|
998
|
-
// Delete refresh token from the database
|
|
999
|
-
await fastify.prisma.admins.update({
|
|
1000
|
-
where: { id: adminAuth.id },
|
|
1001
|
-
data: { refreshToken: null },
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// Clear cookies
|
|
1006
|
-
reply.clearCookie(adminCookieName, { path: "/" });
|
|
1007
|
-
reply.clearCookie(adminRefreshCookieName, { path: "/" });
|
|
1008
|
-
|
|
1009
|
-
reply.send({ message: "Logged out successfully" });
|
|
1010
|
-
} catch (err) {
|
|
1011
|
-
reply.send(err);
|
|
1012
|
-
}
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
// Admin authentication status route
|
|
1016
|
-
fastify.get("/admin/auth/me", async (req, reply) => {
|
|
1017
|
-
try {
|
|
1018
|
-
const adminAuth = req.adminAuth;
|
|
1019
|
-
|
|
1020
|
-
// Fetch admin details from database
|
|
1021
|
-
const admin = await fastify.prisma.admins.findUnique({
|
|
1022
|
-
where: { id: adminAuth.id },
|
|
1023
|
-
select: { id: true, firstName: true, lastName: true, email: true },
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
if (!admin) {
|
|
1027
|
-
throw fastify.httpErrors.notFound("Admin not found");
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
reply.send(admin);
|
|
1031
|
-
} catch (err) {
|
|
1032
|
-
reply.send(err);
|
|
1033
|
-
}
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
console.info(" ✅ Auth Admin Enabled");
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/*
|
|
1040
|
-
===== User Authentication =====
|
|
1041
|
-
*/
|
|
1042
|
-
if (authOptions.user?.active !== false) {
|
|
1043
|
-
// Ensure the user JWT secret is provided
|
|
1044
|
-
if (!authOptions.user.secret) {
|
|
1045
|
-
throw new Error("User JWT secret must be provided.");
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
const userAuthOptions = authOptions.user;
|
|
1049
|
-
const userCookieName = userAuthOptions.cookieOptions?.name || "userToken";
|
|
1050
|
-
const userRefreshCookieName =
|
|
1051
|
-
userAuthOptions.cookieOptions?.refreshTokenName || "userRefreshToken";
|
|
1052
|
-
const userCookieOptions = {
|
|
1053
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
1054
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
1055
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
1056
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
1057
|
-
};
|
|
1058
|
-
const userExcludedPaths = userAuthOptions.excludedPaths || [
|
|
1059
|
-
"/portal/auth/login",
|
|
1060
|
-
"/portal/auth/logout",
|
|
1061
|
-
"/portal/auth/register",
|
|
1062
|
-
];
|
|
1063
|
-
|
|
1064
|
-
// Register JWT for user
|
|
1065
|
-
await fastify.register(jwt, {
|
|
1066
|
-
secret: userAuthOptions.secret,
|
|
1067
|
-
sign: { algorithm: 'HS256', expiresIn: userAuthOptions.expiresIn || "15m" },
|
|
1068
|
-
cookie: { cookieName: userCookieName, signed: false },
|
|
1069
|
-
namespace: "userJwt",
|
|
1070
|
-
jwtVerify: "userJwtVerify",
|
|
1071
|
-
jwtSign: "userJwtSign",
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
// Common function to set tokens as cookies
|
|
1075
|
-
const setAuthCookies = (reply, accessToken, refreshToken) => {
|
|
1076
|
-
reply.setCookie(userCookieName, accessToken, userCookieOptions);
|
|
1077
|
-
reply.setCookie(userRefreshCookieName, refreshToken, {
|
|
1078
|
-
// ...userCookieOptions,
|
|
1079
|
-
httpOnly: true, // Ensures the cookie is not accessible via JavaScript
|
|
1080
|
-
secure: isProduction, // true in production (HTTPS), false in development (HTTP)
|
|
1081
|
-
sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
|
|
1082
|
-
path: '/', // Ensure cookies are valid for the entire site
|
|
1083
|
-
});
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
// User authentication hook
|
|
1087
|
-
fastify.addHook("onRequest", async (request, reply) => {
|
|
1088
|
-
const url = request.url;
|
|
1089
|
-
|
|
1090
|
-
// Skip authentication for excluded paths
|
|
1091
|
-
if (userExcludedPaths.some((path) => url.startsWith(path))) {
|
|
1092
|
-
return;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
if (url.startsWith("/portal")) {
|
|
1096
|
-
try {
|
|
1097
|
-
// Extract token from cookie or Authorization header
|
|
1098
|
-
const authHeader = request.headers.authorization;
|
|
1099
|
-
const authToken =
|
|
1100
|
-
authHeader && authHeader.startsWith("Bearer ")
|
|
1101
|
-
? authHeader.slice(7)
|
|
1102
|
-
: null;
|
|
1103
|
-
const token = request.cookies[userCookieName] || authToken;
|
|
1104
|
-
|
|
1105
|
-
if (!token) {
|
|
1106
|
-
throw fastify.httpErrors.unauthorized(
|
|
1107
|
-
"User access token not provided"
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Verify access token using the namespaced verify method
|
|
1112
|
-
const decoded = await request.userJwtVerify(token);
|
|
1113
|
-
request.userAuth = decoded; // Attach user auth context
|
|
1114
|
-
} catch (err) {
|
|
1115
|
-
// Use built-in HTTP error handling
|
|
1116
|
-
reply.send(
|
|
1117
|
-
fastify.httpErrors.unauthorized("Invalid or expired access token")
|
|
1118
|
-
);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
// User registration route
|
|
1124
|
-
fastify.post("/portal/auth/register", async (req, reply) => {
|
|
1125
|
-
try {
|
|
1126
|
-
const { email, password, firstName, lastName } = req.body;
|
|
1127
|
-
|
|
1128
|
-
// Validate input
|
|
1129
|
-
if (!email || !password || !firstName || !lastName) {
|
|
1130
|
-
throw fastify.httpErrors.badRequest("Missing required fields");
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// Check if user already exists
|
|
1134
|
-
const existingUser = await fastify.prisma.users.findUnique({
|
|
1135
|
-
where: { email },
|
|
1136
|
-
});
|
|
1137
|
-
if (existingUser) {
|
|
1138
|
-
throw fastify.httpErrors.conflict("Email already in use");
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
// Hash the password
|
|
1142
|
-
const hashedPassword = await bcrypt.hash(password, 10);
|
|
1143
|
-
|
|
1144
|
-
// Create the user
|
|
1145
|
-
const user = await fastify.prisma.users.create({
|
|
1146
|
-
data: {
|
|
1147
|
-
email,
|
|
1148
|
-
password: hashedPassword,
|
|
1149
|
-
firstName,
|
|
1150
|
-
lastName,
|
|
1151
|
-
},
|
|
1152
|
-
});
|
|
1153
|
-
|
|
1154
|
-
// Send welcome email
|
|
1155
|
-
await fastify.sendGrid.sendEmail(email, auth?.user?.registerEmail?.subject, auth?.user?.registerEmail?.templateId, { name: firstName, email });
|
|
1156
|
-
|
|
1157
|
-
// Issue access token
|
|
1158
|
-
const accessToken = await reply.userJwtSign({ id: user.id });
|
|
1159
|
-
|
|
1160
|
-
// Generate refresh token
|
|
1161
|
-
const refreshToken = randomUUID();
|
|
1162
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
1163
|
-
|
|
1164
|
-
// Store hashed refresh token in the database
|
|
1165
|
-
await fastify.prisma.users.update({
|
|
1166
|
-
where: { id: user.id },
|
|
1167
|
-
data: { refreshToken: hashedRefreshToken },
|
|
1168
|
-
});
|
|
1169
|
-
|
|
1170
|
-
// Set tokens as cookies
|
|
1171
|
-
setAuthCookies(reply, accessToken, refreshToken);
|
|
1172
|
-
|
|
1173
|
-
reply.send({ accessToken });
|
|
1174
|
-
} catch (err) {
|
|
1175
|
-
reply.send(err);
|
|
1176
|
-
}
|
|
1177
|
-
});
|
|
1178
|
-
|
|
1179
|
-
// User login route
|
|
1180
|
-
fastify.post("/portal/auth/login", async (req, reply) => {
|
|
1181
|
-
try {
|
|
1182
|
-
const { email, password } = req.body;
|
|
1183
|
-
|
|
1184
|
-
if (!email || !password) {
|
|
1185
|
-
throw fastify.httpErrors.badRequest(
|
|
1186
|
-
"Email and password are required"
|
|
1187
|
-
);
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Fetch user from the database
|
|
1191
|
-
const user = await fastify.prisma.users.findUnique({
|
|
1192
|
-
where: { email },
|
|
1193
|
-
});
|
|
1194
|
-
if (!user) {
|
|
1195
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// Compare passwords using bcrypt
|
|
1199
|
-
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
1200
|
-
if (!isValidPassword) {
|
|
1201
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
// Issue access token
|
|
1205
|
-
const accessToken = await reply.userJwtSign({ id: user.id });
|
|
1206
|
-
|
|
1207
|
-
// Generate refresh token
|
|
1208
|
-
const refreshToken = randomUUID();
|
|
1209
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
1210
|
-
|
|
1211
|
-
// Store hashed refresh token in the database
|
|
1212
|
-
await fastify.prisma.users.update({
|
|
1213
|
-
where: { id: user.id },
|
|
1214
|
-
data: { refreshToken: hashedRefreshToken },
|
|
1215
|
-
});
|
|
1216
|
-
|
|
1217
|
-
// Set tokens as cookies
|
|
1218
|
-
setAuthCookies(reply, accessToken, refreshToken);
|
|
1219
|
-
|
|
1220
|
-
reply.send({ accessToken });
|
|
1221
|
-
} catch (err) {
|
|
1222
|
-
reply.send(err);
|
|
1223
|
-
}
|
|
1224
|
-
});
|
|
1225
|
-
|
|
1226
|
-
// User refresh token route
|
|
1227
|
-
fastify.post("/portal/auth/refresh", async (req, reply) => {
|
|
1228
|
-
try {
|
|
1229
|
-
const userAuth = req.userAuth;
|
|
1230
|
-
const refreshToken = req.cookies[userRefreshCookieName];
|
|
1231
|
-
if (!refreshToken) {
|
|
1232
|
-
throw fastify.httpErrors.unauthorized("Refresh token not provided");
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// Fetch user from the database using the refresh token
|
|
1236
|
-
const user = await fastify.prisma.users.findFirst({
|
|
1237
|
-
where: { id: userAuth?.id, refreshToken: { not: null } },
|
|
1238
|
-
});
|
|
1239
|
-
|
|
1240
|
-
if (!user) {
|
|
1241
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
// Verify the refresh token
|
|
1245
|
-
const isValid = await bcrypt.compare(refreshToken, user.refreshToken);
|
|
1246
|
-
if (!isValid) {
|
|
1247
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
// Issue new access token
|
|
1251
|
-
const accessToken = await reply.userJwtSign({ id: user.id });
|
|
1252
|
-
|
|
1253
|
-
// Generate new refresh token
|
|
1254
|
-
const newRefreshToken = randomUUID();
|
|
1255
|
-
const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
|
|
1256
|
-
|
|
1257
|
-
// Update refresh token in the database
|
|
1258
|
-
await fastify.prisma.users.update({
|
|
1259
|
-
where: { id: user.id },
|
|
1260
|
-
data: { refreshToken: hashedNewRefreshToken },
|
|
1261
|
-
});
|
|
1262
|
-
|
|
1263
|
-
// Set new tokens as cookies
|
|
1264
|
-
setAuthCookies(reply, accessToken, newRefreshToken);
|
|
1265
|
-
|
|
1266
|
-
reply.send({ accessToken });
|
|
1267
|
-
} catch (err) {
|
|
1268
|
-
reply.send(err);
|
|
1269
|
-
}
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
// User logout route
|
|
1273
|
-
fastify.post("/portal/auth/logout", async (req, reply) => {
|
|
1274
|
-
try {
|
|
1275
|
-
const userAuth = req.userAuth;
|
|
1276
|
-
if (userAuth) {
|
|
1277
|
-
// Delete refresh token from the database
|
|
1278
|
-
await fastify.prisma.users.update({
|
|
1279
|
-
where: { id: userAuth.id },
|
|
1280
|
-
data: { refreshToken: null },
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
// Clear cookies
|
|
1285
|
-
reply.clearCookie(userCookieName, { path: "/" });
|
|
1286
|
-
reply.clearCookie(userRefreshCookieName, { path: "/" });
|
|
1287
|
-
|
|
1288
|
-
reply.send({ message: "Logged out successfully" });
|
|
1289
|
-
} catch (err) {
|
|
1290
|
-
reply.send(err);
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
// User authentication status route
|
|
1295
|
-
fastify.get("/portal/auth/me", async (req, reply) => {
|
|
1296
|
-
try {
|
|
1297
|
-
const userAuth = req.userAuth;
|
|
1298
|
-
|
|
1299
|
-
// Fetch user details from database
|
|
1300
|
-
const user = await fastify.prisma.users.findUnique({
|
|
1301
|
-
where: { id: userAuth.id },
|
|
1302
|
-
select: { id: true, email: true, firstName: true, lastName: true, ...authOptions.user.me },
|
|
1303
|
-
});
|
|
1304
|
-
|
|
1305
|
-
if (!user) {
|
|
1306
|
-
throw fastify.httpErrors.notFound("User not found");
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
reply.send(user);
|
|
1310
|
-
} catch (err) {
|
|
1311
|
-
reply.send(err);
|
|
1312
|
-
}
|
|
1313
|
-
});
|
|
1314
|
-
|
|
1315
|
-
console.info(" ✅ Auth User Enabled");
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
/*
|
|
1319
|
-
===== GEOCODE =====
|
|
1320
|
-
*/
|
|
1321
|
-
if (geocodeOptions.active !== false) {
|
|
1322
|
-
if (!geocodeOptions.apiKey) {
|
|
1323
|
-
throw new Error("Geocode API key must be provided.");
|
|
1324
|
-
}
|
|
1325
|
-
const geocodioEndpoint = 'https://api.geocod.io/v1.7/geocode';
|
|
1326
|
-
const geocodioApiKey = geocodeOptions.apiKey;
|
|
1327
|
-
|
|
1328
|
-
fastify.decorate('geocode', {
|
|
1329
|
-
// Method to get lat/long and county from a zip code
|
|
1330
|
-
async getLatLongByZip(zipCode) {
|
|
1331
|
-
try {
|
|
1332
|
-
const url = `${geocodioEndpoint}?q=${zipCode}&fields=cd,stateleg&api_key=${geocodioApiKey}`;
|
|
1333
|
-
const response = await fetch(url);
|
|
1334
|
-
const result = await response.json();
|
|
1335
|
-
|
|
1336
|
-
if (result.results && result.results.length > 0) {
|
|
1337
|
-
const location = result.results[0].location;
|
|
1338
|
-
const addressComponents = result.results[0].address_components;
|
|
1339
|
-
|
|
1340
|
-
return {
|
|
1341
|
-
zip: zipCode,
|
|
1342
|
-
lat: location.lat,
|
|
1343
|
-
lng: location.lng,
|
|
1344
|
-
city: addressComponents.city,
|
|
1345
|
-
county: addressComponents.county,
|
|
1346
|
-
country: addressComponents.country,
|
|
1347
|
-
state: addressComponents.state,
|
|
1348
|
-
addressComponents: addressComponents
|
|
1349
|
-
};
|
|
1350
|
-
} else {
|
|
1351
|
-
throw new Error('No results found for the provided zip code.');
|
|
1352
|
-
}
|
|
1353
|
-
} catch (error) {
|
|
1354
|
-
fastify.log.error('Failed to fetch geolocation data:', error);
|
|
1355
|
-
throw new Error('Failed to fetch geolocation data.');
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
});
|
|
1359
|
-
|
|
1360
|
-
console.info(' ✅ Geocodio Enabled');
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
/*
|
|
1366
|
-
===== LIST ROUTES AFTER ALL PLUGINS =====
|
|
1367
|
-
Use the after() method to ensure this runs after all plugins are registered.
|
|
1368
|
-
*/
|
|
1369
|
-
fastify.after(() => {
|
|
1370
|
-
if (professional !== true) {
|
|
1371
|
-
console.info(" ✅ Listing Routes:");
|
|
1372
|
-
fastify.ready(() => {
|
|
1373
|
-
printRoutes(routes, options.colors !== false);
|
|
1374
|
-
// Add rocket emoji
|
|
1375
|
-
console.info(
|
|
1376
|
-
`🚀 Server is ready on port ${process.env.PORT || 3000}\n\n`
|
|
1377
|
-
);
|
|
1378
|
-
// Add goodbye emoji for server shutting down
|
|
1379
|
-
fastify.addHook("onClose", () =>
|
|
1380
|
-
console.info("Server shutting down... Goodbye 👋")
|
|
1381
|
-
);
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
});
|
|
1385
|
-
|
|
1386
|
-
/*
|
|
1387
|
-
* ===== HEALTH CHECK ROUTE =====
|
|
1388
|
-
*/
|
|
1389
|
-
|
|
1390
|
-
// Health Check Route
|
|
1391
|
-
fastify.get("/health", async (request, reply) => {
|
|
1392
|
-
const status = {
|
|
1393
|
-
status: "healthy",
|
|
1394
|
-
timestamp: new Date().toISOString(),
|
|
1395
|
-
uptime: process.uptime(), // Uptime in seconds
|
|
1396
|
-
version: "1.0.0", // Fetch dynamically if possible
|
|
1397
|
-
environment: process.env.NODE_ENV || "development",
|
|
1398
|
-
dependencies: {},
|
|
1399
|
-
resources: {},
|
|
1400
|
-
details: {},
|
|
1401
|
-
};
|
|
1402
|
-
|
|
1403
|
-
try {
|
|
1404
|
-
// Database connectivity check
|
|
1405
|
-
if (fastify.prisma) {
|
|
1406
|
-
await fastify.prisma.$queryRaw`SELECT 1`;
|
|
1407
|
-
status.dependencies.database = "up";
|
|
1408
|
-
} else {
|
|
1409
|
-
status.dependencies.database = "not configured";
|
|
1410
|
-
}
|
|
1411
|
-
} catch (error) {
|
|
1412
|
-
status.dependencies.database = "down";
|
|
1413
|
-
status.status = "degraded";
|
|
1414
|
-
status.details.databaseError = error.message;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
try {
|
|
1418
|
-
// Redis connectivity check
|
|
1419
|
-
if (fastify.redis) {
|
|
1420
|
-
await fastify.redis.ping();
|
|
1421
|
-
status.dependencies.redis = "up";
|
|
1422
|
-
} else {
|
|
1423
|
-
status.dependencies.redis = "not configured";
|
|
1424
|
-
}
|
|
1425
|
-
} catch (error) {
|
|
1426
|
-
status.dependencies.redis = "down";
|
|
1427
|
-
status.status = "degraded";
|
|
1428
|
-
status.details.redisError = error.message;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Resource usage
|
|
1432
|
-
const memoryUsage = process.memoryUsage();
|
|
1433
|
-
const loadAverage = os.loadavg();
|
|
1434
|
-
|
|
1435
|
-
status.resources.memory = {
|
|
1436
|
-
rss: formatBytes(memoryUsage.rss),
|
|
1437
|
-
heapTotal: formatBytes(memoryUsage.heapTotal),
|
|
1438
|
-
heapUsed: formatBytes(memoryUsage.heapUsed),
|
|
1439
|
-
external: formatBytes(memoryUsage.external),
|
|
1440
|
-
arrayBuffers: formatBytes(memoryUsage.arrayBuffers),
|
|
1441
|
-
};
|
|
1442
|
-
|
|
1443
|
-
status.resources.cpu = {
|
|
1444
|
-
loadAverage, // 1, 5, and 15 minute load averages
|
|
1445
|
-
cpus: os.cpus().length,
|
|
1446
|
-
};
|
|
1447
|
-
|
|
1448
|
-
// Disk space availability
|
|
1449
|
-
try {
|
|
1450
|
-
// Implement disk space check using 'check-disk-space' package
|
|
1451
|
-
// Install it via 'npm install check-disk-space'
|
|
1452
|
-
const checkDiskSpace = (await import("check-disk-space")).default;
|
|
1453
|
-
const diskSpace = await checkDiskSpace("/"); // Replace '/' with your drive or mount point
|
|
1454
|
-
status.resources.disk = {
|
|
1455
|
-
free: formatBytes(diskSpace.free),
|
|
1456
|
-
size: formatBytes(diskSpace.size),
|
|
1457
|
-
};
|
|
1458
|
-
} catch (error) {
|
|
1459
|
-
status.resources.disk = "unknown";
|
|
1460
|
-
status.details.diskError = error.message;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
// Application-specific checks
|
|
1464
|
-
// Example: Check if a scheduled job is running
|
|
1465
|
-
if (typeof isJobRunning === "function" && !isJobRunning()) {
|
|
1466
|
-
status.status = "degraded";
|
|
1467
|
-
status.details.jobStatus = "Scheduled job is not running";
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// Configuration validation
|
|
1471
|
-
const requiredEnvVars = [
|
|
1472
|
-
"DATABASE_URL",
|
|
1473
|
-
"ADMIN_JWT_SECRET",
|
|
1474
|
-
"USER_JWT_SECRET",
|
|
1475
|
-
];
|
|
1476
|
-
const missingEnvVars = requiredEnvVars.filter(
|
|
1477
|
-
(varName) => !process.env[varName]
|
|
1478
|
-
);
|
|
1479
|
-
if (missingEnvVars.length > 0) {
|
|
1480
|
-
status.status = "degraded";
|
|
1481
|
-
status.details.missingEnvVars = missingEnvVars;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
// Determine overall health
|
|
1485
|
-
if (status.status === "healthy") {
|
|
1486
|
-
reply.send(status);
|
|
1487
|
-
} else {
|
|
1488
|
-
reply.status(500).send(status);
|
|
1489
|
-
}
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
/*
|
|
1493
|
-
===== Slugify =====
|
|
1494
|
-
*/
|
|
1495
|
-
fastify.decorate('slugify', (string) => {
|
|
1496
|
-
return string
|
|
1497
|
-
.toString()
|
|
1498
|
-
.toLowerCase() // Convert to lowercase
|
|
1499
|
-
.trim() // Trim leading and trailing spaces
|
|
1500
|
-
.replace(/\s+/g, '-') // Replace spaces with -
|
|
1501
|
-
.replace(/[^\w\-]+/g, '') // Remove all non-word characters (allow only letters, numbers, and dashes)
|
|
1502
|
-
.replace(/\-\-+/g, '-') // Replace multiple dashes with a single dash
|
|
1503
|
-
.replace(/^-+/, '') // Trim leading dashes
|
|
1504
|
-
.replace(/-+$/, ''); // Trim trailing dashes
|
|
1505
|
-
});
|
|
1506
|
-
/*
|
|
1507
|
-
===== Generate UUID =====
|
|
1508
|
-
*/
|
|
1509
|
-
fastify.decorate('UUID', () => {
|
|
1510
|
-
return randomUUID(); // Generate a UUID using uncrypto's randomUUID
|
|
1511
|
-
});
|
|
1512
|
-
|
|
1513
|
-
/*
|
|
1514
|
-
===== Ensure Proper Cleanup on Server Shutdown =====
|
|
1515
|
-
*/
|
|
1516
|
-
fastify.addHook("onClose", async () => {
|
|
1517
|
-
if (fastify.prisma) {
|
|
1518
|
-
await fastify.prisma.$disconnect();
|
|
1519
|
-
}
|
|
1520
|
-
// Add additional cleanup for other services if necessary
|
|
1521
|
-
});
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
export default fp(xConfig, {
|
|
1525
|
-
name: "xConfig",
|
|
1526
|
-
});
|