propro-utils 1.7.28 → 1.7.30

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
 
@@ -74,17 +72,15 @@ const authValidation = (requiredPermissions = []) => {
74
72
 
75
73
  let user = null;
76
74
  try {
77
- user = await checkIfUserExists(account.accountId);
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.28",
3
+ "version": "1.7.30",
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
  };
@@ -194,31 +194,58 @@ class AuthMiddleware {
194
194
  });
195
195
  }
196
196
 
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' });
197
+ // Prevent concurrent refresh requests for the same token
198
+ const lockKey = refreshToken;
199
+ if (this.refreshLocks.has(lockKey)) {
200
+ // Wait for the in-flight request to complete
201
+ try {
202
+ const result = await this.refreshLocks.get(lockKey);
203
+ return res.status(200).json(result);
204
+ } catch (error) {
205
+ return res.status(401).json({ error: 'Failed to refresh token' });
205
206
  }
207
+ }
206
208
 
207
- const user = await checkIfUserExists(account.accountId);
209
+ // Create a promise for this refresh operation
210
+ const refreshPromise = (async () => {
211
+ try {
212
+ const response = await this.refreshTokens(refreshToken);
213
+ const { account, access, refresh } = response.data;
208
214
 
209
- const { returnTokens } = req.query;
210
- if (returnTokens === 'true') {
211
- return res.status(200).json({ account, user, access, refresh });
215
+ if (!account || !access || !refresh) {
216
+ throw new Error('Invalid or expired refresh token');
217
+ }
218
+
219
+ const user = await checkIfUserExists(account.accountId);
220
+
221
+ const { returnTokens } = req.query;
222
+ if (returnTokens === 'true') {
223
+ return { account, user, access, refresh };
224
+ }
225
+
226
+ setAuthCookies(
227
+ res,
228
+ { access, refresh },
229
+ account,
230
+ user,
231
+ this.options.appUrl
232
+ );
233
+
234
+ return { message: 'Token refreshed successfully' };
235
+ } finally {
236
+ // Clean up lock after 30 seconds
237
+ setTimeout(() => {
238
+ this.refreshLocks.delete(lockKey);
239
+ }, 30000);
212
240
  }
213
- setAuthCookies(
214
- res,
215
- { access, refresh },
216
- account,
217
- user,
218
- this.options.appUrl
219
- );
241
+ })();
220
242
 
221
- res.status(200).json({ message: 'Token refreshed successfully' });
243
+ // Store the promise so concurrent requests can await it
244
+ this.refreshLocks.set(lockKey, refreshPromise);
245
+
246
+ try {
247
+ const result = await refreshPromise;
248
+ res.status(200).json(result);
222
249
  } catch (error) {
223
250
  console.error('Error refreshing token:', error);
224
251
  res.status(401).json({ error: 'Failed to refresh token' });
@@ -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