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(
|
|
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(
|
|
2
|
-
const axios = require(
|
|
3
|
-
const { getOrSetCache } = require(
|
|
4
|
-
const { v4: uuidv4 } = require(
|
|
5
|
-
const ServiceManager = require(
|
|
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(
|
|
8
|
-
const defaultFolders = require(
|
|
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[
|
|
23
|
+
req.cookies["x-access-token"] || req.headers.authorization?.split(" ")[1];
|
|
24
24
|
|
|
25
25
|
if (!accessToken) {
|
|
26
|
-
throw new Error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
153
|
-
throw new Error(
|
|
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:
|
|
161
|
-
fontSize:
|
|
162
|
-
name:
|
|
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 !==
|
|
177
|
-
console.warn(
|
|
178
|
-
throw new Error(
|
|
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(
|
|
184
|
-
ServiceManager.getService(
|
|
185
|
-
ServiceManager.getService(
|
|
186
|
-
ServiceManager.getService(
|
|
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(
|
|
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
|
-
|
|
199
|
+
"UserStyleSchema service not available - style features will be skipped"
|
|
200
200
|
);
|
|
201
201
|
}
|
|
202
202
|
if (!folderSchema) {
|
|
203
203
|
console.warn(
|
|
204
|
-
|
|
204
|
+
"FolderSchema service not available - folder features will be skipped"
|
|
205
205
|
);
|
|
206
206
|
}
|
|
207
207
|
if (!themeSchema) {
|
|
208
208
|
console.warn(
|
|
209
|
-
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
{
|
|
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
|
-
|
|
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.
|
|
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
|
}
|
package/src/server/index.js
CHANGED
|
@@ -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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
res,
|
|
215
|
-
{ access, refresh },
|
|
216
|
-
account,
|
|
217
|
-
user,
|
|
218
|
-
this.options.appUrl
|
|
219
|
-
);
|
|
241
|
+
})();
|
|
220
242
|
|
|
221
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|