@xenterprises/fastify-xconfig 0.0.10 → 1.0.0
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/README.md +64 -27
- package/package.json +1 -1
- package/server/app.js +2 -9
- package/src/auth/admin.js +117 -177
- package/src/auth/portal.js +116 -225
- package/xConfigReference.js +49 -18
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ This plugin provides a comprehensive setup for configuring various services such
|
|
|
8
8
|
|
|
9
9
|
- Handles CORS, rate-limiting, multipart handling, and error handling out of the box.
|
|
10
10
|
- Integrates with third-party services such as SendGrid, Twilio, Cloudinary, Stripe, and Bugsnag.
|
|
11
|
-
- Provides authentication setup for both Admin and User, with
|
|
11
|
+
- Provides authentication setup for both Admin and User, with Stack Auth integration.
|
|
12
12
|
- Includes Prisma database connection and gracefully handles disconnecting on server shutdown.
|
|
13
13
|
- Adds health check routes and resource usage monitoring, with environment validation.
|
|
14
14
|
- Customizes route listing and applies various performance and security options.
|
|
@@ -38,8 +38,19 @@ fastify.register(xConfig, {
|
|
|
38
38
|
apiSecret: "your-api-secret",
|
|
39
39
|
},
|
|
40
40
|
auth: {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
excludedPaths: ["/public", "/portal/auth/register"],
|
|
42
|
+
admin: {
|
|
43
|
+
active: true,
|
|
44
|
+
stackProjectId: "your-admin-stack-project-id",
|
|
45
|
+
publishableKey: "your-admin-stack-publishable-key",
|
|
46
|
+
secretKey: "your-admin-stack-secret-key",
|
|
47
|
+
},
|
|
48
|
+
user: {
|
|
49
|
+
active: true,
|
|
50
|
+
stackProjectId: "your-user-stack-project-id",
|
|
51
|
+
publishableKey: "your-user-stack-publishable-key",
|
|
52
|
+
secretKey: "your-user-stack-secret-key",
|
|
53
|
+
},
|
|
43
54
|
},
|
|
44
55
|
cors: { origin: ["https://your-frontend.com"], credentials: true },
|
|
45
56
|
});
|
|
@@ -53,7 +64,7 @@ fastify.listen({ port: 3000 });
|
|
|
53
64
|
- **sendGrid**: SendGrid configuration for email services (optional).
|
|
54
65
|
- **twilio**: Twilio configuration for SMS services (optional).
|
|
55
66
|
- **cloudinary**: Cloudinary configuration for media upload services (optional).
|
|
56
|
-
- **auth**: Authentication configuration for Admin and User
|
|
67
|
+
- **auth**: Authentication configuration for Admin and User with Stack Auth integration (optional).
|
|
57
68
|
- **cors**: CORS configuration (optional).
|
|
58
69
|
- **rateLimit**: Rate-limiting options (optional).
|
|
59
70
|
- **multipart**: Multipart handling options for file uploads (optional).
|
|
@@ -62,12 +73,29 @@ fastify.listen({ port: 3000 });
|
|
|
62
73
|
|
|
63
74
|
## Authentication
|
|
64
75
|
|
|
65
|
-
The plugin provides both Admin and User authentication with
|
|
76
|
+
The plugin provides both Admin and User authentication with Stack Auth integration. It handles token verification, protected routes, and authentication endpoints.
|
|
77
|
+
|
|
78
|
+
### User Authentication
|
|
79
|
+
|
|
80
|
+
- `/portal/auth/login` - User login endpoint
|
|
81
|
+
- `/portal/auth/me` - Get current user information
|
|
82
|
+
- `/portal/auth/verify` - Verify a user token
|
|
83
|
+
|
|
84
|
+
### Admin Authentication
|
|
66
85
|
|
|
67
|
-
- `/admin/auth/login`
|
|
68
|
-
- `/
|
|
69
|
-
- `/admin/auth/
|
|
70
|
-
|
|
86
|
+
- `/admin/auth/login` - Admin login endpoint
|
|
87
|
+
- `/admin/auth/me` - Get current admin information
|
|
88
|
+
- `/admin/auth/verify` - Verify an admin token
|
|
89
|
+
|
|
90
|
+
### Stack Auth Integration
|
|
91
|
+
|
|
92
|
+
The plugin integrates with [Stack Auth](https://stack-auth.com) for secure authentication. You'll need to provide:
|
|
93
|
+
|
|
94
|
+
- `stackProjectId` - Your Stack Auth project ID
|
|
95
|
+
- `publishableKey` - Your Stack Auth publishable client key
|
|
96
|
+
- `secretKey` - Your Stack Auth secret server key
|
|
97
|
+
|
|
98
|
+
These can be provided via environment variables or in the plugin options.
|
|
71
99
|
|
|
72
100
|
## Health Check
|
|
73
101
|
|
|
@@ -79,11 +107,11 @@ The `/health` route provides a health check with details about uptime, memory us
|
|
|
79
107
|
- **SendGrid**: Provides an email sending service through SendGrid, integrated with the Fastify instance.
|
|
80
108
|
- **Twilio**: Provides SMS sending and phone number validation services.
|
|
81
109
|
- **Cloudinary**: Handles file uploads to Cloudinary and deletion of media files.
|
|
82
|
-
- **Authentication**:
|
|
110
|
+
- **Authentication**: Stack Auth integration for secure user and admin authentication.
|
|
83
111
|
|
|
84
112
|
## Hooks
|
|
85
113
|
|
|
86
|
-
- **onRequest**: Validates
|
|
114
|
+
- **onRequest**: Validates tokens for protected routes.
|
|
87
115
|
- **onClose**: Gracefully disconnects Prisma and other services on server shutdown.
|
|
88
116
|
|
|
89
117
|
## Error Handling
|
|
@@ -100,11 +128,23 @@ The following dependencies are used for various services and integrations:
|
|
|
100
128
|
|
|
101
129
|
- Prisma Client, SendGrid, Twilio, Cloudinary, Fastify CORS, Fastify Rate Limit, Fastify Multipart, Fastify Under Pressure, and Fastify Bugsnag.
|
|
102
130
|
|
|
103
|
-
##
|
|
131
|
+
## Environment Variables
|
|
132
|
+
|
|
133
|
+
### Required for Authentication
|
|
134
|
+
|
|
135
|
+
- `ADMIN_STACK_PROJECT_ID` - Stack Auth project ID for admin authentication
|
|
136
|
+
- `ADMIN_STACK_PUBLISHABLE_CLIENT_KEY` - Stack Auth publishable key for admin authentication
|
|
137
|
+
- `ADMIN_STACK_SECRET_SERVER_KEY` - Stack Auth secret key for admin authentication
|
|
138
|
+
- `USER_STACK_PROJECT_ID` - Stack Auth project ID for user authentication
|
|
139
|
+
- `USER_STACK_PUBLISHABLE_CLIENT_KEY` - Stack Auth publishable key for user authentication
|
|
140
|
+
- `USER_STACK_SECRET_SERVER_KEY` - Stack Auth secret key for user authentication
|
|
141
|
+
|
|
142
|
+
### Other Environment Variables
|
|
104
143
|
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
144
|
+
- `DATABASE_URL` - Required for Prisma Client
|
|
145
|
+
- `SENDGRID_API_KEY` - Required for SendGrid integration
|
|
146
|
+
- `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PHONE_NUMBER` - Required for Twilio integration
|
|
147
|
+
- `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET` - Required for Cloudinary integration
|
|
108
148
|
|
|
109
149
|
## Example Configuration
|
|
110
150
|
|
|
@@ -123,21 +163,18 @@ fastify.register(xConfig, {
|
|
|
123
163
|
apiSecret: "your-api-secret",
|
|
124
164
|
},
|
|
125
165
|
auth: {
|
|
166
|
+
excludedPaths: ["/public", "/portal/auth/login", "/portal/auth/register"],
|
|
126
167
|
admin: {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
refreshTokenName: "adminRefreshToken",
|
|
132
|
-
},
|
|
168
|
+
active: true,
|
|
169
|
+
stackProjectId: "your-admin-stack-project-id",
|
|
170
|
+
publishableKey: "your-admin-stack-publishable-key",
|
|
171
|
+
secretKey: "your-admin-stack-secret-key",
|
|
133
172
|
},
|
|
134
173
|
user: {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
refreshTokenName: "userRefreshToken",
|
|
140
|
-
},
|
|
174
|
+
active: true,
|
|
175
|
+
stackProjectId: "your-user-stack-project-id",
|
|
176
|
+
publishableKey: "your-user-stack-publishable-key",
|
|
177
|
+
secretKey: "your-user-stack-secret-key",
|
|
141
178
|
},
|
|
142
179
|
},
|
|
143
180
|
cors: { origin: ["https://your-frontend.com"], credentials: true },
|
package/package.json
CHANGED
package/server/app.js
CHANGED
|
@@ -62,14 +62,7 @@ export default async function (fastify, opts) {
|
|
|
62
62
|
},
|
|
63
63
|
user: {
|
|
64
64
|
active: true,
|
|
65
|
-
|
|
66
|
-
expiresIn: '1h',
|
|
67
|
-
cookieOptions: {
|
|
68
|
-
name: 'userToken',
|
|
69
|
-
httpOnly: true,
|
|
70
|
-
secure: true,
|
|
71
|
-
sameSite: 'strict',
|
|
72
|
-
},
|
|
65
|
+
portalStackProjectId: process.env.STACK_PROJECT_ID,
|
|
73
66
|
me: {
|
|
74
67
|
isOnboarded: true
|
|
75
68
|
},
|
|
@@ -89,4 +82,4 @@ export default async function (fastify, opts) {
|
|
|
89
82
|
return { status: fastify.xEcho() }
|
|
90
83
|
})
|
|
91
84
|
|
|
92
|
-
};
|
|
85
|
+
};
|
package/src/auth/admin.js
CHANGED
|
@@ -1,67 +1,74 @@
|
|
|
1
|
-
import
|
|
2
|
-
import bcrypt from "bcrypt";
|
|
1
|
+
import * as jose from "jose";
|
|
3
2
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
3
|
+
|
|
4
4
|
export async function setupAdminAuth(fastify, options) {
|
|
5
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
|
+
if (!projectId) {
|
|
12
|
+
throw new Error("Stack Auth admin project ID is required");
|
|
13
|
+
}
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
throw new Error("Admin JWT secret must be provided.");
|
|
15
|
+
if (!publishableKey || !secretKey) {
|
|
16
|
+
throw new Error("Stack Auth admin publishable and secret keys are required");
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
const
|
|
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 || [
|
|
19
|
+
const adminExcludedPaths = options.admin.excludedPaths || [
|
|
24
20
|
"/admin/auth/login",
|
|
25
21
|
"/admin/auth/logout",
|
|
26
22
|
];
|
|
27
23
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
// Base URL for Stack Auth API
|
|
25
|
+
const stackAuthBaseUrl = `https://api.stack-auth.com/api/v1`;
|
|
26
|
+
const jwksUrl = `${stackAuthBaseUrl}/projects/${projectId}/.well-known/jwks.json`;
|
|
27
|
+
|
|
28
|
+
// Create JWKS for token verification
|
|
29
|
+
const jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
|
|
30
|
+
|
|
31
|
+
// Helper for Stack Auth API headers
|
|
32
|
+
fastify.decorate('getAdminStackAuthHeaders', function (accessType = "server") {
|
|
33
|
+
const headers = {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'Accept': 'application/json',
|
|
36
|
+
'X-Stack-Access-Type': accessType,
|
|
37
|
+
'X-Stack-Project-Id': projectId,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (accessType === "client") {
|
|
41
|
+
headers['X-Stack-Publishable-Client-Key'] = publishableKey;
|
|
42
|
+
} else if (accessType === "server") {
|
|
43
|
+
headers['X-Stack-Secret-Server-Key'] = secretKey;
|
|
36
44
|
}
|
|
37
|
-
}
|
|
38
45
|
|
|
39
|
-
|
|
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",
|
|
46
|
+
return headers;
|
|
52
47
|
});
|
|
53
48
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
// JWT verification helper
|
|
50
|
+
fastify.decorate('verifyAdminStackJWT', async function (accessToken) {
|
|
51
|
+
try {
|
|
52
|
+
// Add validation to ensure accessToken is a valid string
|
|
53
|
+
if (!accessToken || typeof accessToken !== 'string') {
|
|
54
|
+
fastify.log.error('Invalid admin token format: token must be a non-empty string');
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { payload } = await jose.jwtVerify(accessToken, jwks);
|
|
59
|
+
|
|
60
|
+
// Verify the payload has expected properties
|
|
61
|
+
if (!payload || !payload.sub) {
|
|
62
|
+
fastify.log.error('Invalid admin token payload: missing required claims');
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return payload;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
fastify.log.error(error);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
65
72
|
|
|
66
73
|
// Admin authentication hook
|
|
67
74
|
fastify.addHook("onRequest", async (request, reply) => {
|
|
@@ -74,168 +81,101 @@ export async function setupAdminAuth(fastify, options) {
|
|
|
74
81
|
|
|
75
82
|
if (url.startsWith("/admin")) {
|
|
76
83
|
try {
|
|
77
|
-
//
|
|
84
|
+
// Get token from header
|
|
78
85
|
const authHeader = request.headers.authorization;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
);
|
|
86
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
87
|
+
return reply.code(401).send({ error: "Admin access token required" });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Verify token
|
|
91
|
+
const token = authHeader.slice(7);
|
|
92
|
+
const payload = await fastify.verifyAdminStackJWT(token);
|
|
93
|
+
|
|
94
|
+
if (!payload) {
|
|
95
|
+
return reply.code(401).send({ error: "Invalid admin token" });
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
request.adminAuth =
|
|
98
|
+
// Set admin info on request
|
|
99
|
+
request.admin = payload;
|
|
100
|
+
request.adminAuth = { id: payload.sub };
|
|
94
101
|
} catch (err) {
|
|
95
|
-
|
|
96
|
-
reply.send(
|
|
97
|
-
fastify.httpErrors.unauthorized("Invalid or expired access token")
|
|
98
|
-
);
|
|
102
|
+
return reply.code(401).send({ error: "Admin authentication failed" });
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
});
|
|
102
106
|
|
|
103
|
-
// Admin login
|
|
107
|
+
// Admin login endpoint
|
|
104
108
|
fastify.post("/admin/auth/login", async (req, reply) => {
|
|
105
109
|
try {
|
|
106
110
|
const { email, password } = req.body;
|
|
107
111
|
|
|
108
|
-
// Validate input
|
|
109
112
|
if (!email || !password) {
|
|
110
|
-
|
|
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");
|
|
113
|
+
return reply.code(400).send({ error: "Email and password required" });
|
|
121
114
|
}
|
|
122
115
|
|
|
123
|
-
//
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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);
|
|
116
|
+
// Call Stack Auth API for password sign-in
|
|
117
|
+
const response = await fetch(
|
|
118
|
+
`${stackAuthBaseUrl}/auth/password/sign-in`,
|
|
119
|
+
{
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: fastify.getAdminStackAuthHeaders("server"),
|
|
122
|
+
body: JSON.stringify({ email, password })
|
|
123
|
+
}
|
|
124
|
+
);
|
|
144
125
|
|
|
145
|
-
|
|
146
|
-
} catch (err) {
|
|
147
|
-
reply.send(err);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
126
|
+
const data = await response.json().catch(() => ({}));
|
|
150
127
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const refreshToken = req.cookies[adminRefreshCookieName];
|
|
156
|
-
if (!refreshToken) {
|
|
157
|
-
throw fastify.httpErrors.unauthorized("Refresh token not provided");
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
return reply.code(response.status || 400).send({
|
|
130
|
+
error: data.message || "Authentication failed"
|
|
131
|
+
});
|
|
158
132
|
}
|
|
159
133
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
164
|
-
if (!admin) {
|
|
165
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
134
|
+
// Check if we have the expected response properties
|
|
135
|
+
if (!data.access_token || !data.refresh_token || !data.user_id) {
|
|
136
|
+
return reply.code(500).send({ error: "Invalid response from authentication service" });
|
|
166
137
|
}
|
|
167
138
|
|
|
168
|
-
// Verify
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
171
|
-
|
|
139
|
+
// Verify token
|
|
140
|
+
const payload = await fastify.verifyAdminStackJWT(data.access_token);
|
|
141
|
+
if (!payload) {
|
|
142
|
+
return reply.code(401).send({ error: "Invalid token received" });
|
|
172
143
|
}
|
|
173
144
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 },
|
|
145
|
+
reply.send({
|
|
146
|
+
accessToken: data.access_token,
|
|
147
|
+
refreshToken: data.refresh_token,
|
|
148
|
+
admin: payload,
|
|
149
|
+
adminId: data.user_id
|
|
185
150
|
});
|
|
186
|
-
|
|
187
|
-
// Set new tokens as cookies
|
|
188
|
-
setAdminAuthCookies(reply, accessToken, newRefreshToken);
|
|
189
|
-
|
|
190
|
-
reply.send({ accessToken });
|
|
191
151
|
} catch (err) {
|
|
192
|
-
|
|
152
|
+
fastify.log.error(err);
|
|
153
|
+
reply.code(500).send({ error: "Internal server error" });
|
|
193
154
|
}
|
|
194
155
|
});
|
|
195
156
|
|
|
196
|
-
// Admin
|
|
197
|
-
fastify.
|
|
198
|
-
|
|
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
|
-
}
|
|
157
|
+
// Admin info endpoint
|
|
158
|
+
fastify.get("/admin/auth/me", async (req, reply) => {
|
|
159
|
+
reply.send(req.admin || { authenticated: false });
|
|
216
160
|
});
|
|
217
161
|
|
|
218
|
-
//
|
|
219
|
-
fastify.
|
|
220
|
-
|
|
221
|
-
const adminAuth = req.adminAuth;
|
|
162
|
+
// Token verification endpoint
|
|
163
|
+
fastify.post("/admin/auth/verify", async (req, reply) => {
|
|
164
|
+
const { token } = req.body;
|
|
222
165
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
select: { id: true, firstName: true, lastName: true, email: true },
|
|
227
|
-
});
|
|
166
|
+
if (!token) {
|
|
167
|
+
return reply.code(400).send({ error: "Token required" });
|
|
168
|
+
}
|
|
228
169
|
|
|
229
|
-
|
|
230
|
-
throw fastify.httpErrors.notFound("Admin not found");
|
|
231
|
-
}
|
|
170
|
+
const payload = await fastify.verifyAdminStackJWT(token);
|
|
232
171
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
reply.send(err);
|
|
172
|
+
if (!payload) {
|
|
173
|
+
return reply.code(401).send({ valid: false });
|
|
236
174
|
}
|
|
175
|
+
|
|
176
|
+
reply.send({ valid: true, admin: payload });
|
|
237
177
|
});
|
|
238
178
|
|
|
239
179
|
console.info(" ✅ Auth Admin Enabled");
|
|
240
180
|
}
|
|
241
|
-
}
|
|
181
|
+
}
|
package/src/auth/portal.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
1
|
+
import * as jose from "jose";
|
|
2
|
+
import fetch from "node-fetch";
|
|
3
|
+
|
|
5
4
|
export async function setupAuth(fastify, options) {
|
|
6
5
|
if (options.user?.active !== false) {
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
// Get configuration
|
|
7
|
+
const projectId = options.user.portalStackProjectId || process.env.STACK_PROJECT_ID;
|
|
8
|
+
const publishableKey = options.user.publishableKey || process.env.STACK_PUBLISHABLE_CLIENT_KEY;
|
|
9
|
+
const secretKey = options.user.secretKey || process.env.STACK_SECRET_SERVER_KEY;
|
|
10
|
+
|
|
11
|
+
if (!projectId) {
|
|
12
|
+
throw new Error("Stack Auth project ID is required");
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
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 || [
|
|
15
|
+
if (!publishableKey || !secretKey) {
|
|
16
|
+
throw new Error("Stack Auth publishable and secret keys are required");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const userExcludedPaths = options.user.excludedPaths || [
|
|
23
20
|
"/portal/auth/login",
|
|
24
21
|
"/portal/auth/logout",
|
|
25
22
|
"/portal/auth/register",
|
|
@@ -27,260 +24,154 @@ export async function setupAuth(fastify, options) {
|
|
|
27
24
|
"/portal/auth/reset-password"
|
|
28
25
|
];
|
|
29
26
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
// Base URL for Stack Auth API
|
|
28
|
+
const stackAuthBaseUrl = `https://api.stack-auth.com/api/v1`;
|
|
29
|
+
const jwksUrl = `${stackAuthBaseUrl}/projects/${projectId}/.well-known/jwks.json`;
|
|
30
|
+
|
|
31
|
+
// Create JWKS for token verification
|
|
32
|
+
const jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
|
|
33
|
+
|
|
34
|
+
// Helper for Stack Auth API headers
|
|
35
|
+
fastify.decorate('getStackAuthHeaders', function (accessType = "server") {
|
|
36
|
+
const headers = {
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
'Accept': 'application/json',
|
|
39
|
+
'X-Stack-Access-Type': accessType,
|
|
40
|
+
'X-Stack-Project-Id': projectId,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (accessType === "client") {
|
|
44
|
+
headers['X-Stack-Publishable-Client-Key'] = publishableKey;
|
|
45
|
+
} else if (accessType === "server") {
|
|
46
|
+
headers['X-Stack-Secret-Server-Key'] = secretKey;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return headers;
|
|
38
50
|
});
|
|
39
51
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// JWT verification helper
|
|
53
|
+
fastify.decorate('verifyStackJWT', async function (accessToken) {
|
|
54
|
+
try {
|
|
55
|
+
const { payload } = await jose.jwtVerify(accessToken, jwks);
|
|
56
|
+
return payload;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
fastify.log.error(error);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// API URL helper
|
|
64
|
+
fastify.decorate('getStackAuthApiUrl', function () {
|
|
65
|
+
return stackAuthBaseUrl;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Authentication middleware
|
|
53
69
|
fastify.addHook("onRequest", async (request, reply) => {
|
|
54
70
|
const url = request.url;
|
|
55
71
|
|
|
56
|
-
// Skip
|
|
57
|
-
if (userExcludedPaths.some(
|
|
72
|
+
// Skip excluded paths
|
|
73
|
+
if (userExcludedPaths.some(path => url.startsWith(path))) {
|
|
58
74
|
return;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
77
|
if (url.startsWith("/portal")) {
|
|
62
78
|
try {
|
|
63
|
-
//
|
|
79
|
+
// Get token from header
|
|
64
80
|
const authHeader = request.headers.authorization;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
? authHeader.slice(7)
|
|
68
|
-
: null;
|
|
69
|
-
const token = request.cookies[userCookieName] || authToken;
|
|
70
|
-
|
|
71
|
-
if (!token) {
|
|
72
|
-
throw fastify.httpErrors.unauthorized(
|
|
73
|
-
"User access token not provided"
|
|
74
|
-
);
|
|
81
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
82
|
+
return reply.code(401).send({ error: "Access token required" });
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
// Verify
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
} catch (err) {
|
|
81
|
-
// Use built-in HTTP error handling
|
|
82
|
-
reply.send(
|
|
83
|
-
fastify.httpErrors.unauthorized("Invalid or expired access token")
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// User registration route
|
|
90
|
-
fastify.post("/portal/auth/register", async (req, reply) => {
|
|
91
|
-
try {
|
|
92
|
-
const { email, password, firstName, lastName } = req.body;
|
|
85
|
+
// Verify token
|
|
86
|
+
const token = authHeader.slice(7);
|
|
87
|
+
const payload = await fastify.verifyStackJWT(token);
|
|
93
88
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
89
|
+
if (!payload) {
|
|
90
|
+
return reply.code(401).send({ error: "Invalid token" });
|
|
91
|
+
}
|
|
98
92
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
throw fastify.httpErrors.conflict("Email already in use");
|
|
93
|
+
// Set user info on request
|
|
94
|
+
request.user = payload;
|
|
95
|
+
request.userAuth = { id: payload.sub };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return reply.code(401).send({ error: "Authentication failed" });
|
|
105
98
|
}
|
|
106
|
-
|
|
107
|
-
// Hash the password
|
|
108
|
-
const hashedPassword = await bcrypt.hash(password, 10);
|
|
109
|
-
|
|
110
|
-
// Create the user
|
|
111
|
-
const user = await fastify.prisma.users.create({
|
|
112
|
-
data: {
|
|
113
|
-
email,
|
|
114
|
-
password: hashedPassword,
|
|
115
|
-
firstName,
|
|
116
|
-
lastName,
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// // Send welcome email
|
|
121
|
-
// if (options.user?.sendWelcomeEmail) {
|
|
122
|
-
// await fastify.sendGrid.sendEmail(email, auth?.user?.registerEmail?.subject, auth?.user?.registerEmail?.templateId, { name: firstName, email });
|
|
123
|
-
// }
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Issue access token
|
|
127
|
-
const accessToken = await reply.userJwtSign({ id: user.id });
|
|
128
|
-
|
|
129
|
-
// Generate refresh token
|
|
130
|
-
const refreshToken = await fastify.randomUUID();
|
|
131
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
132
|
-
|
|
133
|
-
// Store hashed refresh token in the database
|
|
134
|
-
await fastify.prisma.users.update({
|
|
135
|
-
where: { id: user.id },
|
|
136
|
-
data: { refreshToken: hashedRefreshToken },
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
// Set tokens as cookies
|
|
140
|
-
setAuthCookies(reply, accessToken, refreshToken);
|
|
141
|
-
|
|
142
|
-
reply.send({ accessToken });
|
|
143
|
-
} catch (err) {
|
|
144
|
-
reply.send(err);
|
|
145
99
|
}
|
|
146
100
|
});
|
|
147
101
|
|
|
148
|
-
//
|
|
102
|
+
// Login endpoint
|
|
149
103
|
fastify.post("/portal/auth/login", async (req, reply) => {
|
|
150
104
|
try {
|
|
151
105
|
const { email, password } = req.body;
|
|
152
106
|
|
|
153
107
|
if (!email || !password) {
|
|
154
|
-
|
|
155
|
-
"Email and password are required"
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Fetch user from the database
|
|
160
|
-
const user = await fastify.prisma.users.findUnique({
|
|
161
|
-
where: { email },
|
|
162
|
-
});
|
|
163
|
-
if (!user) {
|
|
164
|
-
throw fastify.httpErrors.unauthorized("Invalid credentials");
|
|
108
|
+
return reply.code(400).send({ error: "Email and password required" });
|
|
165
109
|
}
|
|
166
110
|
|
|
167
|
-
//
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Generate refresh token
|
|
177
|
-
const refreshToken = await fastify.randomUUID();
|
|
178
|
-
const hashedRefreshToken = await bcrypt.hash(refreshToken, 10);
|
|
179
|
-
|
|
180
|
-
// Store hashed refresh token in the database
|
|
181
|
-
await fastify.prisma.users.update({
|
|
182
|
-
where: { id: user.id },
|
|
183
|
-
data: { refreshToken: hashedRefreshToken },
|
|
184
|
-
});
|
|
111
|
+
// Call Stack Auth API for password sign-in
|
|
112
|
+
const response = await fetch(
|
|
113
|
+
`${stackAuthBaseUrl}/auth/password/sign-in`,
|
|
114
|
+
{
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: fastify.getStackAuthHeaders("server"),
|
|
117
|
+
body: JSON.stringify({ email, password })
|
|
118
|
+
}
|
|
119
|
+
);
|
|
185
120
|
|
|
186
|
-
|
|
187
|
-
setAuthCookies(reply, accessToken, refreshToken);
|
|
121
|
+
const data = await response.json().catch(() => ({}));
|
|
188
122
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// User refresh token route
|
|
196
|
-
fastify.post("/portal/auth/refresh", async (req, reply) => {
|
|
197
|
-
try {
|
|
198
|
-
const userAuth = req.userAuth;
|
|
199
|
-
const refreshToken = req.cookies[userRefreshCookieName];
|
|
200
|
-
if (!refreshToken) {
|
|
201
|
-
throw fastify.httpErrors.unauthorized("Refresh token not provided");
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
return reply.code(response.status || 400).send({
|
|
125
|
+
error: data.message || "Authentication failed"
|
|
126
|
+
});
|
|
202
127
|
}
|
|
203
128
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
if (!user) {
|
|
210
|
-
throw fastify.httpErrors.unauthorized("Invalid refresh token");
|
|
129
|
+
// Check if we have the expected response properties
|
|
130
|
+
if (!data.access_token || !data.refresh_token || !data.user_id) {
|
|
131
|
+
return reply.code(500).send({ error: "Invalid response from authentication service" });
|
|
211
132
|
}
|
|
212
133
|
|
|
213
|
-
// Verify
|
|
214
|
-
const
|
|
215
|
-
if (!
|
|
216
|
-
|
|
134
|
+
// Verify token
|
|
135
|
+
const payload = await fastify.verifyStackJWT(data.access_token);
|
|
136
|
+
if (!payload) {
|
|
137
|
+
return reply.code(401).send({ error: "Invalid token received" });
|
|
217
138
|
}
|
|
218
139
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const hashedNewRefreshToken = await bcrypt.hash(newRefreshToken, 10);
|
|
225
|
-
|
|
226
|
-
// Update refresh token in the database
|
|
227
|
-
await fastify.prisma.users.update({
|
|
228
|
-
where: { id: user.id },
|
|
229
|
-
data: { refreshToken: hashedNewRefreshToken },
|
|
140
|
+
reply.send({
|
|
141
|
+
accessToken: data.access_token,
|
|
142
|
+
refreshToken: data.refresh_token,
|
|
143
|
+
user: payload,
|
|
144
|
+
userId: data.user_id
|
|
230
145
|
});
|
|
231
|
-
|
|
232
|
-
// Set new tokens as cookies
|
|
233
|
-
setAuthCookies(reply, accessToken, newRefreshToken);
|
|
234
|
-
|
|
235
|
-
reply.send({ accessToken });
|
|
236
146
|
} catch (err) {
|
|
237
|
-
|
|
147
|
+
fastify.log.error(err);
|
|
148
|
+
reply.code(500).send({ error: "Internal server error" });
|
|
238
149
|
}
|
|
239
150
|
});
|
|
240
151
|
|
|
241
|
-
// User logout route
|
|
242
|
-
fastify.post("/portal/auth/logout", async (req, reply) => {
|
|
243
|
-
try {
|
|
244
|
-
const userAuth = req.userAuth;
|
|
245
|
-
if (userAuth) {
|
|
246
|
-
// Delete refresh token from the database
|
|
247
|
-
await fastify.prisma.users.update({
|
|
248
|
-
where: { id: userAuth.id },
|
|
249
|
-
data: { refreshToken: null },
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Clear cookies
|
|
254
|
-
reply.clearCookie(userCookieName, { path: "/" });
|
|
255
|
-
reply.clearCookie(userRefreshCookieName, { path: "/" });
|
|
256
152
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
153
|
+
// User info endpoint
|
|
154
|
+
fastify.get("/portal/auth/me", async (req, reply) => {
|
|
155
|
+
reply.send(req.user || { authenticated: false });
|
|
261
156
|
});
|
|
262
157
|
|
|
263
|
-
//
|
|
264
|
-
fastify.
|
|
265
|
-
|
|
266
|
-
const userAuth = req.userAuth;
|
|
158
|
+
// Token verification endpoint
|
|
159
|
+
fastify.post("/portal/auth/verify", async (req, reply) => {
|
|
160
|
+
const { token } = req.body;
|
|
267
161
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
select: { id: true, email: true, firstName: true, lastName: true, ...userAuthOptions.me },
|
|
272
|
-
});
|
|
162
|
+
if (!token) {
|
|
163
|
+
return reply.code(400).send({ error: "Token required" });
|
|
164
|
+
}
|
|
273
165
|
|
|
274
|
-
|
|
275
|
-
throw fastify.httpErrors.notFound("User not found");
|
|
276
|
-
}
|
|
166
|
+
const payload = await fastify.verifyStackJWT(token);
|
|
277
167
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
reply.send(err);
|
|
168
|
+
if (!payload) {
|
|
169
|
+
return reply.code(401).send({ valid: false });
|
|
281
170
|
}
|
|
171
|
+
|
|
172
|
+
reply.send({ valid: true, user: payload });
|
|
282
173
|
});
|
|
283
174
|
|
|
284
175
|
console.info(" ✅ Auth User Enabled");
|
|
285
176
|
}
|
|
286
|
-
}
|
|
177
|
+
}
|
package/xConfigReference.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* *Key Features:*
|
|
9
9
|
* - Handles CORS, rate-limiting, multipart handling, and error handling out of the box.
|
|
10
10
|
* - Integrates with third-party services such as SendGrid, Twilio, Cloudinary, Stripe, and Bugsnag.
|
|
11
|
-
* - Provides authentication setup for both Admin and User, with
|
|
11
|
+
* - Provides authentication setup for both Admin and User, with Stack Auth integration.
|
|
12
12
|
* - Includes Prisma database connection and gracefully handles disconnecting on server shutdown.
|
|
13
13
|
* - Adds health check routes and resource usage monitoring, with environment validation.
|
|
14
14
|
* - Customizes routes listing and applies various performance and security options.
|
|
@@ -29,8 +29,19 @@
|
|
|
29
29
|
* twilio: { accountSid: 'your-account-sid', authToken: 'your-auth-token', phoneNumber: 'your-twilio-number' },
|
|
30
30
|
* cloudinary: { cloudName: 'your-cloud-name', apiKey: 'your-api-key', apiSecret: 'your-api-secret' },
|
|
31
31
|
* auth: {
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
+
* },
|
|
34
45
|
* },
|
|
35
46
|
* cors: { origin: ['https://your-frontend.com'], credentials: true },
|
|
36
47
|
* });
|
|
@@ -44,7 +55,7 @@
|
|
|
44
55
|
* - sendGrid: SendGrid configuration for email services (optional).
|
|
45
56
|
* - twilio: Twilio configuration for SMS services (optional).
|
|
46
57
|
* - cloudinary: Cloudinary configuration for media upload services (optional).
|
|
47
|
-
* - auth: Authentication configuration for Admin and User
|
|
58
|
+
* - auth: Authentication configuration for Admin and User with Stack Auth integration (optional).
|
|
48
59
|
* - cors: CORS configuration (optional).
|
|
49
60
|
* - rateLimit: Rate-limiting options (optional).
|
|
50
61
|
* - multipart: Multipart handling options for file uploads (optional).
|
|
@@ -52,12 +63,23 @@
|
|
|
52
63
|
* - underPressure: Under Pressure plugin options for monitoring and throttling under load (optional).
|
|
53
64
|
*
|
|
54
65
|
* *Authentication:*
|
|
55
|
-
* The plugin provides both Admin and User authentication with
|
|
56
|
-
*
|
|
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:
|
|
57
68
|
* - `/admin/auth/login` for admin login.
|
|
58
69
|
* - `/portal/auth/login` for user login.
|
|
59
70
|
* - `/admin/auth/me` to check authentication status for admins.
|
|
60
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.
|
|
61
83
|
*
|
|
62
84
|
* *Health Check:*
|
|
63
85
|
* The `/health` route provides a health check with details about uptime, memory usage, CPU load,
|
|
@@ -68,10 +90,10 @@
|
|
|
68
90
|
* - **SendGrid**: Provides an email sending service through SendGrid, integrated with the Fastify instance.
|
|
69
91
|
* - **Twilio**: Provides SMS sending and phone number validation services.
|
|
70
92
|
* - **Cloudinary**: Handles file uploads to Cloudinary and deletion of media files.
|
|
71
|
-
* - **Authentication**:
|
|
93
|
+
* - **Authentication**: Stack Auth integration for secure user and admin authentication.
|
|
72
94
|
*
|
|
73
95
|
* *Hooks:*
|
|
74
|
-
* - `onRequest`: Validates
|
|
96
|
+
* - `onRequest`: Validates tokens for protected routes.
|
|
75
97
|
* - `onClose`: Gracefully disconnects Prisma and other services on server shutdown.
|
|
76
98
|
*
|
|
77
99
|
* *Error Handling:*
|
|
@@ -86,10 +108,18 @@
|
|
|
86
108
|
* Fastify Under Pressure, and Fastify Bugsnag are used for various services and integrations.
|
|
87
109
|
*
|
|
88
110
|
* *Notes:*
|
|
89
|
-
* - Environment variables such as `DATABASE_URL`, `
|
|
111
|
+
* - Environment variables such as `DATABASE_URL`, `ADMIN_STACK_PROJECT_ID`, and `USER_STACK_PROJECT_ID` are expected to be set.
|
|
90
112
|
* - Services like SendGrid, Twilio, and Cloudinary require API keys to be passed via the options.
|
|
91
113
|
* - This plugin is highly customizable, with options to enable/disable each feature.
|
|
92
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
|
+
*
|
|
93
123
|
* *Example Configuration:*
|
|
94
124
|
* ```javascript
|
|
95
125
|
* fastify.register(xConfig, {
|
|
@@ -106,15 +136,18 @@
|
|
|
106
136
|
* apiSecret: 'your-api-secret',
|
|
107
137
|
* },
|
|
108
138
|
* auth: {
|
|
139
|
+
* excludedPaths: ['/public', '/portal/auth/login', '/portal/auth/register'],
|
|
109
140
|
* admin: {
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
141
|
+
* active: true,
|
|
142
|
+
* stackProjectId: 'your-admin-stack-project-id',
|
|
143
|
+
* publishableKey: 'your-admin-stack-publishable-key',
|
|
144
|
+
* secretKey: 'your-admin-stack-secret-key',
|
|
113
145
|
* },
|
|
114
146
|
* user: {
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
147
|
+
* active: true,
|
|
148
|
+
* stackProjectId: 'your-user-stack-project-id',
|
|
149
|
+
* publishableKey: 'your-user-stack-publishable-key',
|
|
150
|
+
* secretKey: 'your-user-stack-secret-key',
|
|
118
151
|
* },
|
|
119
152
|
* },
|
|
120
153
|
* cors: { origin: ['https://your-frontend.com'], credentials: true },
|
|
@@ -123,14 +156,12 @@
|
|
|
123
156
|
*/
|
|
124
157
|
|
|
125
158
|
import fp from "fastify-plugin";
|
|
126
|
-
import
|
|
127
|
-
import bcrypt from "bcrypt";
|
|
159
|
+
import jose from "jose";
|
|
128
160
|
import { PrismaClient } from "@prisma/client";
|
|
129
161
|
import Sendgrid from '@sendgrid/mail';
|
|
130
162
|
import sgClient from '@sendgrid/client';
|
|
131
163
|
import Twilio from "twilio";
|
|
132
164
|
import { v2 as Cloudinary } from "cloudinary";
|
|
133
|
-
import { randomUUID } from "uncrypto"; // Import randomString from uncrypto
|
|
134
165
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
135
166
|
import Stripe from 'stripe';
|
|
136
167
|
/*
|