Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,24 @@ import type { Filter } from 'mongodb';
import { i18n } from '../../../../server/lib/i18n';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { sendWelcomeEmail } from '../../../../server/lib/sendWelcomeEmail';
import { executeDeleteUser } from '../../../../server/methods/deleteUser';
import { registerUser } from '../../../../server/methods/registerUser';
import { requestDataDownload } from '../../../../server/methods/requestDataDownload';
import { resetAvatar } from '../../../../server/methods/resetAvatar';
import { saveUserPreferences } from '../../../../server/methods/saveUserPreferences';
import { sendConfirmationEmail } from '../../../../server/methods/sendConfirmationEmail';
import { sendForgotPasswordEmail } from '../../../../server/methods/sendForgotPasswordEmail';
import { executeSetUserActiveStatus } from '../../../../server/methods/setUserActiveStatus';
import { getUserForCheck, emailCheck } from '../../../2fa/server/code';
import { resetTOTP } from '../../../2fa/server/functions/resetTOTP';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import {
checkUsernameAvailability,
checkUsernameAvailabilityWithValidation,
} from '../../../lib/server/functions/checkUsernameAvailability';
import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser';
import { getFullUserDataByIdOrUsernameOrImportId } from '../../../lib/server/functions/getFullUserData';
import { generateUsernameSuggestion } from '../../../lib/server/functions/getUsernameSuggestion';
import { saveCustomFields } from '../../../lib/server/functions/saveCustomFields';
import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions/saveCustomFieldsWithoutValidation';
import { saveUser } from '../../../lib/server/functions/saveUser';
Expand All @@ -49,6 +57,7 @@ import { validateNameChars } from '../../../lib/server/functions/validateNameCha
import { validateUsername } from '../../../lib/server/functions/validateUsername';
import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../../lib/server/lib/notifyListener';
import { generateAccessToken } from '../../../lib/server/methods/createToken';
import { deleteUserOwnAccount } from '../../../lib/server/methods/deleteUserOwnAccount';
import { settings } from '../../../settings/server';
import { getURL } from '../../../utils/server/getURL';
import { API } from '../api';
Expand Down Expand Up @@ -84,7 +93,11 @@ API.v1.addRoute(
},
{
async get() {
const suggestions = await Meteor.callAsync('getAvatarSuggestion');
const user = await Users.findOneById(this.userId);
if (!user) {
return API.v1.failure('User not found');
}
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated
const suggestions = await getAvatarSuggestionForUser(user);

return API.v1.success({ suggestions });
},
Expand Down Expand Up @@ -115,7 +128,7 @@ API.v1.addRoute(
confirmRelinquish,
} = this.bodyParams;

await Meteor.callAsync('setUserActiveStatus', userId, active, Boolean(confirmRelinquish));
await executeSetUserActiveStatus(this.userId, userId, active, Boolean(confirmRelinquish));
}
const { fields } = await this.parseJsonQuery();

Expand Down Expand Up @@ -308,7 +321,7 @@ API.v1.addRoute(
}

if (typeof this.bodyParams.active !== 'undefined') {
await Meteor.callAsync('setUserActiveStatus', userId, this.bodyParams.active);
await executeSetUserActiveStatus(this.userId, userId, this.bodyParams.active);
}

