propro-utils 1.7.29 → 1.7.31

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.
@@ -42,7 +42,6 @@ const authValidation = (requiredPermissions = []) => {
42
42
  req.headers.authorization?.split(' ')[1];
43
43
 
44
44
  if (!accessToken) {
45
- console.log('Access token is required');
46
45
  return res.status(403).json({ error: 'Access token is required' });
47
46
  }
48
47
 
@@ -66,7 +65,6 @@ const authValidation = (requiredPermissions = []) => {
66
65
  );
67
66
 
68
67
  if (!validPermissions) {
69
- console.log('Invalid permissions');
70
68
  return res.status(403).json({ error: 'Invalid permissions' });
71
69
  }
72
70
 
@@ -77,14 +75,12 @@ const authValidation = (requiredPermissions = []) => {
77
75
  user = await checkIfUserExists(accountId);
78
76
  if (!user) throw new Error('User not found');
79
77
  } catch (error) {
80
- console.log('User not found 1');
81
78
  return res.status(403).json({error: error?.message || 'User not found'});
82
79
  }
83
80
 
84
81
  req.user = user.id;
85
82
  next();
86
83
  } catch (error) {
87
- console.log(error.message);
88
84
  if (error.response && error.response.status) {
89
85
  next(new Error(error.response.data.message));
90
86
  }
@@ -1,11 +1,11 @@
1
- require('dotenv').config();
2
- const axios = require('axios');
3
- const { getOrSetCache } = require('../utils/redis');
4
- const { v4: uuidv4 } = require('uuid');
5
- const ServiceManager = require('../utils/serviceManager');
1
+ require("dotenv").config();
2
+ const axios = require("axios");
3
+ const { getOrSetCache } = require("../utils/redis");
4
+ const { v4: uuidv4 } = require("uuid");
5
+ const ServiceManager = require("../utils/serviceManager");
6
6
  const defaultUserGlobalStyleShortcuts =
7
- require('./defaultUserGlobalStyleShortcuts.json').defaultGlobalStyleShortcuts;
8
- const defaultFolders = require('./defaultFolders.json').defaultFolders;
7
+ require("./defaultUserGlobalStyleShortcuts.json").defaultGlobalStyleShortcuts;
8
+ const defaultFolders = require("./defaultFolders.json").defaultFolders;
9
9
 
10
10
  /**
11
11
  * Retrieves the account profile data from the authentication server and caches it using Redis.
@@ -20,10 +20,10 @@ const defaultFolders = require('./defaultFolders.json').defaultFolders;
20
20
  const getAccountProfile = async (redisClient, userSchema, accountId) => {
21
21
  try {
22
22
  const accessToken =
23
- req.cookies['x-access-token'] || req.headers.authorization?.split(' ')[1];
23
+ req.cookies["x-access-token"] || req.headers.authorization?.split(" ")[1];
24
24
 
25
25
  if (!accessToken) {
26
- throw new Error('Access token is required');
26
+ throw new Error("Access token is required");
27
27
  }
28
28
 
29
29
  const fetchPermission = async () => {
@@ -49,7 +49,7 @@ const getAccountProfile = async (redisClient, userSchema, accountId) => {
49
49
  );
50
50
 
51
51
  if (!profileData) {
52
- throw new Error('Invalid permissions');
52
+ throw new Error("Invalid permissions");
53
53
  }
54
54
 
55
55
  return profileData;
@@ -57,7 +57,7 @@ const getAccountProfile = async (redisClient, userSchema, accountId) => {
57
57
  if (error.response && error.response.status) {
58
58
  throw new Error(error.response.data.message);
59
59
  }
60
- throw new Error('Error validating token');
60
+ throw new Error("Error validating token");
61
61
  }
62
62
  };
63
63
 
@@ -140,8 +140,8 @@ async function createUserGlobalStyles(userStyleSchema, accountId) {
140
140
  */
141
141
  async function createDefaultFolders(folderSchema, accountId) {
142
142
  try {
143
- console.log('Creating default folders for user:', accountId);
144
- const folderPromises = defaultFolders.map(folder =>
143
+ console.log("Creating default folders for user:", accountId);
144
+ const folderPromises = defaultFolders.map((folder) =>
145
145
  folderSchema.create({
146
146
  ...folder,
147
147
  user_id: accountId,
@@ -149,17 +149,17 @@ async function createDefaultFolders(folderSchema, accountId) {
149
149
  );
150
150
  return Promise.all(folderPromises);
151
151
  } catch (error) {
152
- console.error('Error in createDefaultFolders:', error);
153
- throw new Error('Failed to create default folders');
152
+ console.error("Error in createDefaultFolders:", error);
153
+ throw new Error("Failed to create default folders");
154
154
  }
155
155
  }
156
156
 
157
157
  const DEFAULT_THEME = {
158
158
  canvasBackground: '#1E1D1D',
159
159
  defaultItemWidth: 200,
160
- defaultColor: '#ffffff',
161
- fontSize: '16px',
162
- name: 'Default Theme',
160
+ defaultColor: "#ffffff",
161
+ fontSize: "16px",
162
+ name: "Default Theme",
163
163
  isDefault: true,
164
164
  };
165
165
 
@@ -173,40 +173,40 @@ const DEFAULT_THEME = {
173
173
  */
174
174
  const checkIfUserExists = async accountId => {
175
175
  // Input validation
176
- if (!accountId || typeof accountId !== 'string') {
177
- console.warn('Invalid accountId provided:', accountId);
178
- throw new Error('Invalid accountId provided');
176
+ if (!accountId || typeof accountId !== "string") {
177
+ console.warn("Invalid accountId provided:", accountId);
178
+ throw new Error("Invalid accountId provided");
179
179
  }
180
180
 
181
181
  try {
182
182
  const schemaResults = await Promise.all([
183
- ServiceManager.getService('UserSchema'),
184
- ServiceManager.getService('UserStyleSchema'),
185
- ServiceManager.getService('FolderSchema'),
186
- ServiceManager.getService('ThemeSchema'),
183
+ ServiceManager.getService("UserSchema"),
184
+ ServiceManager.getService("UserStyleSchema"),
185
+ ServiceManager.getService("FolderSchema"),
186
+ ServiceManager.getService("ThemeSchema"),
187
187
  ]);
188
188
 
189
189
  const [userSchema, userStyleSchema, folderSchema, themeSchema] =
190
190
  schemaResults;
191
191
 
192
192
  if (!userSchema) {
193
- throw new Error('UserSchema service not available');
193
+ throw new Error("UserSchema service not available");
194
194
  }
195
195
 
196
196
  // Optional schemas warning
197
197
  if (!userStyleSchema) {
198
198
  console.warn(
199
- 'UserStyleSchema service not available - style features will be skipped'
199
+ "UserStyleSchema service not available - style features will be skipped"
200
200
  );
201
201
  }
202
202
  if (!folderSchema) {
203
203
  console.warn(
204
- 'FolderSchema service not available - folder features will be skipped'
204
+ "FolderSchema service not available - folder features will be skipped"
205
205
  );
206
206
  }
207
207
  if (!themeSchema) {
208
208
  console.warn(
209
- 'ThemeSchema service not available - theme features will be skipped'
209
+ "ThemeSchema service not available - theme features will be skipped"
210
210
  );
211
211
  }
212
212
 
@@ -219,7 +219,7 @@ const checkIfUserExists = async accountId => {
219
219
  if (user) {
220
220
  const updates = [];
221
221
 
222
- // Handle userGlobalStyles
222
+ // Handle userGlobalStyles - check properly if styles exist in the database
223
223
  if (userStyleSchema) {
224
224
  // Consolidate any duplicate global styles
225
225
  await consolidateGlobalStyles(accountId);
@@ -231,19 +231,19 @@ const checkIfUserExists = async accountId => {
231
231
  (async () => {
232
232
  const existingFolders = await folderSchema
233
233
  .find({ user_id: user.id })
234
- .select('name')
234
+ .select("name")
235
235
  .lean();
236
236
 
237
237
  const existingFolderNames = new Set(
238
- existingFolders.map(f => f.name)
238
+ existingFolders.map((f) => f.name)
239
239
  );
240
240
  const foldersToCreate = defaultFolders.filter(
241
- folder => !existingFolderNames.has(folder.name)
241
+ (folder) => !existingFolderNames.has(folder.name)
242
242
  );
243
243
 
244
244
  if (foldersToCreate.length > 0) {
245
245
  return folderSchema.insertMany(
246
- foldersToCreate.map(folder => ({
246
+ foldersToCreate.map((folder) => ({
247
247
  ...folder,
248
248
  user_id: user.id,
249
249
  }))
@@ -258,7 +258,7 @@ const checkIfUserExists = async accountId => {
258
258
  updates.push(
259
259
  (async () => {
260
260
  const defaultThemeExists = await themeSchema.exists({
261
- name: 'Default Theme',
261
+ name: "Default Theme",
262
262
  accountId,
263
263
  });
264
264
 
@@ -283,11 +283,8 @@ const checkIfUserExists = async accountId => {
283
283
  // Wait for all updates to complete
284
284
  await Promise.all(updates);
285
285
 
286
- // Return fresh user data after updates, excluding specified fields
287
- return userSchema
288
- .findOne({ accountId })
289
- .select('-theme -folderList -userGlobalStyles -mapList')
290
- .lean();
286
+ // Return fresh user data after updates
287
+ return userSchema.findOne({ accountId }).lean();
291
288
  }
292
289
 
293
290
  // Create new user with all associated data
@@ -298,7 +295,7 @@ const checkIfUserExists = async accountId => {
298
295
  let userGlobalStyles;
299
296
  if (userStyleSchema) {
300
297
  creationTasks.push(
301
- createUserGlobalStyles(userStyleSchema, accountId).then(styles => {
298
+ createUserGlobalStyles(userStyleSchema, accountId).then((styles) => {
302
299
  userGlobalStyles = styles;
303
300
  return styles;
304
301
  })
@@ -316,8 +313,8 @@ const checkIfUserExists = async accountId => {
316
313
  userId,
317
314
  id: uuidv4(),
318
315
  })
319
- .then(theme => {
320
- console.log('Created theme:', theme);
316
+ .then((theme) => {
317
+ console.log("Created theme:", theme);
321
318
  defaultTheme = theme;
322
319
  return theme;
323
320
  })
@@ -346,7 +343,7 @@ const checkIfUserExists = async accountId => {
346
343
  .select('-theme -folderList -userGlobalStyles -mapList')
347
344
  .lean();
348
345
  } catch (error) {
349
- console.error('Detailed error in checkIfUserExists:', {
346
+ console.error("Detailed error in checkIfUserExists:", {
350
347
  accountId,
351
348
  errorName: error.name,
352
349
  errorMessage: error.message,
@@ -9,10 +9,9 @@ const refreshLimiter = rateLimit({
9
9
  message: 'Too many refresh requests from this IP, please try again after 15 minutes',
10
10
  });
11
11
 
12
- const refreshTokenCache = new Map();
13
-
14
12
  /**
15
13
  * Middleware to refresh access token using refresh token.
14
+ * Note: Cache removed to prevent stale token issues. Token rotation handles efficiency.
16
15
  *
17
16
  * @param {Object} req - The request object.
18
17
  * @param {Object} res - The response object.
@@ -29,11 +28,6 @@ async function refreshTokenMiddleware(req, res, next) {
29
28
  return res.status(400).json({ error: 'Invalid refresh token format' });
30
29
  }
31
30
 
32
- if (refreshTokenCache.has(refreshToken)) {
33
- req.newAccessToken = refreshTokenCache.get(refreshToken);
34
- return next();
35
- }
36
-
37
31
  try {
38
32
  const response = await axios.post(
39
33
  `${process.env.AUTH_URL}/oauth/token`,
@@ -43,11 +37,13 @@ async function refreshTokenMiddleware(req, res, next) {
43
37
  client_id: process.env.CLIENT_ID,
44
38
  client_secret: process.env.CLIENT_SECRET,
45
39
  }),
46
- { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
40
+ {
41
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
42
+ timeout: 10000 // 10 second timeout
43
+ }
47
44
  );
48
45
 
49
46
  if (response.data && response.data.access_token) {
50
- refreshTokenCache.set(refreshToken, response.data.access_token);
51
47
  req.newAccessToken = response.data.access_token;
52
48
  next();
53
49
  } else {
@@ -55,7 +51,9 @@ async function refreshTokenMiddleware(req, res, next) {
55
51
  }
56
52
  } catch (error) {
57
53
  const statusCode = error.response?.status || 500;
58
- res.status(statusCode).json({ error: error.response?.data?.error || 'Error refreshing token' });
54
+ const errorMessage = error.response?.data?.error || error.message || 'Error refreshing token';
55
+ console.error('Refresh token middleware error:', errorMessage);
56
+ res.status(statusCode).json({ error: errorMessage });
59
57
  }
60
58
  }
61
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "propro-utils",
3
- "version": "1.7.29",
3
+ "version": "1.7.31",
4
4
  "description": "Auth middleware for propro-auth",
5
5
  "main": "src/index.js",
6
6
  "private": false,
@@ -47,6 +47,7 @@
47
47
  "jest-mock-axios": "^4.7.3",
48
48
  "jsonwebtoken": "^9.0.2",
49
49
  "multer": "^1.4.5-lts.1",
50
+ "neverthrow": "^8.2.0",
50
51
  "nodemailer": "^6.9.7",
51
52
  "nodemailer-mailgun-transport": "^2.1.5",
52
53
  "querystring": "^0.2.1",
@@ -54,5 +55,6 @@
54
55
  "supertest": "^6.3.4",
55
56
  "test": "^3.3.0",
56
57
  "uuid": "^9.0.1"
57
- }
58
+ },
59
+ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
58
60
  }
@@ -55,6 +55,8 @@ class AuthMiddleware {
55
55
  this.userStyleSchema = userStyleSchema;
56
56
  this.themeSchema = themeSchema;
57
57
  this.router = Router();
58
+ // Track in-flight refresh requests to prevent race conditions
59
+ this.refreshLocks = new Map();
58
60
  this.initializeRoutes();
59
61
  }
60
62
 
@@ -171,7 +173,6 @@ class AuthMiddleware {
171
173
  user = await checkIfUserExists(account.accountId);
172
174
  if (!user) throw new Error('User not found');
173
175
  } catch (error) {
174
- console.log('User not found 3');
175
176
  return res.status(403).json({error: error?.message || 'User not found'});
176
177
  }
177
178
 
@@ -179,7 +180,6 @@ class AuthMiddleware {
179
180
 
180
181
  res.redirect(formatRedirectUrl(this.options.appUrl));
181
182
  } catch (error) {
182
- console.error('Error in callback:', error);
183
183
  res.status(500).send('Internal Server Error');
184
184
  }
185
185
  };
@@ -188,40 +188,94 @@ class AuthMiddleware {
188
188
  const refreshToken =
189
189
  req.cookies['x-refresh-token'] || req.headers['x-refresh-token'];
190
190
  if (!refreshToken) {
191
+ console.error('Refresh token missing in request');
191
192
  return res.status(401).json({
192
193
  redirectUrl: this.constructRedirectUrl(),
193
194
  error: 'No refresh token provided',
195
+ message: 'Authentication required. Please log in.',
194
196
  });
195
197
  }
196
198
 
197
- try {
198
- const response = await this.refreshTokens(refreshToken);
199
- const { account, access, refresh } = response.data;
200
-
201
- if (!account || !access || !refresh) {
202
- return res
203
- .status(401)
204
- .json({ error: 'Invalid or expired refresh token' });
199
+ // Prevent concurrent refresh requests for the same token
200
+ const lockKey = refreshToken;
201
+ if (this.refreshLocks.has(lockKey)) {
202
+ // Wait for the in-flight request to complete
203
+ console.log('Waiting for in-flight refresh request to complete...');
204
+ try {
205
+ const result = await this.refreshLocks.get(lockKey);
206
+ console.log('Reusing result from in-flight refresh request');
207
+ return res.status(200).json(result);
208
+ } catch (error) {
209
+ console.error('In-flight refresh request failed:', error);
210
+ return res.status(401).json({
211
+ error: 'Failed to refresh token',
212
+ message: 'Session refresh failed. Please log in again.',
213
+ });
205
214
  }
215
+ }
206
216
 
207
- const user = await checkIfUserExists(account.accountId);
217
+ // Create a promise for this refresh operation
218
+ const refreshPromise = (async () => {
219
+ console.log('Starting refresh token operation...');
220
+ const startTime = Date.now();
221
+
222
+ try {
223
+ const response = await this.refreshTokens(refreshToken);
224
+ const { account, access, refresh } = response.data;
208
225
 
209
- const { returnTokens } = req.query;
210
- if (returnTokens === 'true') {
211
- return res.status(200).json({ account, user, access, refresh });
226
+ if (!account || !access || !refresh) {
227
+ throw new Error('Invalid or expired refresh token');
228
+ }
229
+
230
+ console.log(`Token refresh successful for account: ${account.accountId}`);
231
+
232
+ const user = await checkIfUserExists(account.accountId);
233
+
234
+ const { returnTokens } = req.query;
235
+ if (returnTokens === 'true') {
236
+ console.log(`Refresh completed in ${Date.now() - startTime}ms (returning tokens)`);
237
+ return { account, user, access, refresh };
238
+ }
239
+
240
+ setAuthCookies(
241
+ res,
242
+ { access, refresh },
243
+ account,
244
+ user,
245
+ this.options.appUrl
246
+ );
247
+
248
+ console.log(`Refresh completed in ${Date.now() - startTime}ms (cookies set)`);
249
+ return {
250
+ message: 'Token refreshed successfully',
251
+ account: { accountId: account.accountId, email: account.email },
252
+ };
253
+ } catch (error) {
254
+ console.error(`Token refresh failed after ${Date.now() - startTime}ms:`, error.message);
255
+ throw error;
256
+ } finally {
257
+ // Clean up lock after 30 seconds
258
+ setTimeout(() => {
259
+ this.refreshLocks.delete(lockKey);
260
+ console.log('Refresh lock cleaned up');
261
+ }, 30000);
212
262
  }
213
- setAuthCookies(
214
- res,
215
- { access, refresh },
216
- account,
217
- user,
218
- this.options.appUrl
219
- );
263
+ })();
264
+
265
+ // Store the promise so concurrent requests can await it
266
+ this.refreshLocks.set(lockKey, refreshPromise);
220
267
 
221
- res.status(200).json({ message: 'Token refreshed successfully' });
268
+ try {
269
+ const result = await refreshPromise;
270
+ res.status(200).json(result);
222
271
  } catch (error) {
223
272
  console.error('Error refreshing token:', error);
224
- res.status(401).json({ error: 'Failed to refresh token' });
273
+ const errorMessage = error?.response?.data?.message || error?.message || 'Failed to refresh token';
274
+ res.status(401).json({
275
+ error: 'Failed to refresh token',
276
+ message: errorMessage,
277
+ details: 'Your session could not be refreshed. Please log in again.',
278
+ });
225
279
  }
226
280
  };
227
281
 
@@ -12,10 +12,14 @@ async function verifyJWT(token, secret = 'thisisasamplesecret') {
12
12
  try {
13
13
  return jwt.verify(token, secret);
14
14
  } catch (err) {
15
- if (err.name === 'TokenExpiredError') {
16
- const newTokenData = await callTokenEndpoint(token);
17
- return newTokenData ? jwt.verify(newTokenData, secret) : null;
18
- }
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
22
+ }
19
23
  return null;
20
24
  }
21
25
  }
@@ -96,7 +100,6 @@ const VerifyAccount = requiredPermissions => {
96
100
  try {
97
101
  const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
98
102
  if (!isValid(decoded, requiredPermissions)) {
99
- console.log('Invalid permissions 1');
100
103
  return res.status(403).json({ error: 'Invalid permissions' });
101
104
  }
102
105
  tokenCache.set(accessToken, decoded);
@@ -114,7 +117,6 @@ const VerifyAccount = requiredPermissions => {
114
117
  );
115
118
 
116
119
  if (!isValid(userResponse.data, requiredPermissions)) {
117
- console.log('Invalid permissions 2');
118
120
  return res.status(403).json({ error: 'Invalid permissions' });
119
121
  }
120
122