From dc8641a2498885e54142b4223d2c9dd8f2f35b53 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Tue, 25 Jul 2023 17:54:02 -0300 Subject: [PATCH 1/4] refactor: Unifying all the code for user creation into a new service. --- apps/meteor/.meteor/packages | 1 + apps/meteor/.meteor/versions | 1 + .../authentication/server/startup/index.js | 234 +----------- apps/meteor/app/cas/server/cas_server.js | 4 +- apps/meteor/app/crowd/server/crowd.ts | 41 ++- .../server/custom_oauth_server.js | 35 +- apps/meteor/app/dolphin/server/lib.ts | 4 +- .../server/classes/ImportDataConverter.ts | 6 +- .../app/lib/server/functions/saveUser.js | 3 +- .../meteor-accounts-saml/server/lib/SAML.ts | 20 +- .../app/slackbridge/server/RocketAdapter.js | 4 +- .../externals/meteor/oauth-encryption.d.ts | 16 + apps/meteor/server/methods/registerUser.ts | 8 +- apps/meteor/server/services/startup.ts | 2 + apps/meteor/server/services/user/service.ts | 334 ++++++++++++++++++ packages/core-services/src/index.ts | 5 + .../core-services/src/types/IUserService.ts | 35 ++ packages/core-typings/src/IUser.ts | 4 +- .../model-typings/src/models/IUsersModel.ts | 2 +- 19 files changed, 474 insertions(+), 285 deletions(-) create mode 100644 apps/meteor/definition/externals/meteor/oauth-encryption.d.ts create mode 100644 apps/meteor/server/services/user/service.ts create mode 100644 packages/core-services/src/types/IUserService.ts diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 37b95896612c..c738fd9d93c1 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -75,3 +75,4 @@ autoupdate@1.8.0 jquery zodern:types +oauth-encryption diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 5e394526000d..db1b2156f7d7 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -63,6 +63,7 @@ mongo-dev-server@1.1.0 mongo-id@1.0.8 npm-mongo@4.16.0 oauth@2.2.0 +oauth-encryption@1.3.2 oauth1@1.5.1 oauth2@1.3.2 ordered-dict@1.1.0 diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 0c3915355135..7debe1dfa447 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -1,26 +1,17 @@ import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; import { Accounts } from 'meteor/accounts-base'; -import _ from 'underscore'; -import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; -import { Roles, Settings, Users } from '@rocket.chat/models'; +import { escapeHTML } from '@rocket.chat/string-helpers'; +import { Users } from '@rocket.chat/models'; +import { User } from '@rocket.chat/core-services'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles'; -import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser'; -import { parseCSV } from '../../../../lib/utils/parseCSV'; import { isValidAttemptByUser, isValidLoginAttemptByIp } from '../lib/restrictLoginAttempts'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; -import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator'; -import { safeGetMeteorUser } from '../../../utils/server/functions/safeGetMeteorUser'; import { safeHtmlDots } from '../../../../lib/utils/safeHtmlDots'; -import { joinDefaultChannels } from '../../../lib/server/functions/joinDefaultChannels'; -import { setAvatarFromServiceWithValidation } from '../../../lib/server/functions/setUserAvatar'; import { i18n } from '../../../../server/lib/i18n'; -import { beforeCreateUserCallback } from '../../../../lib/callbacks/beforeCreateUserCallback'; Accounts.config({ forbidClientAccountCreation: true, @@ -135,186 +126,9 @@ Accounts.emailTemplates.enrollAccount.html = function (user = {} /* , url*/) { }); }; -const getLinkedInName = ({ firstName, lastName }) => { - const { preferredLocale, localized: firstNameLocalized } = firstName; - const { localized: lastNameLocalized } = lastName; - - // LinkedIn new format - if (preferredLocale && firstNameLocalized && preferredLocale.language && preferredLocale.country) { - const locale = `${preferredLocale.language}_${preferredLocale.country}`; - - if (firstNameLocalized[locale] && lastNameLocalized[locale]) { - return `${firstNameLocalized[locale]} ${lastNameLocalized[locale]}`; - } - if (firstNameLocalized[locale]) { - return firstNameLocalized[locale]; - } - } - - // LinkedIn old format - if (!lastName) { - return firstName; - } - return `${firstName} ${lastName}`; -}; - -const onCreateUserAsync = async function (options, user = {}) { - await beforeCreateUserCallback.run(options, user); - - user.status = 'offline'; - user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); - - if (!user.name) { - if (options.profile) { - if (options.profile.name) { - user.name = options.profile.name; - } else if (options.profile.firstName) { - // LinkedIn format - user.name = getLinkedInName(options.profile); - } - } - } - - if (user.services) { - const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); - - for (const service of Object.values(user.services)) { - if (!user.name) { - user.name = service.name || service.username; - } - - if (!user.emails && service.email) { - user.emails = [ - { - address: service.email, - verified, - }, - ]; - } - } - } - - if (!user.active) { - const destinations = []; - const usersInRole = await Roles.findUsersInRole('admin'); - await usersInRole.forEach((adminUser) => { - if (Array.isArray(adminUser.emails)) { - adminUser.emails.forEach((email) => { - destinations.push(`${adminUser.name}<${email.address}>`); - }); - } - }); - - const email = { - to: destinations, - from: settings.get('From_Email'), - subject: Accounts.emailTemplates.userToActivate.subject(), - html: Accounts.emailTemplates.userToActivate.html({ - ...options, - name: options.name || options.profile?.name, - email: options.email || user.emails[0].address, - }), - }; - - await Mailer.send(email); - } - - await callbacks.run('onCreateUser', options, user); - - // App IPostUserCreated event hook - await Apps.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: await safeGetMeteorUser() }); - - return user; -}; - -Accounts.onCreateUser(function (...args) { - // Depends on meteor support for Async - return Promise.await(onCreateUserAsync.call(this, ...args)); -}); - -const { insertUserDoc } = Accounts; -const insertUserDocAsync = async function (options, user) { - const globalRoles = []; - - if (Match.test(user.globalRoles, [String]) && user.globalRoles.length > 0) { - globalRoles.push(...user.globalRoles); - } - - delete user.globalRoles; - - if (user.services && !user.services.password) { - const defaultAuthServiceRoles = parseCSV(settings.get('Accounts_Registration_AuthenticationServices_Default_Roles') || ''); - - if (defaultAuthServiceRoles.length > 0) { - globalRoles.push(...defaultAuthServiceRoles); - } - } - - const roles = getNewUserRoles(globalRoles); - - if (!user.type) { - user.type = 'user'; - } - - if (settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In')) { - user.services = user.services || {}; - user.services.email2fa = { - enabled: true, - changedAt: new Date(), - }; - } - - const _id = insertUserDoc.call(Accounts, options, user); - - user = await Users.findOne({ - _id, - }); - - if (user.username) { - if (options.joinDefaultChannels !== false && user.joinDefaultChannels !== false) { - await joinDefaultChannels(_id, options.joinDefaultChannelsSilenced); - } - - if (user.type !== 'visitor') { - setImmediate(function () { - return callbacks.run('afterCreateUser', user); - }); - } - if (settings.get('Accounts_SetDefaultAvatar') === true) { - const avatarSuggestions = await getAvatarSuggestionForUser(user); - for await (const service of Object.keys(avatarSuggestions)) { - const avatarData = avatarSuggestions[service]; - if (service !== 'gravatar') { - await setAvatarFromServiceWithValidation(_id, avatarData.blob, '', service); - break; - } - } - } - } - - /** - * if settings shows setup wizard to be pending - * and no admin's been found, - * and existing role list doesn't include admin - * create this user admin. - * count this as the completion of setup wizard step 1. - */ - const hasAdmin = await Users.findOneByRolesAndType('admin', 'user', { projection: { _id: 1 } }); - if (!roles.includes('admin') && !hasAdmin) { - roles.push('admin'); - if (settings.get('Show_Setup_Wizard') === 'pending') { - await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); - } - } - - await addUserRolesAsync(_id, roles); - - return _id; -}; - Accounts.insertUserDoc = function (...args) { // Depends on meteor support for Async - return Promise.await(insertUserDocAsync.call(this, ...args)); + return Promise.await(User.create(...args)); }; const validateLoginAttemptAsync = async function (login) { @@ -389,46 +203,6 @@ Accounts.validateLoginAttempt(function (...args) { return Promise.await(validateLoginAttemptAsync.call(this, ...args)); }); -Accounts.validateNewUser(function (user) { - if (user.type === 'visitor') { - return true; - } - - if ( - settings.get('Accounts_Registration_AuthenticationServices_Enabled') === false && - settings.get('LDAP_Enable') === false && - !(user.services && user.services.password) - ) { - throw new Meteor.Error('registration-disabled-authentication-services', 'User registration is disabled for authentication services'); - } - - return true; -}); - -Accounts.validateNewUser(function (user) { - if (user.type === 'visitor') { - return true; - } - - let domainWhiteList = settings.get('Accounts_AllowedDomainsList'); - if (_.isEmpty(domainWhiteList?.trim())) { - return true; - } - - domainWhiteList = domainWhiteList.split(',').map((domain) => domain.trim()); - - if (user.emails && user.emails.length > 0) { - const email = user.emails[0].address; - const inWhiteList = domainWhiteList.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); - - if (inWhiteList === false) { - throw new Meteor.Error('error-invalid-domain'); - } - } - - return true; -}); - export const MAX_RESUME_LOGIN_TOKENS = parseInt(process.env.MAX_RESUME_LOGIN_TOKENS) || 50; Accounts.onLogin(async ({ user }) => { diff --git a/apps/meteor/app/cas/server/cas_server.js b/apps/meteor/app/cas/server/cas_server.js index 1abb08663eba..a18736aeedfb 100644 --- a/apps/meteor/app/cas/server/cas_server.js +++ b/apps/meteor/app/cas/server/cas_server.js @@ -7,6 +7,7 @@ import { RoutePolicy } from 'meteor/routepolicy'; import _ from 'underscore'; import { CredentialTokens, Rooms, Users } from '@rocket.chat/models'; import { validate } from '@rocket.chat/cas-validate'; +import { User } from '@rocket.chat/core-services'; import { logger } from './cas_rocketchat'; import { settings } from '../../settings/server'; @@ -209,7 +210,6 @@ Accounts.registerLoginHandler('cas', async function (options) { const newUser = { username: result.username, active: true, - globalRoles: ['user'], emails: [], services: { cas: { @@ -243,7 +243,7 @@ Accounts.registerLoginHandler('cas', async function (options) { // Create the user logger.debug(`User "${result.username}" does not exist yet, creating it`); - const userId = Accounts.insertUserDoc({}, newUser); + const userId = await User.create({ globalRoles: ['user'] }, newUser); // Fetch and use it user = await Users.findOneById(userId); diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index 38a6b57a9c88..899e0cfa450b 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -4,6 +4,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Users } from '@rocket.chat/models'; import type { IUser } from '@rocket.chat/core-typings'; import { cronJobs } from '@rocket.chat/cron'; +import { User } from '@rocket.chat/core-services'; import { _setRealName } from '../../lib/server/functions/setRealName'; import { settings } from '../../settings/server'; @@ -13,6 +14,17 @@ import { logger } from './logger'; import { crowdIntervalValuesToCronMap } from '../../../server/settings/crowd'; type CrowdUser = Pick & { crowd: Record; crowd_username: string }; +type CrowdUserData = { + displayname: string; + username: string; + email: string; + active: boolean; + crowd_username: string; + password?: string; + _id?: string; + + crowd?: boolean; +}; function fallbackDefaultAccountSystem(bind: typeof Accounts, username: string | Record, password: string) { if (typeof username === 'string') { @@ -73,7 +85,7 @@ export class CROWD { await this.crowdClient.ping(); } - async fetchCrowdUser(crowdUsername: string) { + async fetchCrowdUser(crowdUsername: string): Promise { const userResponse = await this.crowdClient.user.find(crowdUsername); return { @@ -85,7 +97,7 @@ export class CROWD { }; } - async authenticate(username: string, password: string) { + async authenticate(username: string, password: string): Promise<{ crowd: false } | (CrowdUserData & { password: string }) | undefined> { if (!username || !password) { logger.error('No username or password'); return; @@ -141,21 +153,20 @@ export class CROWD { return; } - const crowdUser: Record = await this.fetchCrowdUser(crowdUsername); + const crowdUser = await this.fetchCrowdUser(crowdUsername); - if (user && settings.get('CROWD_Allow_Custom_Username') === true) { + if (user?.username && settings.get('CROWD_Allow_Custom_Username') === true) { crowdUser.username = user.username; } - if (user) { - crowdUser._id = user._id; - } - crowdUser.password = password; - - return crowdUser; + return { + ...crowdUser, + password, + ...(user && { _id: user._id }), + }; } - async syncDataToUser(crowdUser: Record, id: string) { + async syncDataToUser(crowdUser: CrowdUserData, id: string) { const user = { username: this.cleanUsername(crowdUser.username), crowd_username: crowdUser.crowd_username, @@ -209,7 +220,7 @@ export class CROWD { continue; } - let crowdUser = null; + let crowdUser: CrowdUserData | null = null; try { crowdUser = await this.fetchCrowdUser(crowdUsername); @@ -242,7 +253,7 @@ export class CROWD { crowdUser = await this.fetchCrowdUser(crowdUsername); } - if (settings.get('CROWD_Allow_Custom_Username') === true) { + if (settings.get('CROWD_Allow_Custom_Username') === true && user.username) { crowdUser.username = user.username; } @@ -257,7 +268,7 @@ export class CROWD { return username; } - async updateUserCollection(crowdUser: Record) { + async updateUserCollection(crowdUser: CrowdUserData & { password: string }) { const userQuery = { _id: crowdUser._id, }; @@ -287,7 +298,7 @@ export class CROWD { // Attempt to create the new user try { - crowdUser._id = await Accounts.createUserAsync(crowdUser); + crowdUser._id = await User.createWithPassword(crowdUser); // sync the user data await this.syncDataToUser(crowdUser, crowdUser._id); diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index abfdafed6f53..9876c69fe6e3 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -14,6 +14,7 @@ import { isURL } from '../../../lib/utils/isURL'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; import { callbacks } from '../../../lib/callbacks'; import { settings } from '../../settings/server'; +import { beforeCreateUserCallback } from '../../../lib/callbacks/beforeCreateUserCallback'; const logger = new Logger('CustomOAuth'); @@ -377,25 +378,29 @@ export class CustomOAuth { } }); - Accounts.validateNewUser((user) => { - if (!user.services || !user.services[this.name] || !user.services[this.name].id) { - return true; - } + beforeCreateUserCallback.add( + (options, user) => { + if (!user.services || !user.services[this.name] || !user.services[this.name].id) { + return options; + } - if (this.usernameField) { - user.username = user.services[this.name].username; - } + if (this.usernameField) { + user.username = user.services[this.name].username; + } - if (this.emailField) { - user.email = user.services[this.name].email; - } + if (this.emailField) { + user.email = user.services[this.name].email; + } - if (this.nameField) { - user.name = user.services[this.name].name; - } + if (this.nameField) { + user.name = user.services[this.name].name; + } - return true; - }); + return options; + }, + callbacks.priority.HIGH, + `oauth-${this.name}`, + ); } registerAccessTokenService(name) { diff --git a/apps/meteor/app/dolphin/server/lib.ts b/apps/meteor/app/dolphin/server/lib.ts index bb00b684b3b3..afc118eac140 100644 --- a/apps/meteor/app/dolphin/server/lib.ts +++ b/apps/meteor/app/dolphin/server/lib.ts @@ -1,6 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; import type { IUser } from '@rocket.chat/core-typings'; +import type { UserDocOptions } from '@rocket.chat/core-services'; +import type { InsertionModel } from '@rocket.chat/model-typings'; import { settings } from '../../settings/server'; import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server'; @@ -22,7 +24,7 @@ const config = { const Dolphin = new CustomOAuth('dolphin', config); -function DolphinOnCreateUser(options: any, user?: IUser) { +function DolphinOnCreateUser(options: UserDocOptions, user?: InsertionModel) { // TODO: callbacks Fix this if (user?.services?.dolphin?.NickName) { user.username = user.services.dolphin.NickName; diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 7fba63ed4ca4..d9a85e319f00 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -1,4 +1,3 @@ -import { Accounts } from 'meteor/accounts-base'; import { ObjectId } from 'mongodb'; import type { IImportUser, @@ -15,6 +14,7 @@ import type { IMessage as IDBMessage, } from '@rocket.chat/core-typings'; import { ImportData, Rooms, Users, Subscriptions } from '@rocket.chat/models'; +import { User } from '@rocket.chat/core-services'; import type { IConversionCallbacks } from '../definitions/IConversionCallbacks'; import { addUserToDefaultChannels } from '../../../lib/server/functions/addUserToDefaultChannels'; @@ -290,11 +290,11 @@ export class ImportDataConverter { async insertUser(userData: IImportUser): Promise { const password = `${Date.now()}${userData.name || ''}${userData.emails.length ? userData.emails[0].toUpperCase() : ''}`; const userId = userData.emails.length - ? await Accounts.createUserAsync({ + ? await User.createWithPassword({ email: userData.emails[0], password, }) - : await Accounts.createUserAsync({ + : await User.createWithPassword({ username: userData.username, password, joinDefaultChannelsSilenced: true, diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 94eb3246989f..194569e1df37 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -4,6 +4,7 @@ import _ from 'underscore'; import Gravatar from 'gravatar'; import { isUserFederated } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; +import { User } from '@rocket.chat/core-services'; import * as Mailer from '../../../mailer/server/api'; import { getRoles } from '../../../authorization/server'; @@ -284,7 +285,7 @@ const saveNewUser = async function (userData, sendPassword) { createUser.email = userData.email; } - const _id = await Accounts.createUserAsync(createUser); + const _id = await User.createWithPassword(createUser); const updateUser = { $set: { diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index b827a4330d5b..d7fa043cf06d 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -6,6 +6,8 @@ import { Accounts } from 'meteor/accounts-base'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import type { IUser, IIncomingMessage, IPersonalAccessToken } from '@rocket.chat/core-typings'; import { CredentialTokens, Rooms, Users } from '@rocket.chat/models'; +import type { InsertionModel } from '@rocket.chat/model-typings'; +import { User } from '@rocket.chat/core-services'; import { settings } from '../../../settings/server'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; @@ -117,7 +119,7 @@ export class SAML { user = await SAML.findUser(userObject.username, emailRegex); } - const emails = userObject.emailList.map((email) => ({ + const emails: IUser['emails'] = userObject.emailList.map((email) => ({ address: email, verified: settings.get('Accounts_Verify_Email_For_External_Accounts'), })); @@ -130,23 +132,21 @@ export class SAML { // If we received any role from the mapping, use them - otherwise use the default role for creation. const roles = userObject.roles?.length ? userObject.roles : ensureArray(defaultUserRole.split(',')); - const newUser: Record = { - name: userObject.fullName, + const newUser: Partial> = { + name: userObject.fullName || '', active, - globalRoles: roles, emails, services: { saml: { - provider: userObject.samlLogin.provider, + provider: userObject.samlLogin.provider || undefined, idp: userObject.samlLogin.idp, + ...(customIdentifierAttributeName && { + [customIdentifierAttributeName]: userObject.attributeList.get(customIdentifierAttributeName), + }), }, }, }; - if (customIdentifierAttributeName) { - newUser.services.saml[customIdentifierAttributeName] = userObject.attributeList.get(customIdentifierAttributeName); - } - if (generateUsername === true) { username = await generateUsernameSuggestion(newUser); } @@ -162,7 +162,7 @@ export class SAML { } } - const userId = Accounts.insertUserDoc({}, newUser); + const userId = await User.create({ globalRoles: roles }, newUser); user = await Users.findOneById(userId); if (user && userObject.channels && channelsAttributeUpdate !== true) { diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index 4806b4a788c6..22d171608d33 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -2,9 +2,9 @@ import util from 'util'; import _ from 'underscore'; import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; import { Random } from '@rocket.chat/random'; import { Messages, Rooms, Users } from '@rocket.chat/models'; +import { User } from '@rocket.chat/core-services'; import { rocketLogger } from './logger'; import { callbacks } from '../../../lib/callbacks'; @@ -384,7 +384,7 @@ export default class RocketAdapter { newUser.joinDefaultChannels = false; } - rocketUserData.rocketId = await Accounts.createUserAsync(newUser); + rocketUserData.rocketId = await User.createWithPassword(newUser); const userUpdate = { utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, roles: isBot ? ['bot'] : ['user'], diff --git a/apps/meteor/definition/externals/meteor/oauth-encryption.d.ts b/apps/meteor/definition/externals/meteor/oauth-encryption.d.ts new file mode 100644 index 000000000000..26dc38d53ccd --- /dev/null +++ b/apps/meteor/definition/externals/meteor/oauth-encryption.d.ts @@ -0,0 +1,16 @@ +declare module 'meteor/oauth-encryption' { + namespace OAuthEncryption { + function _isBase64(str: string): boolean; + + function loadKey(key: string): void; + + function seal( + data: Record, + userId?: string, + ): { iv: string; ciphertext: string; algorithm: 'aes-128-gcm'; authTag: string }; + + function open(ciphertext: string, userId?: string): Record; + + function isSealed(maybeCipherText: { iv: string; ciphertext: string; algorithm: string; authTag: string }): boolean; + } +} diff --git a/apps/meteor/server/methods/registerUser.ts b/apps/meteor/server/methods/registerUser.ts index 535203f3369f..421249bf8741 100644 --- a/apps/meteor/server/methods/registerUser.ts +++ b/apps/meteor/server/methods/registerUser.ts @@ -5,6 +5,7 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; +import { User } from '@rocket.chat/core-services'; import { settings } from '../../app/settings/server'; import { validateEmailDomain, passwordPolicy, RateLimiter } from '../../app/lib/server'; @@ -33,10 +34,11 @@ Meteor.methods({ const AllowAnonymousWrite = settings.get('Accounts_AllowAnonymousWrite'); const manuallyApproveNewUsers = settings.get('Accounts_ManuallyApproveNewUsers'); if (AllowAnonymousRead === true && AllowAnonymousWrite === true && !formData.email) { - const userId = Accounts.insertUserDoc( - {}, + const userId = await User.create( { globalRoles: ['anonymous'], + }, + { active: true, }, ); @@ -102,7 +104,7 @@ Meteor.methods({ await Accounts.setPasswordAsync(importedUser._id, userData.password); userId = importedUser._id; } else { - userId = await Accounts.createUserAsync(userData); + userId = await User.createWithPassword(userData); } } catch (e) { if (e instanceof Meteor.Error) { diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 2afc502846d0..39d248575965 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -28,6 +28,7 @@ import { TranslationService } from './translation/service'; import { SettingsService } from './settings/service'; import { OmnichannelIntegrationService } from './omnichannel-integrations/service'; import { Logger } from '../lib/logger/Logger'; +import { UserService } from './user/service'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; @@ -55,6 +56,7 @@ api.registerService(new MessageService()); api.registerService(new TranslationService()); api.registerService(new SettingsService()); api.registerService(new OmnichannelIntegrationService()); +api.registerService(new UserService()); // if the process is running in micro services mode we don't need to register services that will run separately if (!isRunningMs()) { diff --git a/apps/meteor/server/services/user/service.ts b/apps/meteor/server/services/user/service.ts new file mode 100644 index 000000000000..5941acd6f121 --- /dev/null +++ b/apps/meteor/server/services/user/service.ts @@ -0,0 +1,334 @@ +import { ServiceClassInternal } from '@rocket.chat/core-services'; +import type { IUserService, UserDocOptions, CreateUserOptions } from '@rocket.chat/core-services'; +import { Match } from 'meteor/check'; +import { OAuthEncryption } from 'meteor/oauth-encryption'; +import { Accounts } from 'meteor/accounts-base'; +import { Roles, Users, Settings } from '@rocket.chat/models'; +import type { InsertionModel } from '@rocket.chat/model-typings'; +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; +import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; +import { hash as bcryptHash } from 'bcrypt'; +import { SHA256 } from '@rocket.chat/sha256'; +import { Random } from '@rocket.chat/random'; + +import { parseCSV } from '../../../lib/utils/parseCSV'; +import { callbacks } from '../../../lib/callbacks'; +import { settings } from '../../../app/settings/server'; +import { getNewUserRoles } from './lib/getNewUserRoles'; +import * as Mailer from '../../../app/mailer/server/api'; +import { AppEvents, Apps } from '../../../ee/server/apps/orchestrator'; +import { safeGetMeteorUser } from '../../../app/utils/server/functions/safeGetMeteorUser'; +import { addUserToDefaultChannels } from '../../../app/lib/server/functions/addUserToDefaultChannels'; +import { getAvatarSuggestionForUser } from '../../../app/lib/server/functions/getAvatarSuggestionForUser'; +import { setAvatarFromServiceWithValidation } from '../../../app/lib/server/functions/setUserAvatar'; +import { addUserRolesAsync } from '../../lib/roles/addUserRoles'; +import { i18n } from '../../lib/i18n'; +import { beforeCreateUserCallback } from '../../../lib/callbacks/beforeCreateUserCallback'; + +const emailTemplate = { + subject() { + const subject = i18n.t('Accounts_Admin_Email_Approval_Needed_Subject_Default'); + const siteName = settings.get('Site_Name'); + + return `[${siteName}] ${subject}`; + }, + + html(options: UserDocOptions = {}) { + const email = options.reason + ? 'Accounts_Admin_Email_Approval_Needed_With_Reason_Default' + : 'Accounts_Admin_Email_Approval_Needed_Default'; + + return Mailer.replace(i18n.t(email), { + name: escapeHTML(options.name || ''), + email: escapeHTML(options.email || ''), + reason: escapeHTML(options.reason || ''), + }); + }, +}; + +export class UserService extends ServiceClassInternal implements IUserService { + protected name = 'user'; + + public async hashPassword(password: string | { algorithm: string; digest: string }): Promise { + if (typeof password !== 'string' && password.algorithm !== 'sha-256') { + throw new Error(`Invalid password hash algorithm. Only 'sha-256' is allowed.`); + } + + const passwordString = typeof password === 'string' ? SHA256(password) : password.digest; + return bcryptHash(passwordString, Accounts._bcryptRounds()); + } + + private getNameFromProfile(profile: UserDocOptions['profile']): string | undefined { + if (!profile) { + return; + } + + if (profile.name) { + return profile.name; + } + + if ('firstName' in profile && profile.firstName) { + // LinkedIn old format + if (typeof profile.firstName === 'string') { + if (profile.lastName && typeof profile.lastName === 'string') { + return `${profile.firstName} ${profile.lastName}`; + } + + return profile.firstName; + } + + const { firstName, lastName } = profile; + + // LinkedIn new format + const { preferredLocale, localized: firstNameLocalized } = firstName; + if (preferredLocale?.language && preferredLocale?.country && firstNameLocalized) { + const locale = `${preferredLocale.language}_${preferredLocale.country}`; + if (firstNameLocalized[locale]) { + if (lastName && typeof lastName !== 'string' && lastName?.localized?.[locale]) { + return `${firstNameLocalized[locale]} ${lastName.localized[locale]}`; + } + + return firstNameLocalized[locale]; + } + } + } + } + + private async notifyAdminOfNewUser(options: UserDocOptions, user: InsertionModel): Promise { + const destinations: string[] = []; + const usersInRole = await Roles.findUsersInRole('admin'); + await usersInRole.forEach((adminUser) => { + if (Array.isArray(adminUser.emails)) { + adminUser.emails.forEach((email) => { + destinations.push(`${adminUser.name}<${email.address}>`); + }); + } + }); + + const email = { + to: destinations, + from: settings.get('From_Email'), + subject: emailTemplate.subject(), + html: emailTemplate.html({ + ...options, + name: options.name || options.profile?.name || user.name, + email: options.email || user.emails?.[0].address, + }), + }; + + await Mailer.send(email); + } + + private async insertOne(user: InsertionModel): Promise { + try { + return (await Users.insertOne(user)).insertedId; + } catch (e: any) { + if (!e.errmsg) { + throw e; + } + if (e.errmsg.includes('emails.address')) { + throw new Meteor.Error(403, 'Email already exists.'); + } + if (e.errmsg.includes('username')) { + throw new Meteor.Error(403, 'Username already exists.'); + } + + throw e; + } + } + + public async create(options: UserDocOptions, doc: Partial>): Promise { + const globalRoles = []; + + if (Match.test(options.globalRoles, [String]) && options.globalRoles.length > 0) { + globalRoles.push(...options.globalRoles); + } + + delete options.globalRoles; + + if (doc.services && !doc.services.password) { + const defaultAuthServiceRoles = parseCSV(settings.get('Accounts_Registration_AuthenticationServices_Default_Roles') || ''); + + if (defaultAuthServiceRoles.length > 0) { + globalRoles.push(...defaultAuthServiceRoles); + } + } + + const roles = getNewUserRoles(globalRoles); + const { name = this.getNameFromProfile(options.profile) } = doc; + + const user: InsertionModel = { + createdAt: new Date(), + _id: Random.id(), + status: UserStatus.OFFLINE, + + ...doc, + type: doc.type || 'user', + services: { + ...(doc.services || {}), + ...(settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In') ? { enabled: true, changedAt: new Date() } : {}), + }, + active: doc.active ?? !settings.get('Accounts_ManuallyApproveNewUsers'), + ...(!!name && { name }), + roles: [], + }; + + if (user.services) { + const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); + + for (const service of Object.values(user.services) as Record[]) { + if (!user.name && typeof service === 'object' && ('name' in service || 'username' in service)) { + user.name = service.name || service.username; + } + + if (!user.emails && service.email) { + user.emails = [ + { + address: service.email, + verified, + }, + ]; + } + + Object.keys(service).forEach((key) => { + const value = service[key]; + // If there's any value that was encrypted without an userId, decrypt it and re-encrypt with the id. + if (OAuthEncryption?.isSealed(value)) { + service[key] = OAuthEncryption.seal(OAuthEncryption.open(value), user._id); + } + }); + } + } + + await beforeCreateUserCallback.run(options, user); + + if (!user.active && settings.get('Accounts_ManuallyApproveNewUsers')) { + await this.notifyAdminOfNewUser(options, user); + } + + await callbacks.run('onCreateUser', options, user); + + // App IPostUserCreated event hook + await Apps.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: await safeGetMeteorUser() }); + + await this.validateNewUser(user); + + const _id = await this.insertOne(user); + + const newUser = await Users.findOne({ + _id, + }); + + if (!newUser) { + throw new Error('error-user-not-found'); + } + + if (newUser.username) { + await this.addUserToDefaultChannels(newUser, options); + await this.callAfterCreateUser(newUser); + await this.setDefaultAvatar(newUser); + } + + const hasAdmin = await Users.findOneByRolesAndType('admin', 'user', { projection: { _id: 1 } }); + if (!roles.includes('admin') && !hasAdmin) { + roles.push('admin'); + if (settings.get('Show_Setup_Wizard') === 'pending') { + await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + } + } + + await addUserRolesAsync(_id, roles); + return _id; + } + + private async addUserToDefaultChannels(user: IUser, options: UserDocOptions): Promise { + if (options.joinDefaultChannels === false) { + return; + } + + return addUserToDefaultChannels(user, Boolean(options.joinDefaultChannelsSilenced)); + } + + private async callAfterCreateUser(user: IUser): Promise { + if (user.type === 'visitor') { + return; + } + + setImmediate(function () { + return callbacks.run('afterCreateUser', user); + }); + } + + private async setDefaultAvatar(user: IUser): Promise { + if (settings.get('Accounts_SetDefaultAvatar') === true) { + const avatarSuggestions = await getAvatarSuggestionForUser(user); + + for await (const service of Object.keys(avatarSuggestions)) { + const avatarData = avatarSuggestions[service]; + if (service !== 'gravatar') { + return setAvatarFromServiceWithValidation(user._id, avatarData.blob, '', service); + } + } + } + } + + private async validateNewUser(user: InsertionModel): Promise { + // validateNewUser + await this.validateRegistrationDisabled(user); + await this.validateDomainAllowList(user); + } + + private async validateRegistrationDisabled(user: InsertionModel): Promise { + if (user.type === 'visitor') { + return; + } + + if ( + settings.get('Accounts_Registration_AuthenticationServices_Enabled') === false && + settings.get('LDAP_Enable') === false && + !user.services?.password + ) { + throw new Meteor.Error('registration-disabled-authentication-services', 'User registration is disabled for authentication services'); + } + } + + private async validateDomainAllowList(user: InsertionModel): Promise { + if (user.type === 'visitor') { + return; + } + + const allowedDomainList = settings.get('Accounts_AllowedDomainsList')?.trim(); + if (!allowedDomainList?.length) { + return; + } + + const domainList = allowedDomainList.split(',').map((domain) => domain.trim()); + + if (user.emails && user.emails.length > 0) { + const email = user.emails[0].address; + const inWhiteList = domainList.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); + + if (inWhiteList === false) { + throw new Meteor.Error('error-invalid-domain'); + } + } + } + + public async createWithPassword(options: CreateUserOptions): Promise { + const { username, email, password } = options; + if (!username && !email) { + throw new Meteor.Error(400, 'Need to set a username or email'); + } + + // #TODO: Check if username and email are not already in use. + + const user = { + ...(username ? { username } : {}), + ...(email ? { emails: [{ address: email, verified: false }] } : {}), + ...(password ? { services: { password: { bcrypt: await this.hashPassword(password) } } } : {}), + }; + + return this.create(options, user); + } +} diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 3352c0d3d373..89a02b268f9a 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -44,6 +44,7 @@ import type { IMessageService } from './types/IMessageService'; import type { ISettingsService } from './types/ISettingsService'; import type { IOmnichannelEEService } from './types/IOmnichannelEEService'; import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService'; +import type { IUserService, UserDocOptions, CreateUserOptions } from './types/IUserService'; export { asyncLocalStorage } from './lib/asyncLocalStorage'; export { MeteorError, isMeteorError } from './MeteorError'; @@ -118,6 +119,9 @@ export { ISettingsService, IOmnichannelEEService, IOmnichannelIntegrationService, + IUserService, + UserDocOptions, + CreateUserOptions, }; // TODO think in a way to not have to pass the service name to proxify here as well @@ -152,6 +156,7 @@ export const OmnichannelIntegration = proxifyWithWait('federation'); export const FederationEE = proxifyWithWait('federation-enterprise'); export const OmnichannelEEService = proxifyWithWait('omnichannel-ee'); +export const User = proxifyWithWait('user'); // Calls without wait. Means that the service is optional and the result may be an error // of service/method not available diff --git a/packages/core-services/src/types/IUserService.ts b/packages/core-services/src/types/IUserService.ts new file mode 100644 index 000000000000..54a68f7b9d8f --- /dev/null +++ b/packages/core-services/src/types/IUserService.ts @@ -0,0 +1,35 @@ +import type { InsertionModel } from '@rocket.chat/model-typings'; +import type { IUser } from '@rocket.chat/core-typings'; + +export type UserDocOptions = { + username?: string; + email?: string; + password?: string | { algorithm: string; digest: string }; + name?: string; + reason?: string; + + joinDefaultChannels?: boolean; + joinDefaultChannelsSilenced?: boolean; + isGuest?: boolean; + globalRoles?: string[]; + + profile?: { + name?: string; + email?: string; + } & ( + | { name?: string } + | { firstName?: string; lastName?: string } + | { + firstName?: { preferredLocale: { language: string; country: string }; localized: Record }; + lastName?: { localized: Record }; + } + ); +}; + +export type CreateUserOptions = UserDocOptions & ({ username: string } | { email: string }); + +export interface IUserService { + hashPassword(password: string | { algorithm: string; digest: string }): Promise; + create(options: UserDocOptions, doc: Partial>): Promise; + createWithPassword(options: CreateUserOptions): Promise; +} diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index e932679aab45..f26603aaa6b5 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -75,14 +75,14 @@ export interface IUserServices { enabled: boolean; changedAt: Date; }; - emailCode: IUserEmailCode[]; + emailCode?: IUserEmailCode[]; saml?: { inResponseTo?: string; provider?: string; idp?: string; idpSession?: string; nameID?: string; - }; + } & Record; ldap?: { id: string; idAttribute?: string; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index fecc7386f401..06c86c839baf 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -300,7 +300,7 @@ export interface IUsersModel extends IBaseModel { findOneByIdAndLoginToken(userId: string, loginToken: string, options?: FindOptions): Promise; findOneActiveById(userId: string, options?: FindOptions): Promise; findOneByIdOrUsername(userId: string, options?: FindOptions): Promise; - findOneByRolesAndType(roles: string[], type: string, options?: FindOptions): Promise; + findOneByRolesAndType(roles: string | string[], type: string, options?: FindOptions): Promise; findNotOfflineByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersNotOffline(options?: FindOptions): FindCursor; countUsersNotOffline(options?: FindOptions): Promise; From 02e5075c3264fe29b76a3888e6f203fb050be688 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 26 Jul 2023 10:39:22 -0300 Subject: [PATCH 2/4] Keep old names --- apps/meteor/app/authentication/server/startup/index.js | 2 +- apps/meteor/app/cas/server/cas_server.js | 2 +- apps/meteor/app/crowd/server/crowd.ts | 2 +- .../app/importer/server/classes/ImportDataConverter.ts | 4 ++-- apps/meteor/app/lib/server/functions/saveUser.js | 2 +- apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts | 2 +- apps/meteor/app/slackbridge/server/RocketAdapter.js | 2 +- apps/meteor/server/methods/registerUser.ts | 4 ++-- apps/meteor/server/services/user/service.ts | 7 ++----- packages/core-services/src/types/IUserService.ts | 4 ++-- 10 files changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 7debe1dfa447..445044c8680e 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -128,7 +128,7 @@ Accounts.emailTemplates.enrollAccount.html = function (user = {} /* , url*/) { Accounts.insertUserDoc = function (...args) { // Depends on meteor support for Async - return Promise.await(User.create(...args)); + return Promise.await(User.insertUserDoc(...args)); }; const validateLoginAttemptAsync = async function (login) { diff --git a/apps/meteor/app/cas/server/cas_server.js b/apps/meteor/app/cas/server/cas_server.js index a18736aeedfb..6560ce745e47 100644 --- a/apps/meteor/app/cas/server/cas_server.js +++ b/apps/meteor/app/cas/server/cas_server.js @@ -243,7 +243,7 @@ Accounts.registerLoginHandler('cas', async function (options) { // Create the user logger.debug(`User "${result.username}" does not exist yet, creating it`); - const userId = await User.create({ globalRoles: ['user'] }, newUser); + const userId = await User.insertUserDoc({ globalRoles: ['user'] }, newUser); // Fetch and use it user = await Users.findOneById(userId); diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index 899e0cfa450b..1332c3b3880c 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -298,7 +298,7 @@ export class CROWD { // Attempt to create the new user try { - crowdUser._id = await User.createWithPassword(crowdUser); + crowdUser._id = await User.create(crowdUser); // sync the user data await this.syncDataToUser(crowdUser, crowdUser._id); diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index d9a85e319f00..339ddfb672b3 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -290,11 +290,11 @@ export class ImportDataConverter { async insertUser(userData: IImportUser): Promise { const password = `${Date.now()}${userData.name || ''}${userData.emails.length ? userData.emails[0].toUpperCase() : ''}`; const userId = userData.emails.length - ? await User.createWithPassword({ + ? await User.create({ email: userData.emails[0], password, }) - : await User.createWithPassword({ + : await User.create({ username: userData.username, password, joinDefaultChannelsSilenced: true, diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 194569e1df37..2338029a646e 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -285,7 +285,7 @@ const saveNewUser = async function (userData, sendPassword) { createUser.email = userData.email; } - const _id = await User.createWithPassword(createUser); + const _id = await User.create(createUser); const updateUser = { $set: { diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts index d7fa043cf06d..6a17181cc17b 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts @@ -162,7 +162,7 @@ export class SAML { } } - const userId = await User.create({ globalRoles: roles }, newUser); + const userId = await User.insertUserDoc({ globalRoles: roles }, newUser); user = await Users.findOneById(userId); if (user && userObject.channels && channelsAttributeUpdate !== true) { diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index 22d171608d33..2edc50b8f563 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -384,7 +384,7 @@ export default class RocketAdapter { newUser.joinDefaultChannels = false; } - rocketUserData.rocketId = await User.createWithPassword(newUser); + rocketUserData.rocketId = await User.create(newUser); const userUpdate = { utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, roles: isBot ? ['bot'] : ['user'], diff --git a/apps/meteor/server/methods/registerUser.ts b/apps/meteor/server/methods/registerUser.ts index 421249bf8741..73123b63437f 100644 --- a/apps/meteor/server/methods/registerUser.ts +++ b/apps/meteor/server/methods/registerUser.ts @@ -34,7 +34,7 @@ Meteor.methods({ const AllowAnonymousWrite = settings.get('Accounts_AllowAnonymousWrite'); const manuallyApproveNewUsers = settings.get('Accounts_ManuallyApproveNewUsers'); if (AllowAnonymousRead === true && AllowAnonymousWrite === true && !formData.email) { - const userId = await User.create( + const userId = await User.insertUserDoc( { globalRoles: ['anonymous'], }, @@ -104,7 +104,7 @@ Meteor.methods({ await Accounts.setPasswordAsync(importedUser._id, userData.password); userId = importedUser._id; } else { - userId = await User.createWithPassword(userData); + userId = await User.create(userData); } } catch (e) { if (e instanceof Meteor.Error) { diff --git a/apps/meteor/server/services/user/service.ts b/apps/meteor/server/services/user/service.ts index 5941acd6f121..974654a9d0a7 100644 --- a/apps/meteor/server/services/user/service.ts +++ b/apps/meteor/server/services/user/service.ts @@ -138,7 +138,7 @@ export class UserService extends ServiceClassInternal implements IUserService { } } - public async create(options: UserDocOptions, doc: Partial>): Promise { + public async insertUserDoc(options: UserDocOptions, doc: Partial>): Promise { const globalRoles = []; if (Match.test(options.globalRoles, [String]) && options.globalRoles.length > 0) { @@ -274,7 +274,6 @@ export class UserService extends ServiceClassInternal implements IUserService { } private async validateNewUser(user: InsertionModel): Promise { - // validateNewUser await this.validateRegistrationDisabled(user); await this.validateDomainAllowList(user); } @@ -315,14 +314,12 @@ export class UserService extends ServiceClassInternal implements IUserService { } } - public async createWithPassword(options: CreateUserOptions): Promise { + public async create(options: CreateUserOptions): Promise { const { username, email, password } = options; if (!username && !email) { throw new Meteor.Error(400, 'Need to set a username or email'); } - // #TODO: Check if username and email are not already in use. - const user = { ...(username ? { username } : {}), ...(email ? { emails: [{ address: email, verified: false }] } : {}), diff --git a/packages/core-services/src/types/IUserService.ts b/packages/core-services/src/types/IUserService.ts index 54a68f7b9d8f..cc850aa387bd 100644 --- a/packages/core-services/src/types/IUserService.ts +++ b/packages/core-services/src/types/IUserService.ts @@ -30,6 +30,6 @@ export type CreateUserOptions = UserDocOptions & ({ username: string } | { email export interface IUserService { hashPassword(password: string | { algorithm: string; digest: string }): Promise; - create(options: UserDocOptions, doc: Partial>): Promise; - createWithPassword(options: CreateUserOptions): Promise; + insertUserDoc(options: UserDocOptions, doc: Partial>): Promise; + create(options: CreateUserOptions): Promise; } From e4133bc680371a324ad2ef66aa802ba1cd85688a Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 26 Jul 2023 10:42:48 -0300 Subject: [PATCH 3/4] oops --- apps/meteor/server/services/user/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/services/user/service.ts b/apps/meteor/server/services/user/service.ts index 974654a9d0a7..f3617acf5dc7 100644 --- a/apps/meteor/server/services/user/service.ts +++ b/apps/meteor/server/services/user/service.ts @@ -326,6 +326,6 @@ export class UserService extends ServiceClassInternal implements IUserService { ...(password ? { services: { password: { bcrypt: await this.hashPassword(password) } } } : {}), }; - return this.create(options, user); + return this.insertUserDoc(options, user); } } From 11121b448d75db961150bd981d7334ff1aa5fa8b Mon Sep 17 00:00:00 2001 From: Pierre Lehnen Date: Wed, 26 Jul 2023 14:14:48 -0300 Subject: [PATCH 4/4] save user roles before running callbacks --- apps/meteor/server/services/user/service.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/meteor/server/services/user/service.ts b/apps/meteor/server/services/user/service.ts index f3617acf5dc7..f93c7ce31b04 100644 --- a/apps/meteor/server/services/user/service.ts +++ b/apps/meteor/server/services/user/service.ts @@ -216,6 +216,13 @@ export class UserService extends ServiceClassInternal implements IUserService { const _id = await this.insertOne(user); + const hasAdmin = await Users.findOneByRolesAndType('admin', 'user', { projection: { _id: 1 } }); + if (!roles.includes('admin') && !hasAdmin) { + roles.push('admin'); + } + + await addUserRolesAsync(_id, roles); + const newUser = await Users.findOne({ _id, }); @@ -224,21 +231,16 @@ export class UserService extends ServiceClassInternal implements IUserService { throw new Error('error-user-not-found'); } + if (!hasAdmin && roles.includes('admin') && settings.get('Show_Setup_Wizard') === 'pending') { + await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + } + if (newUser.username) { await this.addUserToDefaultChannels(newUser, options); await this.callAfterCreateUser(newUser); await this.setDefaultAvatar(newUser); } - const hasAdmin = await Users.findOneByRolesAndType('admin', 'user', { projection: { _id: 1 } }); - if (!roles.includes('admin') && !hasAdmin) { - roles.push('admin'); - if (settings.get('Show_Setup_Wizard') === 'pending') { - await Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); - } - } - - await addUserRolesAsync(_id, roles); return _id; }