@xenterprises/fastify-xconfig 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +145 -0
  2. package/dist/integrations/cloudinary.d.ts +1 -0
  3. package/dist/integrations/cloudinary.js +25 -0
  4. package/dist/integrations/cloudinary.js.map +1 -0
  5. package/dist/integrations/prisma.d.ts +1 -0
  6. package/dist/integrations/prisma.js +13 -0
  7. package/dist/integrations/prisma.js.map +1 -0
  8. package/dist/integrations/sendgrid.d.ts +1 -0
  9. package/dist/integrations/sendgrid.js +22 -0
  10. package/dist/integrations/sendgrid.js.map +1 -0
  11. package/dist/integrations/stripe.d.ts +1 -0
  12. package/dist/integrations/stripe.js +15 -0
  13. package/dist/integrations/stripe.js.map +1 -0
  14. package/dist/integrations/twilio.d.ts +1 -0
  15. package/dist/integrations/twilio.js +17 -0
  16. package/dist/integrations/twilio.js.map +1 -0
  17. package/dist/middleware/bugsnag.d.ts +2 -0
  18. package/dist/middleware/bugsnag.js +9 -0
  19. package/dist/middleware/bugsnag.js.map +1 -0
  20. package/dist/middleware/cors.d.ts +2 -0
  21. package/dist/middleware/cors.js +11 -0
  22. package/dist/middleware/cors.js.map +1 -0
  23. package/dist/middleware/errorHandler.d.ts +2 -0
  24. package/dist/middleware/errorHandler.js +19 -0
  25. package/dist/middleware/errorHandler.js.map +1 -0
  26. package/dist/middleware/multipart.d.ts +2 -0
  27. package/dist/middleware/multipart.js +7 -0
  28. package/dist/middleware/multipart.js.map +1 -0
  29. package/dist/middleware/rateLimit.d.ts +2 -0
  30. package/dist/middleware/rateLimit.js +7 -0
  31. package/dist/middleware/rateLimit.js.map +1 -0
  32. package/dist/middleware/underPressure.d.ts +2 -0
  33. package/dist/middleware/underPressure.js +7 -0
  34. package/dist/middleware/underPressure.js.map +1 -0
  35. package/dist/utils/colorize.d.ts +4 -0
  36. package/dist/utils/colorize.js +33 -0
  37. package/dist/utils/colorize.js.map +1 -0
  38. package/dist/utils/formatBytes.d.ts +1 -0
  39. package/dist/utils/formatBytes.js +10 -0
  40. package/dist/utils/formatBytes.js.map +1 -0
  41. package/dist/utils/randomUUID.d.ts +1 -0
  42. package/dist/utils/randomUUID.js +3 -0
  43. package/dist/utils/randomUUID.js.map +1 -0
  44. package/dist/utils/statAsync.d.ts +2 -0
  45. package/dist/utils/statAsync.js +4 -0
  46. package/dist/utils/statAsync.js.map +1 -0
  47. package/dist/xConfig.d.ts +3 -0
  48. package/dist/xConfig.js +9 -0
  49. package/dist/xConfig.js.map +1 -0
  50. package/package.json +25 -2
  51. package/server/app.js +78 -1
  52. package/src/auth/admin.js +241 -0
  53. package/src/auth/portal.js +284 -0
  54. package/src/integrations/cloudinary.js +98 -0
  55. package/src/integrations/geocode.js +43 -0
  56. package/src/integrations/prisma.js +30 -0
  57. package/src/integrations/sendgrid.js +57 -0
  58. package/src/integrations/twilio.js +146 -0
  59. package/src/lifecycle/xFastifyAfter.js +27 -0
  60. package/src/middleware/bugsnag.js +10 -0
  61. package/src/middleware/cors.js +10 -0
  62. package/src/middleware/fancyErrors.js +26 -0
  63. package/src/middleware/multipart.js +6 -0
  64. package/src/middleware/rateLimit.js +6 -0
  65. package/src/middleware/underPressure.js +6 -0
  66. package/src/utils/colorize.js +37 -0
  67. package/src/utils/cookie.js +5 -0
  68. package/src/utils/formatBytes.js +16 -0
  69. package/src/utils/health.js +126 -0
  70. package/src/utils/xEcho.js +12 -0
  71. package/src/utils/xSlugify.js +20 -0
  72. package/src/utils/xUUID.js +14 -0
  73. package/src/xConfig.js +110 -3
  74. package/ts-reference/integrations/cloudinary.ts +26 -0
  75. package/ts-reference/integrations/prisma.ts +13 -0
  76. package/ts-reference/integrations/sendgrid.ts +27 -0
  77. package/ts-reference/integrations/stripe.ts +15 -0
  78. package/ts-reference/integrations/twilio.ts +20 -0
  79. package/ts-reference/middleware/bugsnag.ts +10 -0
  80. package/ts-reference/middleware/cors.ts +13 -0
  81. package/ts-reference/middleware/errorHandler.ts +24 -0
  82. package/ts-reference/middleware/multipart.ts +8 -0
  83. package/ts-reference/middleware/rateLimit.ts +8 -0
  84. package/ts-reference/middleware/underPressure.ts +11 -0
  85. package/ts-reference/utils/colorize.ts +45 -0
  86. package/ts-reference/utils/formatBytes.ts +8 -0
  87. package/ts-reference/utils/randomUUID.ts +3 -0
  88. package/ts-reference/utils/statAsync.ts +4 -0
  89. package/xConfigReference.js +1495 -0
  90. package/xConfigWorkingList.js +720 -0
  91. package/tsconfig.json +0 -16