const { fields } = await this.parseJsonQuery();
Expand All @@ -331,7 +344,7 @@ API.v1.addRoute(
const user = await getUserFromParams(this.bodyParams);
const { confirmRelinquish = false } = this.bodyParams;

await Meteor.callAsync('deleteUser', user._id, confirmRelinquish);
await executeDeleteUser(this.userId, user._id, confirmRelinquish);
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated

return API.v1.success();
},
Expand All @@ -353,7 +366,7 @@ API.v1.addRoute(

const { confirmRelinquish = false } = this.bodyParams;

await Meteor.callAsync('deleteUserOwnAccount', password, confirmRelinquish);
await deleteUserOwnAccount(this.userId, password, confirmRelinquish);

return API.v1.success();
},
Expand All @@ -372,7 +385,7 @@ API.v1.addRoute(
{
async post() {
const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams;
await Meteor.callAsync('setUserActiveStatus', userId, activeStatus, confirmRelinquish);
await executeSetUserActiveStatus(this.userId, userId, activeStatus, confirmRelinquish);

const user = await Users.findOneById(this.bodyParams.userId, { projection: { active: 1 } });
if (!user) {
Expand Down Expand Up @@ -656,11 +669,15 @@ API.v1.addRoute(
}

// Register the user
const userId = await Meteor.callAsync('registerUser', {
const userId = await registerUser({
...params,
...(secretURL && { secretURL }),
});

if (typeof userId !== 'string') {
return API.v1.failure('Error creating user');
}

// Now set their username
const { fields } = await this.parseJsonQuery();
await setUsernameWithValidation(userId, this.bodyParams.username);
Expand All @@ -687,12 +704,12 @@ API.v1.addRoute(
const user = await getUserFromParams(this.bodyParams);

if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) {
await Meteor.callAsync('resetAvatar');
await resetAvatar(this.userId, this.userId);
} else if (
(await hasPermissionAsync(this.userId, 'edit-other-user-avatar')) ||
(await hasPermissionAsync(this.userId, 'manage-moderation-actions'))
) {
await Meteor.callAsync('resetAvatar', user._id);
await resetAvatar(this.userId, user._id);
} else {
throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', {
method: 'users.resetAvatar',
Expand Down Expand Up @@ -753,7 +770,7 @@ API.v1.addRoute(
return API.v1.failure("The 'email' param is required");
}

await Meteor.callAsync('sendForgotPasswordEmail', email.toLowerCase());
await sendForgotPasswordEmail(email.toLowerCase());
return API.v1.success();
},
},
Expand All @@ -764,7 +781,11 @@ API.v1.addRoute(
{ authRequired: true },
{
async get() {
const result = await Meteor.callAsync('getUsernameSuggestion');
const user = await Users.findOneById(this.userId, { projection: { name: 1, emails: 1, services: 1 } });
if (!user) {
return API.v1.failure('User not found');
}
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated
const result = await generateUsernameSuggestion(user);

return API.v1.success({ result });
},
Expand Down Expand Up @@ -1021,7 +1042,11 @@ API.v1.addRoute(
{
async get() {
const { fullExport = false } = this.queryParams;
const result = (await Meteor.callAsync('requestDataDownload', { fullExport: fullExport === 'true' })) as {
const user = await Users.findOneById(this.userId);
if (!user) {
return API.v1.failure('User not found');
}
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated
const result = (await requestDataDownload({ userData: user, fullExport: fullExport === 'true' })) as {
requested: boolean;
exportOperation: IExportOperation;
};
Expand Down
82 changes: 43 additions & 39 deletions apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,61 @@ declare module '@rocket.chat/ddp-client' {
}
}

Meteor.methods<ServerMethods>({
async deleteUserOwnAccount(password, confirmRelinquish) {
check(password, String);
export const deleteUserOwnAccount = async (fromUserId: string, password: string, confirmRelinquish = false): Promise<boolean> => {
check(password, String);
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated

if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'deleteUserOwnAccount',
});
}
if (!settings.get('Accounts_AllowDeleteOwnAccount')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'deleteUserOwnAccount',
});
}

if (!settings.get('Accounts_AllowDeleteOwnAccount')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
if (!fromUserId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'deleteUserOwnAccount',
});
}

const user = await Users.findOneById(fromUserId);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'deleteUserOwnAccount',
});
}

if (user.services?.password && trim(user.services.password.bcrypt)) {
const result = await Accounts._checkPasswordAsync(user as Meteor.User, {
digest: password.toLowerCase(),
algorithm: 'sha-256',
});
if (result.error) {
throw new Meteor.Error('error-invalid-password', 'Invalid password', {
method: 'deleteUserOwnAccount',
});
}
} else if (!user.username || SHA256(user.username) !== password.trim()) {
throw new Meteor.Error('error-invalid-username', 'Invalid username', {
method: 'deleteUserOwnAccount',
});
}

await deleteUser(fromUserId, confirmRelinquish);

// App IPostUserDeleted event hook
await Apps.self?.triggerEvent(AppEvents.IPostUserDeleted, { user });

return true;
};

Meteor.methods<ServerMethods>({
async deleteUserOwnAccount(password, confirmRelinquish) {
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'deleteUserOwnAccount',
});
}

const user = await Users.findOneById(uid);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'deleteUserOwnAccount',
});
}

if (user.services?.password && trim(user.services.password.bcrypt)) {
const result = await Accounts._checkPasswordAsync(user as Meteor.User, {
digest: password.toLowerCase(),
algorithm: 'sha-256',
});
if (result.error) {
throw new Meteor.Error('error-invalid-password', 'Invalid password', {
method: 'deleteUserOwnAccount',
});
}
} else if (!user.username || SHA256(user.username) !== password.trim()) {
throw new Meteor.Error('error-invalid-username', 'Invalid username', {
method: 'deleteUserOwnAccount',
});
}

await deleteUser(uid, confirmRelinquish);

// App IPostUserDeleted event hook
await Apps.self?.triggerEvent(AppEvents.IPostUserDeleted, { user });

return true;
return deleteUserOwnAccount(uid, password, confirmRelinquish);
},
});
72 changes: 41 additions & 31 deletions apps/meteor/server/methods/deleteUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,45 +15,55 @@ declare module '@rocket.chat/ddp-client' {
}
}

