propro-utils 1.7.31 → 1.7.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "propro-utils",
3
- "version": "1.7.31",
3
+ "version": "1.7.37",
4
4
  "description": "Auth middleware for propro-auth",
5
5
  "main": "src/index.js",
6
6
  "private": false,
@@ -251,13 +251,18 @@ class AuthMiddleware {
251
251
  account: { accountId: account.accountId, email: account.email },
252
252
  };
253
253
  } catch (error) {
254
- console.error(`Token refresh failed after ${Date.now() - startTime}ms:`, error.message);
254
+ const status = error?.response?.status;
255
+ console.error(`Token refresh failed after ${Date.now() - startTime}ms (status: ${status}):`, error.message);
256
+
257
+ // Immediately clean up lock on failure to prevent blocking
258
+ this.refreshLocks.delete(lockKey);
259
+
255
260
  throw error;
256
261
  } finally {
257
- // Clean up lock after 30 seconds
262
+ // Clean up lock after 30 seconds for successful requests
258
263
  setTimeout(() => {
259
264
  this.refreshLocks.delete(lockKey);
260
- console.log('Refresh lock cleaned up');
265
+ console.log('Refresh lock cleaned up (delayed)');
261
266
  }, 30000);
262
267
  }
263
268
  })();
@@ -270,11 +275,27 @@ class AuthMiddleware {
270
275
  res.status(200).json(result);
271
276
  } catch (error) {
272
277
  console.error('Error refreshing token:', error);
278
+
279
+ const status = error?.response?.status || 401;
273
280
  const errorMessage = error?.response?.data?.message || error?.message || 'Failed to refresh token';
274
- res.status(401).json({
281
+
282
+ // Pass through rate limit status
283
+ if (status === 429) {
284
+ const retryAfter = error?.response?.headers?.['retry-after'];
285
+ return res.status(429).json({
286
+ error: 'Too many requests',
287
+ message: 'Rate limit exceeded. Please try again later.',
288
+ retryAfter: retryAfter ? parseInt(retryAfter) : 900,
289
+ });
290
+ }
291
+
292
+ // Handle 401 unauthorized
293
+ res.status(status).json({
275
294
  error: 'Failed to refresh token',
276
295
  message: errorMessage,
277
- details: 'Your session could not be refreshed. Please log in again.',
296
+ details: status === 401
297
+ ? 'Your session has expired. Please log in again.'
298
+ : 'Your session could not be refreshed. Please log in again.',
278
299
  });
279
300
  }
280
301
  };
@@ -1,5 +1,5 @@
1
- const axios = require('axios');
2
- const jwt = require('jsonwebtoken');
1
+ const axios = require("axios");
2
+ const jwt = require("jsonwebtoken");
3
3
  const tokenCache = new Map();
4
4
 
5
5
  /**
@@ -8,18 +8,26 @@ const tokenCache = new Map();
8
8
  * @param {string} secret - The secret used to sign the JWT.
9
9
  * @returns {promise<object|null>} - The decoded payload of the JWT if it is valid, or null if it is not valid.
10
10
  */
11
- async function verifyJWT(token, secret = 'thisisasamplesecret') {
11
+ async function verifyJWT(token, secret = "thisisasamplesecret") {
12
+ if (!secret) {
13
+ // Require JWT_SECRET from environment variables
14
+ if (!process.env.JWT_SECRET) {
15
+ throw new Error("JWT_SECRET environment variable is required");
16
+ }
17
+ secret = process.env.JWT_SECRET;
18
+ }
19
+
12
20
  try {
13
21
  return jwt.verify(token, secret);
14
22
  } catch (err) {
15
- try{
16
- if (err.name === 'TokenExpiredError') {
17
- const newTokenData = await callTokenEndpoint(token);
18
- return newTokenData ? jwt?.verify?.(newTokenData, secret) : null;
19
- }
20
- }catch(e){
21
- return null
23
+ try {
24
+ if (err.name === "TokenExpiredError") {
25
+ const newTokenData = await callTokenEndpoint(token);
26
+ return newTokenData ? jwt?.verify?.(newTokenData, secret) : null;
22
27
  }
28
+ } catch (e) {
29
+ return null;
30
+ }
23
31
  return null;
24
32
  }
25
33
  }
@@ -59,22 +67,22 @@ async function exchangeToken(
59
67
  try {
60
68
  const formattedRedirectUri = formatRedirectUrl(authUrl);
61
69
  const response = await axios.post(
62
- formattedRedirectUri + '/api/v1/auth/authorize',
70
+ formattedRedirectUri + "/api/v1/auth/authorize",
63
71
  {
64
- grantType: 'authorization_code',
72
+ grantType: "authorization_code",
65
73
  code: code,
66
74
  redirectUri: redirectUri,
67
75
  clientId: clientId,
68
76
  clientSecret: clientSecret,
69
77
  },
70
78
  {
71
- headers: { 'Content-Type': 'application/json' },
79
+ headers: { "Content-Type": "application/json" },
72
80
  }
73
81
  );
74
82
 
75
83
  return response.data;
76
84
  } catch (error) {
77
- console.error('Error exchanging token:', error);
85
+ console.error("Error exchanging token:", error);
78
86
  return null;
79
87
  }
80
88
  }
@@ -84,11 +92,11 @@ async function exchangeToken(
84
92
  * @param {Array} requiredPermissions - Array of required permissions for the user
85
93
  * @returns {Function} - Express middleware function
86
94
  */
87
- const VerifyAccount = requiredPermissions => {
95
+ const VerifyAccount = (requiredPermissions) => {
88
96
  return async (req, res, next) => {
89
- const accessToken = req.headers.authorization?.split(' ')[1];
97
+ const accessToken = req.headers.authorization?.split(" ")[1];
90
98
  if (!accessToken) {
91
- return res.status(401).json({ error: 'Access token is required' });
99
+ return res.status(401).json({ error: "Access token is required" });
92
100
  }
93
101
 
94
102
  // Check if token is in cache
@@ -100,7 +108,7 @@ const VerifyAccount = requiredPermissions => {
100
108
  try {
101
109
  const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
102
110
  if (!isValid(decoded, requiredPermissions)) {
103
- return res.status(403).json({ error: 'Invalid permissions' });
111
+ return res.status(403).json({ error: "Invalid permissions" });
104
112
  }
105
113
  tokenCache.set(accessToken, decoded);
106
114
  req.user = decoded;
@@ -117,14 +125,14 @@ const VerifyAccount = requiredPermissions => {
117
125
  );
118
126
 
119
127
  if (!isValid(userResponse.data, requiredPermissions)) {
120
- return res.status(403).json({ error: 'Invalid permissions' });
128
+ return res.status(403).json({ error: "Invalid permissions" });
121
129
  }
122
130
 
123
131
  tokenCache.set(accessToken, userResponse.data);
124
132
  req.user = userResponse.data;
125
133
  return next();
126
134
  } catch (networkError) {
127
- return res.status(401).json({ error: 'Error validating token' });
135
+ return res.status(401).json({ error: "Error validating token" });
128
136
  }
129
137
  }
130
138
  };
@@ -137,7 +145,7 @@ const VerifyAccount = requiredPermissions => {
137
145
  * @returns {boolean} - Returns true if the decoded token has all the required permissions, false otherwise.
138
146
  */
139
147
  function isValid(decodedToken, requiredPermissions) {
140
- return requiredPermissions.every(permission =>
148
+ return requiredPermissions.every((permission) =>
141
149
  decodedToken.permissions.includes(permission)
142
150
  );
143
151
  }
@@ -152,12 +160,12 @@ function formatRedirectUrl(redirectUrl) {
152
160
  let urlToRedirect;
153
161
 
154
162
  if (
155
- !redirectUrl.startsWith('http://') &&
156
- !redirectUrl.startsWith('https://')
163
+ !redirectUrl.startsWith("http://") &&
164
+ !redirectUrl.startsWith("https://")
157
165
  ) {
158
166
  if (
159
- redirectUrl.includes('localhost') |
160
- redirectUrl.includes('host.docker.internal')
167
+ redirectUrl.includes("localhost") |
168
+ redirectUrl.includes("host.docker.internal")
161
169
  ) {
162
170
  urlToRedirect = `http://${redirectUrl}`;
163
171
  } else {