@@ -0,0 +1,241 @@
1
+ import jwt from "@fastify/jwt";
2
+ import bcrypt from "bcrypt";
3
+ const isProduction = process.env.NODE_ENV === 'production';
4
+ export async function setupAdminAuth(fastify, options) {
5
+ if (options.admin?.active !== false) {
6
+
7
+ // Ensure the admin JWT secret is provided
8
+ if (!options.admin.secret) {
9
+ throw new Error("Admin JWT secret must be provided.");
10
+ }
11
+
12
+ const adminAuthOptions = options.admin;
13
+ const adminCookieName =
14
+ adminAuthOptions.cookieOptions?.name || "adminToken";
15
+ const adminRefreshCookieName =
16
+ adminAuthOptions.cookieOptions?.refreshTokenName || "adminRefreshToken";
17
+ const adminCookieOptions = {
18
+ httpOnly: true, // Ensures the cookie is not accessible via JavaScript
19
+ secure: isProduction, // true in production (HTTPS), false in development (HTTP)
20
+ sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
21
+ path: '/', // Ensure cookies are valid for the entire site
22
+ };
23
+ const adminExcludedPaths = adminAuthOptions.excludedPaths || [
24
+ "/admin/auth/login",
25
+ "/admin/auth/logout",
26
+ ];
27
+
28
+ // Decorator to hash admin passwords
29
+ async function hashAdminPassword(password) {
30
+ const saltRounds = 10; // Number of salt rounds for bcrypt (10 is generally a good default)
31
+ try {
32
+ const hashedPassword = await bcrypt.hash(password, saltRounds);
33
+ return hashedPassword;
34
+ } catch (error) {
35
+ throw new Error("Failed to hash password: " + error.message);
36
+ }
37
+ }
38
+
39
+ fastify.decorate("hashAdminPassword", hashAdminPassword);
40
+
41
+ // Register JWT for admin
42
+ await fastify.register(jwt, {
43
+ secret: adminAuthOptions.secret,
44
+ sign: { algorithm: 'HS256', expiresIn: adminAuthOptions.expiresIn || "15m" },
45
+ cookie: {
46
+ cookieName: adminCookieName,
47
+ signed: false,
48
+ },
49
+ namespace: "adminJwt",
50
+ jwtVerify: "adminJwtVerify",
51
+ jwtSign: "adminJwtSign",
52
+ });
53
+
54
+ // Common function to set tokens as cookies
55
+ const setAdminAuthCookies = (reply, accessToken, refreshToken) => {
56
+ reply.setCookie(adminCookieName, accessToken, adminCookieOptions);
57
+ reply.setCookie(adminRefreshCookieName, refreshToken, {
58
+ // ...adminCookieOptions,
59
+ httpOnly: true, // Ensures the cookie is not accessible via JavaScript
60
+ secure: isProduction, // true in production (HTTPS), false in development (HTTP)
61
+ sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
62
+ path: '/', // Ensure cookies are valid for the entire site
63
+ });
64
+ };
65
+
66
+ // Admin authentication hook
67
+ fastify.addHook("onRequest", async (request, reply) => {
68
+ const url = request.url;
69
+
70
+ // Skip authentication for excluded paths
71
+ if (adminExcludedPaths.some((path) => url.startsWith(path))) {
72
+ return;
73
+ }
74
+
75
+ if (url.startsWith("/admin")) {
76
+ try {
77
+ // Extract token from cookie or Authorization header
78
+ const authHeader = request.headers.authorization;
79
+ const authToken =
80
+ authHeader && authHeader.startsWith("Bearer ")
81
+ ? authHeader.slice(7)
82
+ : null;
83
+ const token = request.cookies[adminCookieName] || authToken;
84
+
85
+ if (!token) {
86
+ throw fastify.httpErrors.unauthorized(
87
+ "Admin access token not provided"
88
+ );
89
+ }
90
+
91
+ // Verify access token
92
+ const decoded = await request.adminJwtVerify(token);
93
+ request.adminAuth = decoded; // Attach admin auth context
94
+ } catch (err) {
95
+ // Use built-in HTTP error handling
96
+ reply.send(
97
+ fastify.httpErrors.unauthorized("Invalid or expired access token")
98
+ );
99
+ }
100
+ }
101
+ });
102
+
103
+ // Admin login route
104
+ fastify.post("/admin/auth/login", async (req, reply) => {
105
+ try {
106
+ const { email, password } = req.body;
107
+
108
+ // Validate input
109
+ if (!email || !password) {
110
+ throw fastify.httpErrors.badRequest(
111
+ "Email and password are required"
112
+ );
113
+ }
114
+
115
+ // Fetch admin from the database
116
+ const admin = await fastify.prisma.admins.findUnique({
117
+ where: { email },
118
+ });
119
+ if (!admin) {
120
+ throw fastify.httpErrors.unauthorized("Invalid credentials");
121
+ }
122
+
123
+ // Compare passwords using bcrypt
124
+ const isValidPassword = await bcrypt.compare(password, admin.password);
125
+ if (!isValidPassword) {
126
+ throw fastify.httpErrors.unauthorized("Invalid credentials");
127
+ }
128
+
129
+ // Issue access token
130
+ const accessToken = await reply.adminJwtSign({ id: admin.id });
131
+
132
+ // Generate refresh token
133
+ const refreshToken = await fastify.randomUUID();
134
+ const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
135
+
136
+ // Store hashed refresh token in the database
137
+ await fastify.prisma.admins.update({
138
+ where: { id: admin.id },
139
+ data: { refreshToken: hashedRefreshToken },
140
+ });
141
+
142
+ // Set tokens as cookies
143
+ setAdminAuthCookies(reply, accessToken, refreshToken);
144
+
145
+ reply.send({ accessToken });
146
+ } catch (err) {
147
+ reply.send(err);
148
+ }
149
+ });
150
+
151
+ // Admin refresh token route
152
+ fastify.post("/admin/auth/refresh", async (req, reply) => {
153
+ try {
154
+ const adminAuth = req.adminAuth;
155
+ const refreshToken = req.cookies[adminRefreshCookieName];
156
+ if (!refreshToken) {
157
+ throw fastify.httpErrors.unauthorized("Refresh token not provided");
158
+ }
159
+
160
+ // Fetch admin from the database using the refresh token
161
+ const admin = await fastify.prisma.admins.findFirst({
162
+ where: { id: adminAuth.id, refreshToken: { not: null } },
163
+ });
164
+ if (!admin) {
165
+ throw fastify.httpErrors.unauthorized("Invalid refresh token");
166
+ }
167
+
168
+ // Verify the refresh token
169
+ const isValid = await bcrypt.compare(refreshToken, admin.refreshToken);
170
+ if (!isValid) {
171
+ throw fastify.httpErrors.unauthorized("Invalid refresh token");
172
+ }
173
+
174
+ // Issue new access token
175
+ const accessToken = await reply.adminJwtSign({ id: admin.id });
176
+
177
+ // Generate new refresh token
178
+ const newRefreshToken = await fastify.randomUUID();
179
+ const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
180
+
181
+ // Update refresh token in the database
182
+ await fastify.prisma.admins.update({
183
+ where: { id: admin.id },
184
+ data: { refreshToken: hashedNewRefreshToken },
185
+ });
186
+
187
+ // Set new tokens as cookies
188
+ setAdminAuthCookies(reply, accessToken, newRefreshToken);
189
+
190
+ reply.send({ accessToken });
191
+ } catch (err) {
192
+ reply.send(err);
193
+ }
194
+ });
195
+
196
+ // Admin logout route
197
+ fastify.post("/admin/auth/logout", async (req, reply) => {
198
+ try {
199
+ const adminAuth = req.adminAuth;
200
+ if (adminAuth) {
201
+ // Delete refresh token from the database
202
+ await fastify.prisma.admins.update({
203
+ where: { id: adminAuth.id },
204
+ data: { refreshToken: null },
205
+ });
206
+ }
207
+
208
+ // Clear cookies
209
+ reply.clearCookie(adminCookieName, { path: "/" });
210
+ reply.clearCookie(adminRefreshCookieName, { path: "/" });
211
+
212
+ reply.send({ message: "Logged out successfully" });
213
+ } catch (err) {
214
+ reply.send(err);
215
+ }
216
+ });
217
+
218
+ // Admin authentication status route
219
+ fastify.get("/admin/auth/me", async (req, reply) => {
220
+ try {
221
+ const adminAuth = req.adminAuth;
222
+
223
+ // Fetch admin details from database
224
+ const admin = await fastify.prisma.admins.findUnique({
225
+ where: { id: adminAuth.id },
226
+ select: { id: true, firstName: true, lastName: true, email: true },
227
+ });
228
+
229
+ if (!admin) {
230
+ throw fastify.httpErrors.notFound("Admin not found");
231
+ }
232
+
233
+ reply.send(admin);
234
+ } catch (err) {
235
+ reply.send(err);
236
+ }
237
+ });
238
+
239
+ console.info(" ✅ Auth Admin Enabled");
240
+ }
241
+ }
@@ -0,0 +1,284 @@
1
+ import jwt from "@fastify/jwt";
2
+ import bcrypt from "bcrypt";
3
+ import { config } from "process";
4
+ const isProduction = process.env.NODE_ENV === 'production';
5
+ export async function setupAuth(fastify, options) {
6
+ if (options.user?.active !== false) {
7
+ // Ensure the user JWT secret is provided
8
+ if (!options.user.secret) {
9
+ throw new Error("User JWT secret must be provided.");
10
+ }
11
+
12
+ const userAuthOptions = options.user;
13
+ const userCookieName = userAuthOptions.cookieOptions?.name || "userToken";
14
+ const userRefreshCookieName =
15
+ userAuthOptions.cookieOptions?.refreshTokenName || "userRefreshToken";
16
+ const userCookieOptions = {
17
+ httpOnly: true, // Ensures the cookie is not accessible via JavaScript
18
+ secure: isProduction, // true in production (HTTPS), false in development (HTTP)
19
+ sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
20
+ path: '/', // Ensure cookies are valid for the entire site
21
+ };
22
+ const userExcludedPaths = userAuthOptions.excludedPaths || [
23
+ "/portal/auth/login",
24
+ "/portal/auth/logout",
25
+ "/portal/auth/register",
26
+ ];
27
+
28
+ // Register JWT for user
29
+ await fastify.register(jwt, {
30
+ secret: userAuthOptions.secret,
31
+ sign: { algorithm: 'HS256', expiresIn: userAuthOptions.expiresIn || "15m" },
32
+ cookie: { cookieName: userCookieName, signed: false },
33
+ namespace: "userJwt",
34
+ jwtVerify: "userJwtVerify",
35
+ jwtSign: "userJwtSign",
36
+ });
37
+
38
+ // Common function to set tokens as cookies
39
+ const setAuthCookies = (reply, accessToken, refreshToken) => {
40
+ reply.setCookie(userCookieName, accessToken, userCookieOptions);
41
+ reply.setCookie(userRefreshCookieName, refreshToken, {
42
+ // ...userCookieOptions,
43
+ httpOnly: true, // Ensures the cookie is not accessible via JavaScript
44
+ secure: isProduction, // true in production (HTTPS), false in development (HTTP)
45
+ sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
46
+ path: '/', // Ensure cookies are valid for the entire site
47
+ });
48
+ };
49
+
50
+ // User authentication hook
51
+ fastify.addHook("onRequest", async (request, reply) => {
52
+ const url = request.url;
53
+
54
+ // Skip authentication for excluded paths
55
+ if (userExcludedPaths.some((path) => url.startsWith(path))) {
56
+ return;
57
+ }
58
+
59
+ if (url.startsWith("/portal")) {
60
+ try {
61
+ // Extract token from cookie or Authorization header
62
+ const authHeader = request.headers.authorization;
63
+ const authToken =
64
+ authHeader && authHeader.startsWith("Bearer ")
65
+ ? authHeader.slice(7)
66
+ : null;
67
+ const token = request.cookies[userCookieName] || authToken;
68
+
69
+ if (!token) {
70
+ throw fastify.httpErrors.unauthorized(
71
+ "User access token not provided"
72
+ );
73
+ }
74
+
75
+ // Verify access token using the namespaced verify method
76
+ const decoded = await request.userJwtVerify(token);
77
+ request.userAuth = decoded; // Attach user auth context
78
+ } catch (err) {
79
+ // Use built-in HTTP error handling
80
+ reply.send(
81
+ fastify.httpErrors.unauthorized("Invalid or expired access token")
82
+ );
83
+ }
84
+ }
85
+ });
86
+
87
+ // User registration route
88
+ fastify.post("/portal/auth/register", async (req, reply) => {
89
+ try {
90
+ const { email, password, firstName, lastName } = req.body;
91
+
92
+ // Validate input
93
+ if (!email || !password || !firstName || !lastName) {
94
+ throw fastify.httpErrors.badRequest("Missing required fields");
95
+ }
96
+
97
+ // Check if user already exists
98
+ const existingUser = await fastify.prisma.users.findUnique({
99
+ where: { email },
100
+ });
101
+ if (existingUser) {
102
+ throw fastify.httpErrors.conflict("Email already in use");
103
+ }
104
+
105
+ // Hash the password
106
+ const hashedPassword = await bcrypt.hash(password, 10);
107
+
108
+ // Create the user
109
+ const user = await fastify.prisma.users.create({
110
+ data: {
111
+ email,
112
+ password: hashedPassword,
113
+ firstName,
114
+ lastName,
115
+ },
116
+ });
117
+
118
+ // Send welcome email
119
+ if (options.user?.sendWelcomeEmail) {
120
+ await fastify.sendGrid.sendEmail(email, auth?.user?.registerEmail?.subject, auth?.user?.registerEmail?.templateId, { name: firstName, email });
121
+ }
122
+
123
+
124
+ // Issue access token
125
+ const accessToken = await reply.userJwtSign({ id: user.id });
126
+
127
+ // Generate refresh token
128
+ const refreshToken = await fastify.randomUUID();
129
+ const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
130
+
131
+ // Store hashed refresh token in the database
132
+ await fastify.prisma.users.update({
133
+ where: { id: user.id },
134
+ data: { refreshToken: hashedRefreshToken },
135
+ });
136
+
137
+ // Set tokens as cookies
138
+ setAuthCookies(reply, accessToken, refreshToken);
139
+
140
+ reply.send({ accessToken });
141
+ } catch (err) {
142
+ reply.send(err);
143
+ }
144
+ });
145
+
146
+ // User login route
147
+ fastify.post("/portal/auth/login", async (req, reply) => {
148
+ try {
149
+ const { email, password } = req.body;
150
+
151
+ if (!email || !password) {
152
+ throw fastify.httpErrors.badRequest(
153
+ "Email and password are required"
154
+ );
155
+ }
156
+
157
+ // Fetch user from the database
158
+ const user = await fastify.prisma.users.findUnique({
159
+ where: { email },
160
+ });
161
+ if (!user) {
162
+ throw fastify.httpErrors.unauthorized("Invalid credentials");
163
+ }
164
+
165
+ // Compare passwords using bcrypt
166
+ const isValidPassword = await bcrypt.compare(password, user.password);
167
+ if (!isValidPassword) {
168
+ throw fastify.httpErrors.unauthorized("Invalid credentials");
169
+ }
170
+
171
+ // Issue access token
172
+ const accessToken = await reply.userJwtSign({ id: user.id });
173
+
174
+ // Generate refresh token
175
+ const refreshToken = await fastify.randomUUID();
176
+ const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
177
+
178
+ // Store hashed refresh token in the database
179
+ await fastify.prisma.users.update({
180
+ where: { id: user.id },
181
+ data: { refreshToken: hashedRefreshToken },
182
+ });
183
+
184
+ // Set tokens as cookies
185
+ setAuthCookies(reply, accessToken, refreshToken);
186
+
187
+ reply.send({ accessToken });
188
+ } catch (err) {
189
+ reply.send(err);
190
+ }
191
+ });
192
+
193
+ // User refresh token route
194
+ fastify.post("/portal/auth/refresh", async (req, reply) => {
195
+ try {
196
+ const userAuth = req.userAuth;
197
+ const refreshToken = req.cookies[userRefreshCookieName];
198
+ if (!refreshToken) {
199
+ throw fastify.httpErrors.unauthorized("Refresh token not provided");
200
+ }
201
+
202
+ // Fetch user from the database using the refresh token
203
+ const user = await fastify.prisma.users.findFirst({
204
+ where: { id: userAuth?.id, refreshToken: { not: null } },
205
+ });
206
+
207
+ if (!user) {
208
+ throw fastify.httpErrors.unauthorized("Invalid refresh token");
209
+ }
210
+
211
+ // Verify the refresh token
212
+ const isValid = await bcrypt.compare(refreshToken, user.refreshToken);
213
+ if (!isValid) {
214
+ throw fastify.httpErrors.unauthorized("Invalid refresh token");
215
+ }
216
+
217
+ // Issue new access token
218
+ const accessToken = await reply.userJwtSign({ id: user.id });
219
+
220
+ // Generate new refresh token
221
+ const newRefreshToken = randomUUID();
222
+ const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
223
+
224
+ // Update refresh token in the database
225
+ await fastify.prisma.users.update({
226
+ where: { id: user.id },
227
+ data: { refreshToken: hashedNewRefreshToken },
228
+ });
229
+
230
+ // Set new tokens as cookies
231
+ setAuthCookies(reply, accessToken, newRefreshToken);
232
+
233
+ reply.send({ accessToken });
234
+ } catch (err) {
235
+ reply.send(err);
236
+ }
237
+ });
238
+
239
+ // User logout route
240
+ fastify.post("/portal/auth/logout", async (req, reply) => {
241
+ try {
242
+ const userAuth = req.userAuth;
243
+ if (userAuth) {
244
+ // Delete refresh token from the database
245
+ await fastify.prisma.users.update({
246
+ where: { id: userAuth.id },
247
+ data: { refreshToken: null },
248
+ });
249
+ }
250
+
251
+ // Clear cookies
252
+ reply.clearCookie(userCookieName, { path: "/" });
253
+ reply.clearCookie(userRefreshCookieName, { path: "/" });
254
+
255
+ reply.send({ message: "Logged out successfully" });
256
+ } catch (err) {
257
+ reply.send(err);
258
+ }
259
+ });
260
+
261
+ // User authentication status route
262
+ fastify.get("/portal/auth/me", async (req, reply) => {
263
+ try {
264
+ const userAuth = req.userAuth;
265
+
266
+ // Fetch user details from database
267
+ const user = await fastify.prisma.users.findUnique({
268
+ where: { id: userAuth.id },
269
+ select: { id: true, email: true, firstName: true, lastName: true, ...authOptions.user.me },
270
+ });
271
+
272
+ if (!user) {
273
+ throw fastify.httpErrors.notFound("User not found");
274
+ }
275
+
276
+ reply.send(user);
277
+ } catch (err) {
278
+ reply.send(err);
279
+ }
280
+ });
281
+
282
+ console.info(" ✅ Auth User Enabled");
283
+ }
284
+ }
@@ -0,0 +1,98 @@
1
+ import { v2 as Cloudinary } from "cloudinary";
2
+
3
+ export async function setupCloudinary(fastify, options) {
4
+
5
+ if (options.active !== false) {
6
+ if (
7
+ !options.cloudName ||
8
+ !options.apiKey ||
9
+ !options.apiSecret
10
+ ) {
11
+ throw new Error(
12
+ "Cloudinary cloudName, apiKey, and apiSecret must be provided."
13
+ );
14
+ }
15
+
16
+ Cloudinary.config({
17
+ cloud_name: options.cloudName,
18
+ api_key: options.apiKey,
19
+ api_secret: options.apiSecret,
20
+ },
21
+ );
22
+
23
+ fastify.decorate('cloudinary', {
24
+ upload: async (fileStream, options = {}) => {
25
+ return new Promise((resolve, reject) => {
26
+ if (options.folder) {
27
+ options.folder = options.folder || options.folder;
28
+ }
29
+ options.resource_type = options.resource_type || 'auto';
30
+ options.timestamp = Math.floor(Date.now() / 1000); // Add timestamp to options
31
+ options.use_filename = options.use_filename !== undefined ? options.use_filename : true;
32
+ options.unique_filename = options.unique_filename !== undefined ? options.unique_filename : true;
33
+
34
+ const uploadStream = Cloudinary.uploader.upload_stream(options, (error, result) => {
35
+ if (error) {
36
+ fastify.log.error("Cloudinary upload failed:", error);
37
+ reject(new Error(`Failed to upload to Cloudinary: ${JSON.stringify(error)}`));
38
+ } else {
39
+ resolve(result); // Resolve with the result from Cloudinary
40
+ }
41
+ });
42
+
43
+ fileStream.pipe(uploadStream)
44
+ .on('error', (streamError) => {
45
+ fastify.log.error("Stream error:", streamError);
46
+ reject(new Error(`Stream error during Cloudinary upload: ${streamError.message}`));
47
+ })
48
+ .on('end', () => {
49
+ fastify.log.info("Stream ended successfully.");
50
+ });
51
+ });
52
+ },
53
+
54
+ uploadLarge: async (fileStream, options = {}) => {
55
+ return new Promise((resolve, reject) => {
56
+ if (options.folder) {
57
+ options.folder = options.folder || options.folder;
58
+ }
59
+ options.resource_type = options.resource_type || 'auto';
60
+ options.timestamp = Math.floor(Date.now() / 1000);
61
+ options.use_filename = options.use_filename !== undefined ? options.use_filename : true;
62
+ options.unique_filename = options.unique_filename !== undefined ? options.unique_filename : true;
63
+ options.overwrite = options.overwrite !== undefined ? options.overwrite : false;
64
+
65
+ const uploadStream = Cloudinary.uploader.upload_stream(options, (error, result) => {
66
+ if (error) {
67
+ fastify.log.error("Cloudinary upload failed:", error);
68
+ reject(new Error(`Failed to upload to Cloudinary: ${JSON.stringify(error)}`));
69
+ } else {
70
+ resolve(result);
71
+ }
72
+ });
73
+
74
+ fileStream.pipe(uploadStream)
75
+ .on('error', (streamError) => {
76
+ fastify.log.error("Stream error:", streamError);
77
+ reject(new Error(`Stream error during Cloudinary upload: ${streamError.message}`));
78
+ })
79
+ .on('end', () => {
80
+ fastify.log.info("Stream ended successfully.");
81
+ });
82
+ });
83
+ },
84
+
85
+ delete: async (publicId) => {
86
+ try {
87
+ const response = await Cloudinary.uploader.destroy(publicId);
88
+ return response;
89
+ } catch (error) {
90
+ fastify.log.error("Cloudinary delete failed:", error);
91
+ throw new Error("Failed to delete from Cloudinary.");
92
+ }
93
+ }
94
+ });
95
+
96
+ console.info(" ✅ Cloudinary Enabled");
97
+ }
98
+ }
@@ -0,0 +1,43 @@
1
+ export async function setupGeocode(fastify, options) {
2
+ if (options.active !== false) {
3
+ if (!options.apiKey) {
4
+ throw new Error("Geocode API key must be provided.");
5
+ }
6
+ const geocodioEndpoint = 'https://api.geocod.io/v1.7/geocode';
7
+ const geocodioApiKey = options.apiKey;
8
+
9
+ fastify.decorate('geocode', {
10
+ // Method to get lat/long and county from a zip code
11
+ async getLatLongByZip(zipCode) {
12
+ try {
13
+ const url = `${geocodioEndpoint}?q=${zipCode}&fields=cd,stateleg&api_key=${geocodioApiKey}`;
14
+ const response = await fetch(url);
15
+ const result = await response.json();
16
+
17
+ if (result.results && result.results.length > 0) {
18
+ const location = result.results[0].location;
19
+ const addressComponents = result.results[0].address_components;
20
+
21
+ return {
22
+ zip: zipCode,
23
+ lat: location.lat,
24
+ lng: location.lng,
25
+ city: addressComponents.city,
26
+ county: addressComponents.county,
27
+ country: addressComponents.country,
28
+ state: addressComponents.state,
29
+ addressComponents: addressComponents
30
+ };
31
+ } else {
32
+ throw new Error('No results found for the provided zip code.');
33
+ }
34
+ } catch (error) {
35
+ fastify.log.error('Failed to fetch geolocation data:', error);
36
+ throw new Error('Failed to fetch geolocation data.');
37
+ }
38
+ }
39
+ });
40
+
41
+ console.info(' ✅ Geocodio Enabled');
42
+ }
43
+ }
@@ -0,0 +1,30 @@
1
+
2
+ import { PrismaClient } from "@prisma/client";
3
+ export async function setupPrisma(fastify, options) {
4
+ if (options.active !== false) {
5
+ delete options.active;
6
+ const prisma = new PrismaClient(options);
7
+
8
+ // Connect to the database
9
+ await prisma.$connect();
10
+
11
+ // Decorate Fastify instance with Prisma Client
12
+ fastify.decorate("prisma", prisma);
13
+
14
+ // Disconnect Prisma Client when Fastify closes
15
+ fastify.addHook("onClose", async () => {
16
+ await fastify.prisma.$disconnect();
17
+ });
18
+
19
+ console.info(" ✅ Prisma Enabled");
20
+ }
21
+ /*
22
+ ===== Ensure Proper Cleanup on Server Shutdown =====
23
+ */
24
+ fastify.addHook("onClose", async () => {
25
+ if (fastify.prisma) {
26
+ await fastify.prisma.$disconnect();
27
+ }
28
+ // Add additional cleanup for other services if necessary
29
+ });
30
+ }