@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 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 JWT-based token handling.
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
- admin: { secret: "admin-jwt-secret", expiresIn: "1h" },
42
- user: { secret: "user-jwt-secret", expiresIn: "1h" },
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 JWT tokens (optional).
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 JWT support. It sets secure cookies, manages token refresh, and validates tokens on protected routes.
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` for admin login.
68
- - `/portal/auth/login` for user login.
69
- - `/admin/auth/me` to check authentication status for admins.
70
- - `/portal/auth/me` to check authentication status for users.
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**: JWT-based authentication for Admin and User with token-based sessions.
110
+ - **Authentication**: Stack Auth integration for secure user and admin authentication.
83
111
 
84
112
  ## Hooks
85
113
 
86
- - **onRequest**: Validates JWT tokens for protected routes.
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
- ## Notes
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
- - Environment variables such as `DATABASE_URL`, `ADMIN_JWT_SECRET`, and `USER_JWT_SECRET` are expected to be set.
106
- - Services like SendGrid, Twilio, and Cloudinary require API keys to be passed via the options.
107
- - This plugin is highly customizable, with options to enable/disable each feature.
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
- secret: "admin-secret",
128
- expiresIn: "1h",
129
- cookieOptions: {
130
- name: "adminCookie",
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
- secret: "user-secret",
136
- expiresIn: "1h",
137
- cookieOptions: {
138
- name: "userCookie",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xenterprises/fastify-xconfig",
3
3
  "type": "module",
4
- "version": "0.0.10",
4
+ "version": "1.0.0",
5
5
  "description": "Fastify configuration plugin for setting up middleware, services, and route handling.",
6
6
  "main": "src/xConfig.js",
7
7
  "scripts": {
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
- secret: process.env.USER_JWT_SECRET,
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 jwt from "@fastify/jwt";
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
- // Ensure the admin JWT secret is provided
8
- if (!options.admin.secret) {
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 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 || [
19
+ const adminExcludedPaths = options.admin.excludedPaths || [
24
20
  "/admin/auth/login",
25
21
  "/admin/auth/logout",
26
22
  ];
27
23
 
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);
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
- 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",
46
+ return headers;
52
47
  });
53
48
 
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
- };
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
- // Extract token from cookie or Authorization header
84
+ // Get token from header
78
85
  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
- );
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
- // Verify access token
92
- const decoded = await request.adminJwtVerify(token);
93
- request.adminAuth = decoded; // Attach admin auth context
98
+ // Set admin info on request
99
+ request.admin = payload;
100
+ request.adminAuth = { id: payload.sub };
94
101
  } catch (err) {
95
- // Use built-in HTTP error handling
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 route
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
- 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");
113
+ return reply.code(400).send({ error: "Email and password required" });
121
114
  }
122
115
 
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);
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
- reply.send({ accessToken });
146
- } catch (err) {
147
- reply.send(err);
148
- }
149
- });
126
+ const data = await response.json().catch(() => ({}));
150
127
 
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");
128
+ if (!response.ok) {
129
+ return reply.code(response.status || 400).send({
130
+ error: data.message || "Authentication failed"
131
+ });
158
132
  }
159
133
 
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");
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 the refresh token
169
- const isValid = await bcrypt.compare(refreshToken, admin.refreshToken);
170
- if (!isValid) {
171
- throw fastify.httpErrors.unauthorized("Invalid refresh token");
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
- // 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 },
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
- reply.send(err);
152
+ fastify.log.error(err);
153
+ reply.code(500).send({ error: "Internal server error" });
193
154
  }
194
155
  });
195
156
 
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
- }
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
- // Admin authentication status route
219
- fastify.get("/admin/auth/me", async (req, reply) => {
220
- try {
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
- // 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
- });
166
+ if (!token) {
167
+ return reply.code(400).send({ error: "Token required" });
168
+ }
228
169
 
229
- if (!admin) {
230
- throw fastify.httpErrors.notFound("Admin not found");
231
- }
170
+ const payload = await fastify.verifyAdminStackJWT(token);
232
171
 
233
- reply.send(admin);
234
- } catch (err) {
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
+ }
@@ -1,25 +1,22 @@
1
- import jwt from "@fastify/jwt";
2
- import bcrypt from "bcrypt";
3
- import { config } from "process";
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
- // Ensure the user JWT secret is provided
8
- if (!options.user.secret) {
9
- throw new Error("User JWT secret must be provided.");
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
- 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 || [
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
- // Register JWT for user
31
- await fastify.register(jwt, {
32
- secret: userAuthOptions.secret,
33
- sign: { algorithm: 'HS256', expiresIn: userAuthOptions.expiresIn || "15m" },
34
- cookie: { cookieName: userCookieName, signed: false },
35
- namespace: "userJwt",
36
- jwtVerify: "userJwtVerify",
37
- jwtSign: "userJwtSign",
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
- // Common function to set tokens as cookies
41
- const setAuthCookies = (reply, accessToken, refreshToken) => {
42
- reply.setCookie(userCookieName, accessToken, userCookieOptions);
43
- reply.setCookie(userRefreshCookieName, refreshToken, {
44
- // ...userCookieOptions,
45
- httpOnly: true, // Ensures the cookie is not accessible via JavaScript
46
- secure: isProduction, // true in production (HTTPS), false in development (HTTP)
47
- sameSite: isProduction ? 'None' : 'Lax', // 'None' for cross-origin, 'Lax' for development
48
- path: '/', // Ensure cookies are valid for the entire site
49
- });
50
- };
51
-
52
- // User authentication hook
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 authentication for excluded paths
57
- if (userExcludedPaths.some((path) => url.startsWith(path))) {
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
- // Extract token from cookie or Authorization header
79
+ // Get token from header
64
80
  const authHeader = request.headers.authorization;
65
- const authToken =
66
- authHeader && authHeader.startsWith("Bearer ")
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 access token using the namespaced verify method
78
- const decoded = await request.userJwtVerify(token);
79
- request.userAuth = decoded; // Attach user auth context
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
- // Validate input
95
- if (!email || !password || !firstName || !lastName) {
96
- throw fastify.httpErrors.badRequest("Missing required fields");
97
- }
89
+ if (!payload) {
90
+ return reply.code(401).send({ error: "Invalid token" });
91
+ }
98
92
 
99
- // Check if user already exists
100
- const existingUser = await fastify.prisma.users.findUnique({
101
- where: { email },
102
- });
103
- if (existingUser) {
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
- // User login route
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
- throw fastify.httpErrors.badRequest(
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
- // Compare passwords using bcrypt
168
- const isValidPassword = await bcrypt.compare(password, user.password);
169
- if (!isValidPassword) {
170
- throw fastify.httpErrors.unauthorized("Invalid credentials");
171
- }
172
-
173
- // Issue access token
174
- const accessToken = await reply.userJwtSign({ id: user.id });
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
- // Set tokens as cookies
187
- setAuthCookies(reply, accessToken, refreshToken);
121
+ const data = await response.json().catch(() => ({}));
188
122
 
189
- reply.send({ accessToken });
190
- } catch (err) {
191
- reply.send(err);
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
- // Fetch user from the database using the refresh token
205
- const user = await fastify.prisma.users.findFirst({
206
- where: { id: userAuth?.id, refreshToken: { not: null } },
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 the refresh token
214
- const isValid = await bcrypt.compare(refreshToken, user.refreshToken);
215
- if (!isValid) {
216
- throw fastify.httpErrors.unauthorized("Invalid refresh token");
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
- // Issue new access token
220
- const accessToken = await reply.userJwtSign({ id: user.id });
221
-
222
- // Generate new refresh token
223
- const newRefreshToken = fastify.randomUUID();
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
- reply.send(err);
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
- reply.send({ message: "Logged out successfully" });
258
- } catch (err) {
259
- reply.send(err);
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
- // User authentication status route
264
- fastify.get("/portal/auth/me", async (req, reply) => {
265
- try {
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
- // Fetch user details from database
269
- const user = await fastify.prisma.users.findUnique({
270
- where: { id: userAuth.id },
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
- if (!user) {
275
- throw fastify.httpErrors.notFound("User not found");
276
- }
166
+ const payload = await fastify.verifyStackJWT(token);
277
167
 
278
- reply.send(user);
279
- } catch (err) {
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
+ }
@@ -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 JWT-based token handling.
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
- * admin: { secret: 'admin-jwt-secret', expiresIn: '1h' },
33
- * user: { secret: 'user-jwt-secret', expiresIn: '1h' },
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 JWT tokens (optional).
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 JWT support. It sets secure cookies,
56
- * manages token refresh, and validates tokens on protected routes. For example:
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**: JWT-based authentication for Admin and User with token-based sessions.
93
+ * - **Authentication**: Stack Auth integration for secure user and admin authentication.
72
94
  *
73
95
  * *Hooks:*
74
- * - `onRequest`: Validates JWT tokens for protected routes.
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`, `ADMIN_JWT_SECRET`, and `USER_JWT_SECRET` are expected to be set.
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
- * secret: 'admin-secret',
111
- * expiresIn: '1h',
112
- * cookieOptions: { name: 'adminCookie', refreshTokenName: 'adminRefreshToken' },
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
- * secret: 'user-secret',
116
- * expiresIn: '1h',
117
- * cookieOptions: { name: 'userCookie', refreshTokenName: 'userRefreshToken' },
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 jwt from "@fastify/jwt";
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
  /*