propro-utils 1.5.76 → 1.5.77

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.
@@ -109,105 +109,193 @@ const DEFAULT_THEME = {
109
109
  };
110
110
 
111
111
  /**
112
- * Checks if a user exists based on the given account ID.
113
- * If the user does not exist, creates a new user with the given account ID.
114
- * If the user exists but doesn't have global styles, creates them.
112
+ * Checks and initializes a user account with all required associated data.
113
+ * Handles user creation, global styles, folders, and themes in an optimized way.
115
114
  *
116
- * @param {string} accountId - The account ID of the user to check/create.
117
- * @returns {Promise<Object>} A promise that resolves to the user object.
118
- * @throws {Error} If there's an issue with database operations.
115
+ * @param {string} accountId - The account ID of the user to check/create
116
+ * @returns {Promise<Object>} A promise that resolves to the complete user object
117
+ * @throws {Error} If there's an issue with database operations or invalid input
119
118
  */
120
119
  const checkIfUserExists = async accountId => {
120
+ // Input validation
121
+ if (!accountId || typeof accountId !== 'string') {
122
+ throw new Error('Invalid accountId provided');
123
+ }
124
+
121
125
  try {
122
- const userSchema = await ServiceManager.getService('UserSchema');
123
- const userStyleSchema = await ServiceManager.getService('UserStyleSchema');
124
- const folderSchema = await ServiceManager.getService('FolderSchema');
125
- const themeSchema = await ServiceManager.getService('ThemeSchema');
126
+ // Load all required schemas in parallel
127
+ const [userSchema, userStyleSchema, folderSchema, themeSchema] =
128
+ await Promise.all([
129
+ ServiceManager.getService('UserSchema'),
130
+ ServiceManager.getService('UserStyleSchema'),
131
+ ServiceManager.getService('FolderSchema'),
132
+ ServiceManager.getService('ThemeSchema'),
133
+ ]);
134
+
135
+ if (!userSchema) {
136
+ throw new Error('UserSchema service not available');
137
+ }
126
138
 
127
- let user = await userSchema
139
+ // Find existing user with populated fields
140
+ const user = await userSchema
128
141
  .findOne({ accountId })
129
142
  .populate('userGlobalStyles')
130
- .populate('theme');
143
+ .populate('theme')
144
+ .lean();
131
145
 
132
146
  if (user) {
133
- if (!user.userGlobalStyles) {
134
- user.userGlobalStyles = await createUserGlobalStyles(
135
- userStyleSchema,
136
- accountId
137
- );
138
- await user.save();
139
- } else if (user.userGlobalStyles.styleShortcuts.length === 0) {
140
- user.userGlobalStyles.styleShortcuts = defaultUserGlobalStyleShortcuts;
141
- await user.userGlobalStyles.save();
147
+ const updates = [];
148
+
149
+ // Handle userGlobalStyles
150
+ if (userStyleSchema) {
151
+ if (!user.userGlobalStyles) {
152
+ updates.push(
153
+ createUserGlobalStyles(userStyleSchema, accountId).then(
154
+ async styles => {
155
+ await userSchema.updateOne(
156
+ { _id: user._id },
157
+ { userGlobalStyles: styles._id }
158
+ );
159
+ return styles;
160
+ }
161
+ )
162
+ );
163
+ } else if (user.userGlobalStyles.styleShortcuts?.length === 0) {
164
+ updates.push(
165
+ userStyleSchema.updateOne(
166
+ { _id: user.userGlobalStyles._id },
167
+ { styleShortcuts: defaultUserGlobalStyleShortcuts }
168
+ )
169
+ );
170
+ }
142
171
  }
143
- // Check if user has any folders
144
- const userFolders = await folderSchema.find({ user_id: user.id });
145
- const defaultFolderNames = defaultFolders.map(folder => folder.name);
146
- const userFolderNames = userFolders.map(folder => folder.name);
147
172
 
148
- const foldersToCreate = defaultFolders.filter(
149
- folder => !userFolderNames.includes(folder.name)
150
- );
173
+ // Handle folders
174
+ if (folderSchema) {
175
+ updates.push(
176
+ (async () => {
177
+ const existingFolders = await folderSchema
178
+ .find({ user_id: user.id })
179
+ .select('name')
180
+ .lean();
181
+
182
+ const existingFolderNames = new Set(
183
+ existingFolders.map(f => f.name)
184
+ );
185
+ const foldersToCreate = defaultFolders.filter(
186
+ folder => !existingFolderNames.has(folder.name)
187
+ );
151
188
 
152
- if (foldersToCreate.length > 0) {
153
- await Promise.all(
154
- foldersToCreate.map(folder =>
155
- folderSchema.create({
156
- ...folder,
157
- user_id: user.id,
158
- })
159
- )
189
+ if (foldersToCreate.length > 0) {
190
+ return folderSchema.insertMany(
191
+ foldersToCreate.map(folder => ({
192
+ ...folder,
193
+ user_id: user.id,
194
+ }))
195
+ );
196
+ }
197
+ })()
160
198
  );
161
199
  }
162
200
 
163
- // Check if user has any themes
164
- const userThemes = await themeSchema.find({
165
- name: 'Default Theme',
166
- accountId,
167
- });
168
- if (userThemes.length === 0) {
169
- const defaultTheme = await themeSchema.create({
170
- ...DEFAULT_THEME,
171
- accountId,
172
- userId: user.id,
173
- id: uuidv4(),
174
- });
175
- user.theme = [defaultTheme._id];
176
- await user.save();
201
+ // Handle themes
202
+ if (themeSchema) {
203
+ updates.push(
204
+ (async () => {
205
+ const defaultThemeExists = await themeSchema.exists({
206
+ name: 'Default Theme',
207
+ accountId,
208
+ });
209
+
210
+ if (!defaultThemeExists) {
211
+ const defaultTheme = await themeSchema.create({
212
+ ...DEFAULT_THEME,
213
+ accountId,
214
+ userId: user.id,
215
+ id: uuidv4(),
216
+ });
217
+
218
+ await userSchema.updateOne(
219
+ { _id: user._id },
220
+ { theme: [defaultTheme._id] }
221
+ );
222
+ }
223
+ })()
224
+ );
177
225
  }
178
226
 
179
- return user;
227
+ // Wait for all updates to complete
228
+ await Promise.all(updates);
229
+
230
+ // Return fresh user data after updates
231
+ return userSchema
232
+ .findOne({ accountId })
233
+ .populate('userGlobalStyles')
234
+ .populate('theme');
180
235
  }
181
236
 
182
- const userGlobalStyles = await createUserGlobalStyles(
183
- userStyleSchema,
184
- accountId
185
- );
237
+ // Create new user with all associated data
238
+ const userId = uuidv4();
239
+ const creationTasks = [];
186
240
 
187
- user = await userSchema.create({
241
+ // Prepare user global styles
242
+ let userGlobalStyles;
243
+ if (userStyleSchema) {
244
+ creationTasks.push(
245
+ createUserGlobalStyles(userStyleSchema, accountId).then(styles => {
246
+ userGlobalStyles = styles;
247
+ return styles;
248
+ })
249
+ );
250
+ }
251
+
252
+ // Prepare default theme
253
+ let defaultTheme;
254
+ if (themeSchema) {
255
+ creationTasks.push(
256
+ themeSchema
257
+ .create({
258
+ ...DEFAULT_THEME,
259
+ accountId,
260
+ userId,
261
+ id: uuidv4(),
262
+ })
263
+ .then(theme => {
264
+ defaultTheme = theme;
265
+ return theme;
266
+ })
267
+ );
268
+ }
269
+
270
+ // Wait for parallel creation tasks
271
+ await Promise.all(creationTasks);
272
+
273
+ // Create the user with all references
274
+ const newUser = await userSchema.create({
188
275
  accountId,
189
- id: uuidv4(),
276
+ id: userId,
190
277
  verified: false,
191
- userGlobalStyles: userGlobalStyles._id,
278
+ userGlobalStyles: userGlobalStyles?._id,
279
+ theme: defaultTheme ? [defaultTheme._id] : undefined,
192
280
  });
193
281
 
194
- const folders = await createDefaultFolders(folderSchema, user.id);
195
- console.log('Folders created:', folders);
196
- user.folders = folders.map(folder => folder._id);
282
+ // Create folders if schema available
283
+ if (folderSchema) {
284
+ await createDefaultFolders(folderSchema, newUser.id);
285
+ }
197
286
 
198
- const defaultTheme = await themeSchema.create({
199
- ...DEFAULT_THEME,
287
+ // Return complete user object with populated fields
288
+ return userSchema
289
+ .findById(newUser._id)
290
+ .populate('userGlobalStyles')
291
+ .populate('theme');
292
+ } catch (error) {
293
+ console.error('Error in checkIfUserExists:', {
200
294
  accountId,
201
- userId: user.id,
202
- id: uuidv4(),
295
+ error: error.message,
296
+ stack: error.stack,
203
297
  });
204
- user.theme = [defaultTheme._id];
205
- await user.save();
206
-
207
- return user;
208
- } catch (error) {
209
- console.error('Error in checkIfUserExists:', error);
210
- throw new Error('Failed to get or create user');
298
+ throw new Error(`Failed to get or create user: ${error.message}`);
211
299
  }
212
300
  };
213
301
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "propro-utils",
3
- "version": "1.5.76",
3
+ "version": "1.5.77",
4
4
  "description": "Auth middleware for propro-auth",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -57,16 +57,32 @@ const sanitizeUser = user => {
57
57
  };
58
58
 
59
59
  /**
60
- * Sets the authentication cookies in the response object.
61
- *
62
- * @param {Object} res - The response object.
63
- * @param {Object} tokens - The authentication tokens.
64
- * @param {Object} account - The user's account information.
65
- * @param {Object} user - The user's information.
66
- * @param {string} appUrl - The URL of the client application.
60
+ * Sets a cookie using Chrome Extension API
61
+ * @param {Object} details - Cookie details
62
+ * @returns {Promise} Promise that resolves when cookie is set
67
63
  */
68
- const setAuthCookies = (res, tokens, account, user, appUrl) => {
69
- if (!tokens || !tokens.refresh || !tokens.access) {
64
+ const setChromeExtensionCookie = details => {
65
+ return new Promise((resolve, reject) => {
66
+ try {
67
+ chrome.cookies.set(details, cookie => {
68
+ if (chrome.runtime.lastError) {
69
+ reject(chrome.runtime.lastError);
70
+ } else {
71
+ resolve(cookie);
72
+ }
73
+ });
74
+ } catch (error) {
75
+ // Not in extension context
76
+ resolve(null);
77
+ }
78
+ });
79
+ };
80
+
81
+ /**
82
+ * Sets cookies for both web and extension contexts
83
+ */
84
+ const setAuthCookies = async (res, tokens, account, user, appUrl) => {
85
+ if (!tokens?.refresh?.token || !tokens?.access?.token) {
70
86
  throw new Error('Invalid tokens object');
71
87
  }
72
88
  if (!account) {
@@ -82,96 +98,131 @@ const setAuthCookies = (res, tokens, account, user, appUrl) => {
82
98
  const accessMaxAge =
83
99
  new Date(tokens.access.expires).getTime() - currentDateTime.getTime();
84
100
 
101
+ // Domain configuration
85
102
  let domain;
86
103
  try {
87
104
  domain = appUrl ? new URL(appUrl).hostname : undefined;
88
- if (domain && domain.includes('mapmap.app')) {
105
+ if (domain?.includes('mapmap.app')) {
89
106
  domain = '.mapmap.app';
90
107
  }
108
+ if (domain?.includes('localhost')) {
109
+ domain = undefined;
110
+ }
91
111
  } catch (error) {
92
- console.error('Invalid appUrl:', error);
112
+ console.error('Invalid appUrl:', { error, appUrl });
93
113
  domain = undefined;
94
114
  }
95
115
 
96
116
  const commonAttributes = {
97
- secure: process.env.NODE_ENV === 'production',
98
- sameSite: process.env.NODE_ENV === 'production' ? 'None' : 'Lax',
99
- domain: domain,
117
+ secure: true,
118
+ sameSite: 'None',
119
+ domain,
120
+ path: '/',
100
121
  };
101
122
 
102
- if (commonAttributes.sameSite === 'None') {
103
- commonAttributes.secure = true;
104
- }
105
-
106
- // Set httpOnly cookies
107
- res.cookie('x-refresh-token', tokens.refresh.token, {
108
- ...commonAttributes,
109
- httpOnly: true,
110
- maxAge: refreshMaxAge,
111
- });
112
-
113
- res.cookie('x-access-token', tokens.access.token, {
114
- ...commonAttributes,
115
- httpOnly: true,
116
- maxAge: accessMaxAge,
117
- });
123
+ const httpOnlyCookies = {
124
+ 'x-refresh-token': {
125
+ value: tokens.refresh.token,
126
+ maxAge: refreshMaxAge,
127
+ httpOnly: true,
128
+ },
129
+ 'x-access-token': {
130
+ value: tokens.access.token,
131
+ maxAge: accessMaxAge,
132
+ httpOnly: true,
133
+ },
134
+ };
118
135
 
119
- // Set non-httpOnly cookies
120
136
  const sanitizedUser = sanitizeUser(user);
121
137
  const sanitizedAccount = { ...account };
122
138
  delete sanitizedAccount.passwordHistory;
123
139
 
124
- res.cookie('user', safeStringify(sanitizedUser), {
125
- ...commonAttributes,
126
- maxAge: refreshMaxAge,
127
- });
140
+ const regularCookies = {
141
+ user: {
142
+ value: safeStringify(sanitizedUser),
143
+ maxAge: refreshMaxAge,
144
+ },
145
+ account: {
146
+ value: safeStringify(sanitizedAccount),
147
+ maxAge: refreshMaxAge,
148
+ },
149
+ has_account_token: {
150
+ value: JSON.stringify({ value: 'true', expires: accessMaxAge }),
151
+ maxAge: accessMaxAge,
152
+ },
153
+ };
128
154
 
129
- res.cookie('account', safeStringify(sanitizedAccount), {
130
- ...commonAttributes,
131
- maxAge: refreshMaxAge,
132
- });
155
+ try {
156
+ Object.entries({ ...httpOnlyCookies, ...regularCookies }).forEach(
157
+ ([name, config]) => {
158
+ res.cookie(name, config.value, {
159
+ ...commonAttributes,
160
+ ...config,
161
+ });
162
+ }
163
+ );
133
164
 
134
- res.cookie(
135
- 'has_account_token',
136
- JSON.stringify({ value: 'true', expires: accessMaxAge }),
137
- {
138
- ...commonAttributes,
139
- maxAge: accessMaxAge,
140
- }
141
- );
165
+ const extensionCookiePromises = Object.entries({
166
+ ...httpOnlyCookies,
167
+ ...regularCookies,
168
+ }).map(([name, config]) => {
169
+ return setChromeExtensionCookie({
170
+ url: `https://${domain || 'mapmap.app'}`,
171
+ name,
172
+ value: config.value,
173
+ secure: true,
174
+ httpOnly: !!config.httpOnly,
175
+ sameSite: 'no_restriction',
176
+ path: '/',
177
+ expirationDate: Math.floor((Date.now() + config.maxAge) / 1000),
178
+ domain: domain?.startsWith('.') ? domain : `.${domain || 'mapmap.app'}`,
179
+ });
180
+ });
181
+
182
+ await Promise.allSettled(extensionCookiePromises);
142
183
 
143
- console.log('Setting auth cookies');
144
- console.log('sanitizedUser', sanitizedUser);
184
+ console.log('Auth cookies set successfully', {
185
+ domain,
186
+ sameSite: commonAttributes.sameSite,
187
+ cookieNames: [
188
+ ...Object.keys(httpOnlyCookies),
189
+ ...Object.keys(regularCookies),
190
+ ],
191
+ });
192
+ } catch (error) {
193
+ console.error('Error setting cookies:', {
194
+ error: error.message,
195
+ stack: error.stack,
196
+ });
197
+ throw new Error('Failed to set authentication cookies');
198
+ }
145
199
  };
146
200
 
147
201
  /**
148
- * Clears all authentication cookies from the response object.
149
- *
150
- * @param {Object} res - The response object.
151
- * @param {string} appUrl - The URL of the client application.
202
+ * Clears cookies from both web and extension contexts
152
203
  */
153
- const clearAuthCookies = (res, appUrl) => {
204
+ const clearAuthCookies = async (res, appUrl) => {
154
205
  let domain;
155
206
  try {
156
207
  domain = appUrl ? new URL(appUrl).hostname : undefined;
157
- if (domain && domain.includes('mapmap.app')) {
208
+ if (domain?.includes('mapmap.app')) {
158
209
  domain = '.mapmap.app';
159
210
  }
211
+ if (domain?.includes('localhost')) {
212
+ domain = undefined;
213
+ }
160
214
  } catch (error) {
161
215
  console.error('Invalid appUrl:', error);
162
216
  domain = undefined;
163
217
  }
164
218
 
165
219
  const commonAttributes = {
166
- secure: process.env.NODE_ENV === 'production',
167
- sameSite: process.env.NODE_ENV === 'production' ? 'None' : 'Lax',
168
- domain: domain,
220
+ secure: true,
221
+ sameSite: 'None',
222
+ domain,
223
+ path: '/',
169
224
  };
170
225
 
171
- if (commonAttributes.sameSite === 'None') {
172
- commonAttributes.secure = true;
173
- }
174
-
175
226
  const cookieNames = [
176
227
  'x-refresh-token',
177
228
  'x-access-token',
@@ -180,11 +231,35 @@ const clearAuthCookies = (res, appUrl) => {
180
231
  'has_account_token',
181
232
  ];
182
233
 
234
+ // Clear web cookies
183
235
  cookieNames.forEach(cookieName => {
184
236
  res.clearCookie(cookieName, commonAttributes);
185
237
  });
186
238
 
187
- console.log('Cleared auth cookies');
239
+ try {
240
+ const extensionClearPromises = cookieNames.map(
241
+ name =>
242
+ new Promise(resolve => {
243
+ chrome.cookies.remove(
244
+ {
245
+ url: `https://${domain || 'mapmap.app'}`,
246
+ name,
247
+ },
248
+ resolve
249
+ );
250
+ })
251
+ );
252
+
253
+ await Promise.allSettled(extensionClearPromises);
254
+ } catch (error) {
255
+ // Not in extension context, ignore
256
+ }
257
+
258
+ console.log('Auth cookies cleared successfully', {
259
+ domain,
260
+ cookieNames,
261
+ sameSite: commonAttributes.sameSite,
262
+ });
188
263
  };
189
264
 
190
265
  module.exports = {