-
Notifications
You must be signed in to change notification settings - Fork 0
Optimize: admin tools #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e967ba4
929cd5f
1496065
0bd0a02
4eae6a4
d6a6eb4
9a7a729
c1fb661
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,9 @@ import OwnershipTransferHandler from '../../../../app/src/Features/Collaborators | |
| import HttpErrorHandler from '../../../../app/src/Features/Errors/HttpErrorHandler.mjs' | ||
| import ErrorController from '../../../../app/src/Features/Errors/ErrorController.mjs' | ||
| import Errors, { OError } from '../../../../app/src/Features/Errors/Errors.js' | ||
| import HaveIBeenPwned from '../../../../app/src/Features/Authentication/HaveIBeenPwned.mjs' | ||
| import { db } from '../../../../app/src/infrastructure/mongodb.mjs' | ||
| import AuthenticationManager from '../../../../app/src/Features/Authentication/AuthenticationManager.mjs' | ||
|
|
||
| const __dirname = Path.dirname(fileURLToPath(import.meta.url)) | ||
|
|
||
|
|
@@ -212,17 +214,23 @@ async function _getUsers( | |
| samlIdentifiers: 1, | ||
| thirdPartyIdentifiers: 1, | ||
| suspended: 1, | ||
| 'features.collaborators': 1, | ||
| 'features.compileTimeout': 1, | ||
| } | ||
| const projectionDeleted = {}; | ||
| for (const key of Object.keys(projection)) { | ||
| projectionDeleted[key] = `$user.${key}` | ||
| } | ||
| projectionDeleted.deletedAt = '$deleterData.deletedAt' | ||
|
|
||
| const activeUsers = await UserGetter.promises.getUsers({}, projection) | ||
| const activeUsers = await User.find({}, projection) | ||
| .limit(1000) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Applying Useful? React with 👍 / 👎. |
||
| .lean() | ||
|
|
||
| const deletedUsers = await DeletedUser.aggregate([ | ||
| { $match: { user: { $type: 'object' } } }, | ||
| { $project: projectionDeleted }, | ||
| { $limit: 1000 }, | ||
| ]) | ||
|
|
||
| const allUsers = [...activeUsers, ...deletedUsers] | ||
|
|
@@ -237,6 +245,62 @@ async function _getUsers( | |
| } | ||
| } | ||
|
|
||
| async function _searchUsers(searchTerm) { | ||
| const projection = { | ||
| _id: 1, | ||
| email: 1, | ||
| first_name: 1, | ||
| last_name: 1, | ||
| lastActive: 1, | ||
| lastLoggedIn: 1, | ||
| signUpDate: 1, | ||
| loginCount: 1, | ||
| isAdmin: 1, | ||
| hashedPassword: 1, | ||
| samlIdentifiers: 1, | ||
| thirdPartyIdentifiers: 1, | ||
| suspended: 1, | ||
| 'features.collaborators': 1, | ||
| 'features.compileTimeout': 1, | ||
| } | ||
|
|
||
| const activeUsers = await User.find({ | ||
| $or: [ | ||
| { email: { $regex: searchTerm, $options: 'i' } }, | ||
| { first_name: { $regex: searchTerm, $options: 'i' } }, | ||
| { last_name: { $regex: searchTerm, $options: 'i' } }, | ||
| { $expr: { $regexMatch: { input: { $toString: '$_id' }, regex: searchTerm, options: 'i' } } } | ||
| ] | ||
| }, projection) | ||
| .limit(1000) | ||
| .lean() | ||
|
|
||
| const projectionDeleted = {}; | ||
| for (const key of Object.keys(projection)) { | ||
| projectionDeleted[key] = `$user.${key}` | ||
| } | ||
| projectionDeleted.deletedAt = '$deleterData.deletedAt' | ||
|
|
||
| const deletedUsers = await DeletedUser.aggregate([ | ||
| { $match: { user: { $type: 'object' }, $or: [ | ||
| { 'user.email': { $regex: searchTerm, $options: 'i' } }, | ||
| { 'user.first_name': { $regex: searchTerm, $options: 'i' } }, | ||
| { 'user.last_name': { $regex: searchTerm, $options: 'i' } }, | ||
| { $expr: { $regexMatch: { input: { $toString: '$user._id' }, regex: searchTerm, options: 'i' } } } | ||
| ] } }, | ||
| { $project: projectionDeleted }, | ||
| { $limit: 1000 }, | ||
| ]) | ||
|
|
||
| let allUsers = [...activeUsers, ...deletedUsers] | ||
| const formattedUsers = _formatUsers(allUsers) | ||
|
|
||
| return { | ||
| totalSize: formattedUsers.length, | ||
| users: formattedUsers, | ||
| } | ||
| } | ||
|
|
||
| function _formatUsers(users) { | ||
| const formattedUsers = [] | ||
| const yearAgo = new Date() | ||
|
|
@@ -313,6 +377,10 @@ function _formatUserInfo(user, maxDate) { | |
| authMethods, | ||
| allowUpdateDetails, | ||
| allowUpdateIsAdmin, | ||
| features: user.features && { | ||
| collaborators: user.features.collaborators, | ||
| compileTimeout: user.features.compileTimeout, | ||
| }, | ||
| ...(user.suspended && { suspended: user.suspended }), | ||
| inactive: !user.lastActive || user.lastActive < maxDate, | ||
| ...(user.deletedAt && { deletedAt: user.deletedAt }), | ||
|
|
@@ -543,6 +611,46 @@ async function updateUser(req, res, next) { | |
|
|
||
| for (let [key, value] of Object.entries(updatesInput)) { | ||
| if (key === 'email') continue | ||
| // Update features if needed | ||
| if (key === 'features' && value && typeof value === 'object') { | ||
| const features = updatesInput.features | ||
| if (features && typeof features === 'object') { | ||
| if ('collaborators' in features && (!Number.isInteger(features.collaborators) || features.collaborators < -1)) { | ||
| return HttpErrorHandler.unprocessableEntity(req, res, 'invalid_collaborators') | ||
| } | ||
| if ('compileTimeout' in features && (!Number.isInteger(features.compileTimeout) || features.compileTimeout <= 0)) { | ||
| return HttpErrorHandler.unprocessableEntity(req, res, 'invalid_compile_timeout') | ||
| } | ||
| } | ||
| update.features = { | ||
| ...(user.features ?? {}), | ||
| ...value, | ||
| } | ||
| continue | ||
| } | ||
| // Update password if needed | ||
| if (key === 'password') { | ||
| // ignore empty password updates | ||
| if (value == null || value === '') { | ||
| continue | ||
| } | ||
| // validate password | ||
| if (typeof value !== 'string' || value.length < 8) { | ||
| return HttpErrorHandler.unprocessableEntity(req, res, 'Password must be at least 8 characters long') | ||
| } | ||
| let isPasswordReused | ||
| try { | ||
| isPasswordReused = await HaveIBeenPwned.promises.checkPasswordForReuse(value) | ||
| } catch (err) { | ||
| logger.warn({ err }, 'Failed to check password against HaveIBeenPwned') | ||
| } | ||
| if (isPasswordReused) { | ||
| return HttpErrorHandler.unprocessableEntity(req, res, 'Password is too common, please choose a different one') | ||
| } | ||
| const hashedPassword = await AuthenticationManager.promises.hashPassword(value) | ||
| update.hashedPassword = hashedPassword | ||
| continue | ||
| } | ||
|
|
||
| const newValue = typeof value === 'string' ? value.trim() : value | ||
| if (newValue === user[key]) continue | ||
|
|
@@ -575,6 +683,13 @@ async function updateUser(req, res, next) { | |
| delete update.last_name | ||
| } | ||
|
|
||
| // delete password from response for security reasons | ||
| if (update.password) { | ||
| delete update.password | ||
| } | ||
| if (update.hashedPassword) { | ||
| delete update.hashedPassword | ||
| } | ||
| return res.json(update) | ||
| } | ||
|
|
||
|
|
@@ -603,9 +718,21 @@ async function getAdditionalUserInfo(req, res, next) { | |
| res.json({ activationLink }) | ||
| } | ||
|
|
||
| async function getUsersJsonBySearch(req, res) { | ||
| const { search } = req.body | ||
| if (typeof search !== 'string' || search.trim() === '') { | ||
| return HttpErrorHandler.unprocessableEntity(req, res, 'Search term is empty') | ||
| } | ||
|
|
||
| const usersPage = await _searchUsers(search.trim()) | ||
| res.json(usersPage) | ||
| } | ||
|
|
||
|
|
||
| export default { | ||
| manageUsersPage: expressify(manageUsersPage), | ||
| getUsersJson: expressify(getUsersJson), | ||
| getUsersJsonBySearch: expressify(getUsersJsonBySearch), | ||
| getAdditionalUserInfo: expressify(getAdditionalUserInfo), | ||
| registerNewUser: expressify(registerNewUser), | ||
| activateAccountPage: expressify(activateAccountPage), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cap is applied before project filters and sorting, so when there are more than 1000 projects the list only reflects an arbitrary prefix of records; admins can miss matching deleted/trashed/inactive projects and
totalSizeno longer represents the real result set.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, filter in backend is deprecated.