@xenterprises/fastify-xconfig 1.1.8 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +189 -0
- package/README.md +127 -135
- package/package.json +16 -24
- package/server/app.js +9 -46
- package/src/lifecycle/xFastifyAfter.js +4 -4
- package/src/middleware/cors.js +6 -1
- package/src/utils/health.js +10 -11
- package/src/xConfig.js +1 -29
- package/test/index.js +6 -6
- package/test/xConfig.test.js +278 -0
- package/xConfigReference.js +103 -1505
- package/src/auth/admin.js +0 -182
- package/src/auth/portal.js +0 -176
- package/src/integrations/cloudinary.js +0 -98
- package/src/integrations/geocode.js +0 -43
- package/src/integrations/sendgrid.js +0 -58
- package/src/integrations/twilio.js +0 -146
- package/xConfigWorkingList.js +0 -720
package/src/auth/admin.js
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import * as jose from "jose";
|
|
2
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
3
|
-
|
|
4
|
-
export async function setupAdminAuth(fastify, options) {
|
|
5
|
-
if (options.admin?.active !== false) {
|
|
6
|
-
// Get configuration
|
|
7
|
-
const projectId = options.admin.stackProjectId || process.env.ADMIN_STACK_PROJECT_ID;
|
|
8
|
-
const publishableKey = options.admin.publishableKey || process.env.ADMIN_STACK_PUBLISHABLE_CLIENT_KEY;
|
|
9
|
-
const secretKey = options.admin.secretKey || process.env.ADMIN_STACK_SECRET_SERVER_KEY;
|
|
10
|
-
|
|
11
|
-
// Rest of the code remains the same
|
|
12
|
-
if (!projectId) {
|
|
13
|
-
throw new Error("Stack Auth admin project ID is required");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (!publishableKey || !secretKey) {
|
|
17
|
-
throw new Error("Stack Auth admin publishable and secret keys are required");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const adminExcludedPaths = options.admin.excludedPaths || [
|
|
21
|
-
"/admin/auth/login",
|
|
22
|
-
"/admin/auth/logout",
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
// Base URL for Stack Auth API
|
|
26
|
-
const stackAuthBaseUrl = `https://api.stack-auth.com/api/v1`;
|
|
27
|
-
const jwksUrl = `${stackAuthBaseUrl}/projects/${projectId}/.well-known/jwks.json`;
|
|
28
|
-
|
|
29
|
-
// Create JWKS for token verification
|
|
30
|
-
const jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
|
|
31
|
-
|
|
32
|
-
// Helper for Stack Auth API headers
|
|
33
|
-
fastify.decorate('getAdminStackAuthHeaders', function (accessType = "server") {
|
|
34
|
-
const headers = {
|
|
35
|
-
'Content-Type': 'application/json',
|
|
36
|
-
'Accept': 'application/json',
|
|
37
|
-
'X-Stack-Access-Type': accessType,
|
|
38
|
-
'X-Stack-Project-Id': projectId,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
if (accessType === "client") {
|
|
42
|
-
headers['X-Stack-Publishable-Client-Key'] = publishableKey;
|
|
43
|
-
} else if (accessType === "server") {
|
|
44
|
-
headers['X-Stack-Secret-Server-Key'] = secretKey;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return headers;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// JWT verification helper
|
|
51
|
-
fastify.decorate('verifyAdminStackJWT', async function (accessToken) {
|
|
52
|
-
try {
|
|
53
|
-
// Add validation to ensure accessToken is a valid string
|
|
54
|
-
if (!accessToken || typeof accessToken !== 'string') {
|
|
55
|
-
fastify.log.error('Invalid admin token format: token must be a non-empty string');
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const { payload } = await jose.jwtVerify(accessToken, jwks);
|
|
60
|
-
|
|
61
|
-
// Verify the payload has expected properties
|
|
62
|
-
if (!payload || !payload.sub) {
|
|
63
|
-
fastify.log.error('Invalid admin token payload: missing required claims');
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return payload;
|
|
68
|
-
} catch (error) {
|
|
69
|
-
fastify.log.error(error);
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// Admin authentication hook
|
|
75
|
-
fastify.addHook("onRequest", async (request, reply) => {
|
|
76
|
-
const url = request.url;
|
|
77
|
-
|
|
78
|
-
// Skip authentication for excluded paths
|
|
79
|
-
if (adminExcludedPaths.some((path) => url.startsWith(path))) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (url.startsWith("/admin")) {
|
|
84
|
-
try {
|
|
85
|
-
// Get token from header
|
|
86
|
-
const authHeader = request.headers.authorization;
|
|
87
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
88
|
-
return reply.code(401).send({ error: "Admin access token required" });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Verify token
|
|
92
|
-
const token = authHeader.slice(7);
|
|
93
|
-
const payload = await fastify.verifyAdminStackJWT(token);
|
|
94
|
-
|
|
95
|
-
if (!payload) {
|
|
96
|
-
return reply.code(401).send({ error: "Invalid admin token" });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Set admin info on request
|
|
100
|
-
request.admin = payload;
|
|
101
|
-
request.adminAuth = { id: payload.sub };
|
|
102
|
-
} catch (err) {
|
|
103
|
-
return reply.code(401).send({ error: "Admin authentication failed" });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Admin login endpoint
|
|
109
|
-
fastify.post("/admin/auth/login", async (req, reply) => {
|
|
110
|
-
try {
|
|
111
|
-
const { email, password } = req.body;
|
|
112
|
-
|
|
113
|
-
if (!email || !password) {
|
|
114
|
-
return reply.code(400).send({ error: "Email and password required" });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Call Stack Auth API for password sign-in
|
|
118
|
-
const response = await fetch(
|
|
119
|
-
`${stackAuthBaseUrl}/auth/password/sign-in`,
|
|
120
|
-
{
|
|
121
|
-
method: 'POST',
|
|
122
|
-
headers: fastify.getAdminStackAuthHeaders("server"),
|
|
123
|
-
body: JSON.stringify({ email, password })
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
const data = await response.json().catch(() => ({}));
|
|
128
|
-
|
|
129
|
-
if (!response.ok) {
|
|
130
|
-
return reply.code(response.status || 400).send({
|
|
131
|
-
error: data.message || "Authentication failed"
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Check if we have the expected response properties
|
|
136
|
-
if (!data.access_token || !data.refresh_token || !data.user_id) {
|
|
137
|
-
return reply.code(500).send({ error: "Invalid response from authentication service" });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Verify token
|
|
141
|
-
const payload = await fastify.verifyAdminStackJWT(data.access_token);
|
|
142
|
-
if (!payload) {
|
|
143
|
-
return reply.code(401).send({ error: "Invalid token received" });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
reply.send({
|
|
147
|
-
accessToken: data.access_token,
|
|
148
|
-
refreshToken: data.refresh_token,
|
|
149
|
-
admin: payload,
|
|
150
|
-
adminId: data.user_id
|
|
151
|
-
});
|
|
152
|
-
} catch (err) {
|
|
153
|
-
fastify.log.error(err);
|
|
154
|
-
reply.code(500).send({ error: "Internal server error" });
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Admin info endpoint
|
|
159
|
-
fastify.get("/admin/auth/me", async (req, reply) => {
|
|
160
|
-
reply.send(req.admin || { authenticated: false });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Token verification endpoint
|
|
164
|
-
fastify.post("/admin/auth/verify", async (req, reply) => {
|
|
165
|
-
const { token } = req.body;
|
|
166
|
-
|
|
167
|
-
if (!token) {
|
|
168
|
-
return reply.code(400).send({ error: "Token required" });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const payload = await fastify.verifyAdminStackJWT(token);
|
|
172
|
-
|
|
173
|
-
if (!payload) {
|
|
174
|
-
return reply.code(401).send({ valid: false });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
reply.send({ valid: true, admin: payload });
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
console.info(" ✅ Auth Admin Enabled");
|
|
181
|
-
}
|
|
182
|
-
}
|
package/src/auth/portal.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import * as jose from "jose";
|
|
2
|
-
|
|
3
|
-
export async function setupAuth(fastify, options) {
|
|
4
|
-
if (options.user?.active !== false) {
|
|
5
|
-
// Get configuration
|
|
6
|
-
const projectId = options.user.portalStackProjectId || process.env.STACK_PROJECT_ID;
|
|
7
|
-
const publishableKey = options.user.publishableKey || process.env.STACK_PUBLISHABLE_CLIENT_KEY;
|
|
8
|
-
const secretKey = options.user.secretKey || process.env.STACK_SECRET_SERVER_KEY;
|
|
9
|
-
|
|
10
|
-
if (!projectId) {
|
|
11
|
-
throw new Error("Stack Auth project ID is required");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (!publishableKey || !secretKey) {
|
|
15
|
-
throw new Error("Stack Auth publishable and secret keys are required");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const userExcludedPaths = options.user.excludedPaths || [
|
|
19
|
-
"/portal/auth/login",
|
|
20
|
-
"/portal/auth/logout",
|
|
21
|
-
"/portal/auth/register",
|
|
22
|
-
"/portal/auth/forgot-password",
|
|
23
|
-
"/portal/auth/reset-password"
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
// Base URL for Stack Auth API
|
|
27
|
-
const stackAuthBaseUrl = `https://api.stack-auth.com/api/v1`;
|
|
28
|
-
const jwksUrl = `${stackAuthBaseUrl}/projects/${projectId}/.well-known/jwks.json`;
|
|
29
|
-
|
|
30
|
-
// Create JWKS for token verification
|
|
31
|
-
const jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
|
|
32
|
-
|
|
33
|
-
// Helper for Stack Auth API headers
|
|
34
|
-
fastify.decorate('getStackAuthHeaders', function (accessType = "server") {
|
|
35
|
-
const headers = {
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
'Accept': 'application/json',
|
|
38
|
-
'X-Stack-Access-Type': accessType,
|
|
39
|
-
'X-Stack-Project-Id': projectId,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
if (accessType === "client") {
|
|
43
|
-
headers['X-Stack-Publishable-Client-Key'] = publishableKey;
|
|
44
|
-
} else if (accessType === "server") {
|
|
45
|
-
headers['X-Stack-Secret-Server-Key'] = secretKey;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return headers;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// JWT verification helper
|
|
52
|
-
fastify.decorate('verifyStackJWT', async function (accessToken) {
|
|
53
|
-
try {
|
|
54
|
-
const { payload } = await jose.jwtVerify(accessToken, jwks);
|
|
55
|
-
return payload;
|
|
56
|
-
} catch (error) {
|
|
57
|
-
fastify.log.error(error);
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// API URL helper
|
|
63
|
-
fastify.decorate('getStackAuthApiUrl', function () {
|
|
64
|
-
return stackAuthBaseUrl;
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Authentication middleware
|
|
68
|
-
fastify.addHook("onRequest", async (request, reply) => {
|
|
69
|
-
const url = request.url;
|
|
70
|
-
|
|
71
|
-
// Skip excluded paths
|
|
72
|
-
if (userExcludedPaths.some(path => url.startsWith(path))) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (url.startsWith("/portal")) {
|
|
77
|
-
try {
|
|
78
|
-
// Get token from header
|
|
79
|
-
const authHeader = request.headers.authorization;
|
|
80
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
81
|
-
return reply.code(401).send({ error: "Access token required" });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Verify token
|
|
85
|
-
const token = authHeader.slice(7);
|
|
86
|
-
const payload = await fastify.verifyStackJWT(token);
|
|
87
|
-
|
|
88
|
-
if (!payload) {
|
|
89
|
-
return reply.code(401).send({ error: "Invalid token" });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Set user info on request
|
|
93
|
-
request.user = payload;
|
|
94
|
-
request.userAuth = { id: payload.sub };
|
|
95
|
-
} catch (err) {
|
|
96
|
-
return reply.code(401).send({ error: "Authentication failed" });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// Login endpoint
|
|
102
|
-
fastify.post("/portal/auth/login", async (req, reply) => {
|
|
103
|
-
try {
|
|
104
|
-
const { email, password } = req.body;
|
|
105
|
-
|
|
106
|
-
if (!email || !password) {
|
|
107
|
-
return reply.code(400).send({ error: "Email and password required" });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Call Stack Auth API for password sign-in using native fetch
|
|
111
|
-
const response = await fetch(
|
|
112
|
-
`${stackAuthBaseUrl}/auth/password/sign-in`,
|
|
113
|
-
{
|
|
114
|
-
method: 'POST',
|
|
115
|
-
headers: fastify.getStackAuthHeaders("server"),
|
|
116
|
-
body: JSON.stringify({ email, password })
|
|
117
|
-
}
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const data = await response.json().catch(() => ({}));
|
|
121
|
-
|
|
122
|
-
if (!response.ok) {
|
|
123
|
-
return reply.code(response.status || 400).send({
|
|
124
|
-
error: data.message || "Authentication failed"
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Check if we have the expected response properties
|
|
129
|
-
if (!data.access_token || !data.refresh_token || !data.user_id) {
|
|
130
|
-
return reply.code(500).send({ error: "Invalid response from authentication service" });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Verify token
|
|
134
|
-
const payload = await fastify.verifyStackJWT(data.access_token);
|
|
135
|
-
if (!payload) {
|
|
136
|
-
return reply.code(401).send({ error: "Invalid token received" });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
reply.send({
|
|
140
|
-
accessToken: data.access_token,
|
|
141
|
-
refreshToken: data.refresh_token,
|
|
142
|
-
user: payload,
|
|
143
|
-
userId: data.user_id
|
|
144
|
-
});
|
|
145
|
-
} catch (err) {
|
|
146
|
-
fastify.log.error(err);
|
|
147
|
-
reply.code(500).send({ error: "Internal server error" });
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// User info endpoint
|
|
153
|
-
fastify.get("/portal/auth/me", async (req, reply) => {
|
|
154
|
-
reply.send(req.user || { authenticated: false });
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Token verification endpoint
|
|
158
|
-
fastify.post("/portal/auth/verify", async (req, reply) => {
|
|
159
|
-
const { token } = req.body;
|
|
160
|
-
|
|
161
|
-
if (!token) {
|
|
162
|
-
return reply.code(400).send({ error: "Token required" });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const payload = await fastify.verifyStackJWT(token);
|
|
166
|
-
|
|
167
|
-
if (!payload) {
|
|
168
|
-
return reply.code(401).send({ valid: false });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
reply.send({ valid: true, user: payload });
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
console.info(" ✅ Auth User Enabled");
|
|
175
|
-
}
|
|
176
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import Sendgrid from '@sendgrid/mail';
|
|
2
|
-
import sgClient from '@sendgrid/client';
|
|
3
|
-
export async function setupSendgrid(fastify, options) {
|
|
4
|
-
if (options.active !== false) {
|
|
5
|
-
if (!options.apiKey)
|
|
6
|
-
throw new Error("SendGrid API key must be provided.");
|
|
7
|
-
|
|
8
|
-
Sendgrid.setApiKey(options.apiKey);
|
|
9
|
-
sgClient.setApiKey(options.apiKeyEmailValidation);
|
|
10
|
-
// Decorator to group SendGrid-related methods under fastify.sendGrid
|
|
11
|
-
fastify.decorate('sendgrid', {
|
|
12
|
-
sendEmail: async (to, subject, templateId, dynamicTemplateData) => {
|
|
13
|
-
try {
|
|
14
|
-
|
|
15
|
-
const msg = {
|
|
16
|
-
to: to,
|
|
17
|
-
from: options.fromEmail,
|
|
18
|
-
subject: subject,
|
|
19
|
-
templateId: templateId,
|
|
20
|
-
dynamicTemplateData: { ...dynamicTemplateData, subject: subject },
|
|
21
|
-
};
|
|
22
|
-
await Sendgrid.send(msg);
|
|
23
|
-
return true;
|
|
24
|
-
} catch (error) {
|
|
25
|
-
fastify.log.error("SendGrid send email failed:", error);
|
|
26
|
-
throw new Error("Failed to send email.");
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
validateEmail: async (email) => {
|
|
30
|
-
const data = {
|
|
31
|
-
email: email,
|
|
32
|
-
// source: 'signup',
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const request = {
|
|
36
|
-
url: `/v3/validations/email`,
|
|
37
|
-
method: 'POST',
|
|
38
|
-
body: data,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const [response, body] = await sgClient.request(request);
|
|
43
|
-
console.log(body)
|
|
44
|
-
if (response.statusCode === 200) {
|
|
45
|
-
return body?.result; // Return the validation result
|
|
46
|
-
} else {
|
|
47
|
-
throw new Error(body.errors ? body.errors.map(err => err.message).join(', ') : 'Failed to validate email.');
|
|
48
|
-
}
|
|
49
|
-
} catch (error) {
|
|
50
|
-
fastify.log.error('SendGrid email validation failed:', error);
|
|
51
|
-
throw new Error('Failed to validate email.');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
console.info(" ✅ SendGrid Enabled");
|
|
57
|
-
}
|
|
58
|
-
}
|