Meteor.methods<ServerMethods>({
async deleteUser(userId, confirmRelinquish = false) {
check(userId, String);
const uid = Meteor.userId();
if (!uid || (await hasPermissionAsync(uid, 'delete-user')) !== true) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'deleteUser',
});
}
export const executeDeleteUser = async (fromUserId: IUser['_id'], userId: IUser['_id'], confirmRelinquish = false): Promise<boolean> => {
check(userId, String);
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated
if ((await hasPermissionAsync(fromUserId, 'delete-user')) !== true) {
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'deleteUser',
});
}

const user = await Users.findOneById(userId);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user to delete', {
method: 'deleteUser',
});
}
const user = await Users.findOneById(userId);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user to delete', {
method: 'deleteUser',
});
}

if (user.type === 'app') {
throw new Meteor.Error('error-cannot-delete-app-user', 'Deleting app user is not allowed', {
method: 'deleteUser',
});
}
if (user.type === 'app') {
throw new Meteor.Error('error-cannot-delete-app-user', 'Deleting app user is not allowed', {
method: 'deleteUser',
});
}

const adminCount = await Users.col.countDocuments({ roles: 'admin' });

const userIsAdmin = user.roles?.indexOf('admin') > -1;

if (adminCount === 1 && userIsAdmin) {
throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without admins is not allowed', {
method: 'deleteUser',
action: 'Remove_last_admin',
});
}

await deleteUser(userId, confirmRelinquish, fromUserId);

const adminCount = await Users.col.countDocuments({ roles: 'admin' });
// App IPostUserDeleted event hook
await Apps.self?.triggerEvent(AppEvents.IPostUserDeleted, { user, performedBy: await Users.findOneById(fromUserId) });
Comment thread
MarcosSpessatto marked this conversation as resolved.
Outdated

const userIsAdmin = user.roles?.indexOf('admin') > -1;
return true;
};

if (adminCount === 1 && userIsAdmin) {
throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without admins is not allowed', {
Meteor.methods<ServerMethods>({
async deleteUser(userId, confirmRelinquish = false) {
const uid = Meteor.userId();
if (!uid) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'deleteUser',
action: 'Remove_last_admin',
});
}

await deleteUser(userId, confirmRelinquish, uid);

// App IPostUserDeleted event hook
await Apps.self?.triggerEvent(AppEvents.IPostUserDeleted, { user, performedBy: await Meteor.userAsync() });

return true;
return executeDeleteUser(uid, userId, confirmRelinquish);
},
});
Loading