Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/rest-batch4-logout-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': minor
---

Moved the post-logout cleanup hook (`afterLogoutCleanUpCallback` + `Apps.IPostUserLoggedOut`) into a server-side `Accounts.onLogout` handler and into `POST /v1/users.logout`. Both DDP and REST logout paths now fire those callbacks server-side; the client no longer needs to invoke `logoutCleanUp` after detecting a logout, and the deprecated DDP method keeps its registration with a deprecation log pointing at `/v1/users.logout`.
6 changes: 6 additions & 0 deletions .changeset/rest-cloud-connect-workspace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Added `POST /v1/cloud.connectWorkspace` (replaces the deprecated `cloud:connectWorkspace` DDP method). Body is `{ token }`; auth-gated with `manage-cloud` permission. The legacy DDP method remains registered with a deprecation log pointing at the new route.
6 changes: 6 additions & 0 deletions .changeset/rest-integrations-history-clear-replay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Added `POST /v1/integrations.history.clear` and `POST /v1/integrations.outgoing.replay` (replace the deprecated `clearIntegrationHistory` and `replayOutgoingIntegration` DDP methods). Bodies `{ integrationId }` and `{ integrationId, historyId }` respectively. Permissions (`manage-outgoing-integrations` or `manage-own-outgoing-integrations`) are enforced the same way the DDP methods did. Legacy DDP methods remain registered with deprecation logs pointing at the new routes.
6 changes: 6 additions & 0 deletions .changeset/rest-permissions-add-remove-role.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Added `POST /v1/permissions.addRole` and `POST /v1/permissions.removeRole` (replace the deprecated `authorization:addPermissionToRole` and `authorization:removeRoleFromPermission` DDP methods). Body is `{ permissionId, role }` on both. The same per-user permission checks (`access-permissions`, `access-setting-permissions`) the DDP methods enforced are reused. Legacy DDP methods remain registered with deprecation logs pointing at the new routes.
5 changes: 5 additions & 0 deletions .changeset/rest-users-setavatar-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': minor
---

Extended `POST /v1/users.setAvatar` to accept an optional `service` multipart field. When provided, the value is stored as the user's `avatarOrigin`, matching what the deprecated `setAvatarFromService` DDP method did. The legacy DDP method remains registered with a deprecation log pointing at the new route.
6 changes: 6 additions & 0 deletions .changeset/rest-users-verify-email.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Added `POST /v1/users.verifyEmail` (replaces the two-call DDP flow of `verifyEmail` + `afterVerifyEmail`). Body is `{ token }`; the server resolves the user, marks the email verified, and runs the anonymous→user role swap in a single request. The deprecated `afterVerifyEmail` DDP method keeps its registration with a deprecation log pointing at the new route.
21 changes: 21 additions & 0 deletions apps/meteor/app/api/server/v1/cloud.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { CloudRegistrationIntentData, CloudConfirmationPollData, CloudRegistrationStatus } from '@rocket.chat/core-typings';
import {
isCloudConfirmationPollProps,
isCloudConnectWorkspaceProps,
isCloudCreateRegistrationIntentProps,
isCloudManualRegisterProps,
ajv,
Expand All @@ -11,6 +12,7 @@ import {

import { CloudWorkspaceRegistrationError } from '../../../../lib/errors/CloudWorkspaceRegistrationError';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { connectWorkspace } from '../../../cloud/server/functions/connectWorkspace';
import { getCheckoutUrl } from '../../../cloud/server/functions/getCheckoutUrl';
import { getConfirmationPoll } from '../../../cloud/server/functions/getConfirmationPoll';
import {
Expand Down Expand Up @@ -287,6 +289,25 @@ declare module '@rocket.chat/rest-typings' {
}
}

API.v1.post(
'cloud.connectWorkspace',
{
authRequired: true,
permissionsRequired: ['manage-cloud'],
body: isCloudConnectWorkspaceProps,
response: {
200: successResponseSchema,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
await connectWorkspace(this.bodyParams.token);
return API.v1.success();
Comment on lines +306 to +307

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new cloud.connectWorkspace route ignores the boolean result of connectWorkspace and always returns success, so failed workspace connections are incorrectly reported as successful.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/cloud.ts, line 306:

<comment>The new `cloud.connectWorkspace` route ignores the boolean result of `connectWorkspace` and always returns success, so failed workspace connections are incorrectly reported as successful.</comment>

<file context>
@@ -287,6 +289,25 @@ declare module '@rocket.chat/rest-typings' {
+		},
+	},
+	async function action() {
+		await connectWorkspace(this.bodyParams.token);
+		return API.v1.success();
+	},
</file context>
Suggested change
await connectWorkspace(this.bodyParams.token);
return API.v1.success();
const connected = await connectWorkspace(this.bodyParams.token);
if (!connected) {
return API.v1.failure('Failed to connect workspace');
}
return API.v1.success();

},
);

API.v1.get(
'cloud.checkoutUrl',
{
Expand Down
50 changes: 50 additions & 0 deletions apps/meteor/app/api/server/v1/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
isIntegrationsGetProps,
isIntegrationsUpdateProps,
isIntegrationsListProps,
isIntegrationsClearHistoryProps,
isIntegrationsReplayProps,
validateBadRequestErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
Expand All @@ -16,6 +18,10 @@ import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
import type { Filter } from 'mongodb';

import {
clearIntegrationHistoryMethod,
replayOutgoingIntegrationMethod,
} from '../../../integrations/server/functions/clearIntegrationHistory';
import {
mountIntegrationHistoryQueryBasedOnPermissions,
mountIntegrationQueryBasedOnPermissions,
Expand Down Expand Up @@ -363,3 +369,47 @@ API.v1.put(
}
},
);

const voidIntegrationsResponse = ajv.compile<void>({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
});

API.v1.post(
'integrations.history.clear',
{
authRequired: true,
body: isIntegrationsClearHistoryProps,
response: {
200: voidIntegrationsResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
await clearIntegrationHistoryMethod(this.userId, this.bodyParams.integrationId);
return API.v1.success();
},
);

API.v1.post(
'integrations.outgoing.replay',
{
authRequired: true,
body: isIntegrationsReplayProps,
response: {
200: voidIntegrationsResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { integrationId, historyId } = this.bodyParams;
await replayOutgoingIntegrationMethod(this.userId, { integrationId, historyId });
return API.v1.success();
},
);
56 changes: 56 additions & 0 deletions apps/meteor/app/api/server/v1/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';

import { addPermissionToRoleMethod, removeRoleFromPermissionMethod } from '../../../authorization/server/functions/permissionRole';
import { permissionsGetMethod } from '../../../authorization/server/streamer/permissions';
import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener';
import type { ExtractRoutesFromAPI } from '../ApiClass';
Expand Down Expand Up @@ -62,6 +63,25 @@ const isPermissionsListAll = ajvQuery.compile<PermissionsListAllProps>(permissio

const isBodyParamsValidPermissionUpdate = ajv.compile<PermissionsUpdateProps>(permissionUpdatePropsSchema);

type PermissionRolePayload = { permissionId: string; role: string };

const isPermissionRolePayload = ajv.compile<PermissionRolePayload>({
type: 'object',
properties: {
permissionId: { type: 'string', minLength: 1 },
role: { type: 'string', minLength: 1 },
},
required: ['permissionId', 'role'],
additionalProperties: false,
});

const voidPermissionResponse = ajv.compile<void>({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
});

const permissionsEndpoints = API.v1
.get(
'permissions.listAll',
Expand Down Expand Up @@ -185,6 +205,42 @@ const permissionsEndpoints = API.v1
permissions: result,
});
},
)
.post(
'permissions.addRole',
{
authRequired: true,
body: isPermissionRolePayload,
Comment on lines +210 to +213

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Add a route-level permissionsRequired gate for permissions.addRole. Without it, authorization failures are handled inside the helper and fall back to 400 instead of a consistent early 403.

(Based on your team's feedback about permission checks at public API entry points.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/permissions.ts, line 210:

<comment>Add a route-level `permissionsRequired` gate for `permissions.addRole`. Without it, authorization failures are handled inside the helper and fall back to 400 instead of a consistent early 403.

(Based on your team's feedback about permission checks at public API entry points.) </comment>

<file context>
@@ -185,6 +205,42 @@ const permissionsEndpoints = API.v1
 		},
+	)
+	.post(
+		'permissions.addRole',
+		{
+			authRequired: true,
</file context>
Suggested change
'permissions.addRole',
{
authRequired: true,
body: isPermissionRolePayload,
'permissions.addRole',
{
authRequired: true,
permissionsRequired: ['access-permissions'],
body: isPermissionRolePayload,

response: {
200: voidPermissionResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { permissionId, role } = this.bodyParams;
await addPermissionToRoleMethod(this.userId, permissionId, role);
return API.v1.success();
},
)
.post(
'permissions.removeRole',
{
authRequired: true,
body: isPermissionRolePayload,
Comment on lines +228 to +231

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Add a route-level permissionsRequired gate for permissions.removeRole. This keeps permission failures on the middleware path (403) instead of relying on helper-thrown errors that are returned as 400.

(Based on your team's feedback about permission checks at public API entry points.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/permissions.ts, line 228:

<comment>Add a route-level `permissionsRequired` gate for `permissions.removeRole`. This keeps permission failures on the middleware path (403) instead of relying on helper-thrown errors that are returned as 400.

(Based on your team's feedback about permission checks at public API entry points.) </comment>

<file context>
@@ -185,6 +205,42 @@ const permissionsEndpoints = API.v1
+		},
+	)
+	.post(
+		'permissions.removeRole',
+		{
+			authRequired: true,
</file context>
Suggested change
'permissions.removeRole',
{
authRequired: true,
body: isPermissionRolePayload,
'permissions.removeRole',
{
authRequired: true,
permissionsRequired: ['access-permissions'],
body: isPermissionRolePayload,

response: {
200: voidPermissionResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
const { permissionId, role } = this.bodyParams;
await removeRoleFromPermissionMethod(this.userId, permissionId, role);
return API.v1.success();
},
);

export type PermissionsEndpoints = ExtractRoutesFromAPI<typeof permissionsEndpoints>;
Expand Down
44 changes: 43 additions & 1 deletion apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,12 @@ import type { Filter } from 'mongodb';
import { generatePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/generateToken';
import { regeneratePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/regenerateToken';
import { removePersonalAccessTokenOfUser } from '../../../../imports/personal-access-tokens/server/api/methods/removeToken';
import { runUserLogoutCleanUp } from '../../../../server/hooks/userLogoutCleanUp';
import { UserChangedAuditStore } from '../../../../server/lib/auditServerEvents/userChanged';
import { i18n } from '../../../../server/lib/i18n';
import { SystemLogger } from '../../../../server/lib/logger/system';
import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey';
import { runAfterVerifyEmail } from '../../../../server/lib/users/runAfterVerifyEmail';
import { registerUser } from '../../../../server/methods/registerUser';
import { requestDataDownload } from '../../../../server/methods/requestDataDownload';
import { resetAvatar } from '../../../../server/methods/resetAvatar';
Expand Down Expand Up @@ -343,7 +345,9 @@ API.v1
}
}

await setUserAvatar(user, fileBuffer, mimetype, 'rest');
const service = typeof fields.service === 'string' && fields.service.length > 0 ? fields.service : 'rest';

await setUserAvatar(user, fileBuffer, mimetype, service as 'rest');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unsafe type assertion as 'rest' on a value that may not be 'rest'. If fields.service provides a value like an OAuth provider name (e.g., "github"), the cast silences the type error without changing runtime behavior: setUserAvatar will receive the non-'rest' value, bypass the service === 'rest' branch, and fall into RocketChatFile.dataURIParse(dataURI) with a Buffer argument — which expects a string and will fail at runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/users.ts, line 350:

<comment>Unsafe type assertion `as 'rest'` on a value that may not be `'rest'`. If `fields.service` provides a value like an OAuth provider name (e.g., `"github"`), the cast silences the type error without changing runtime behavior: `setUserAvatar` will receive the non-`'rest'` value, bypass the `service === 'rest'` branch, and fall into `RocketChatFile.dataURIParse(dataURI)` with a `Buffer` argument — which expects a string and will fail at runtime.</comment>

<file context>
@@ -347,7 +347,7 @@ API.v1
 			const service = typeof fields.service === 'string' && fields.service.length > 0 ? fields.service : 'rest';
 
-			await setUserAvatar(user, fileBuffer, mimetype, service);
+			await setUserAvatar(user, fileBuffer, mimetype, service as 'rest');
 
 			return API.v1.success();
</file context>
Suggested change
await setUserAvatar(user, fileBuffer, mimetype, service as 'rest');
if (service !== 'rest') {
return API.v1.failure("The 'service' param must be 'rest' when uploading a file");
}
await setUserAvatar(user, fileBuffer, mimetype, service);


return API.v1.success();
},
Expand Down Expand Up @@ -1877,6 +1881,8 @@ API.v1
return API.v1.forbidden();
}

const user = await Users.findOneById(userId);

// this method logs the user out automatically, if successful returns 1, otherwise 0
if (!(await Users.unsetLoginTokens(userId))) {
throw new Meteor.Error('error-invalid-user-id', 'Invalid user id');
Expand All @@ -1886,6 +1892,10 @@ API.v1

void notifyOnUserChange({ clientAction: 'updated', id: userId, diff: { 'services.resume.loginTokens': [] } });

if (user) {
await runUserLogoutCleanUp(user);
}

return API.v1.success({
message: `User ${userId} has been logged out!`,
});
Expand Down Expand Up @@ -2080,6 +2090,38 @@ API.v1
},
);

API.v1.post(
'users.verifyEmail',
{
authRequired: false,
body: ajv.compile<{ token: string }>({
type: 'object',
properties: {
token: { type: 'string', minLength: 1 },
},
required: ['token'],
additionalProperties: false,
}),
response: {
200: voidSuccessResponse,
400: validateBadRequestErrorResponse,
},
},
async function action() {
const { token } = this.bodyParams;

const user = await Users.findOne<Pick<IUser, '_id'>>({ 'services.email.verificationTokens.token': token }, { projection: { _id: 1 } });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The user lookup should occur after verifyEmail succeeds. If findOne returns null due to a race or replication lag but verifyEmail still succeeds (it removes the token from the document), runAfterVerifyEmail will be silently skipped—leaving the anonymous→user role swap incomplete. Move the lookup after the callAsync or re-fetch the user post-verification.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/users.ts, line 2121:

<comment>The user lookup should occur after `verifyEmail` succeeds. If `findOne` returns `null` due to a race or replication lag but `verifyEmail` still succeeds (it removes the token from the document), `runAfterVerifyEmail` will be silently skipped—leaving the anonymous→user role swap incomplete. Move the lookup after the `callAsync` or re-fetch the user post-verification.</comment>

<file context>
@@ -2088,6 +2098,38 @@ API.v1
+	async function action() {
+		const { token } = this.bodyParams;
+
+		const user = await Users.findOne<Pick<IUser, '_id'>>({ 'services.email.verificationTokens.token': token }, { projection: { _id: 1 } });
+
+		await Meteor.callAsync('verifyEmail', token);
</file context>


await Meteor.callAsync('verifyEmail', token);

if (user) {
await runAfterVerifyEmail(user._id);
}

return API.v1.success();
},
Comment on lines +2110 to +2122

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 High: Missing authorization check in users.update API allows account takeover and privilege escalation

The users.update API endpoint does not enforce sufficient authorization checks before calling saveUser to update user information. While it requires 2FA, it lacks explicit permission requirements (e.g., edit-other-user-info), potentially allowing authenticated users to modify the profiles of other users.

Trace
graph TD
    subgraph SG0 ["apps/meteor/app/2fa/server/code/index.ts"]
        getUserForCheck["Retrieves user data required for two-factor authentication checks."]
    end
    style SG0 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG1 ["apps/meteor/app/2fa/server/functions/resetTOTP.ts"]
        sendResetNotification_2["sendResetNotification"]
        resetTOTP["Resets a user's TOTP configuration, optionally notifying the user via email."]
    end
    style SG1 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG2 ["apps/meteor/app/api/server/ApiClass.ts"]
        APIClass.success["Returns a successful API response."]
        APIClass.failure["Returns a failure API response."]
        APIClass.forbidden["Returns a 403 Forbidden API response."]
        APIClass.this.parseJsonQuery["Parses JSON query parameters for an API context."]
    end
    style SG2 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG3 ["apps/meteor/app/api/server/helpers/getPaginationItems.ts"]
        getPaginationItems["Calculates pagination offset and count based on request parameters and system settings."]
    end
    style SG3 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG4 ["apps/meteor/app/api/server/helpers/getUserFromParams.ts"]
        getUserFromParams["Retrieves a user object based on userId or username parameters from an API request."]
    end
    style SG4 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG5 ["apps/meteor/app/api/server/helpers/getUserInfo.ts"]
        isVerifiedEmail["isVerifiedEmail"]
        getUserPreferences["getUserPreferences"]
        filterOutdatedVersionUpdateBanners["filterOutdatedVersionUpdateBanners"]
        getUserCalendar["getUserCalendar"]
        getUserInfo["Constructs and returns a comprehensive user information object for API responses, including preferences and calendar settings."]
    end
    style SG5 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG6 ["apps/meteor/app/api/server/helpers/isUserFromParams.ts"]
        isUserFromParams["Validates if the provided parameters identify the currently logged-in user."]
    end
    style SG6 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG7 ["apps/meteor/app/api/server/lib/eraseTeam.ts"]
        eraseTeamOnRelinquishRoomOwnerships["eraseTeamOnRelinquishRoomOwnerships"]
        eraseRoomLooseValidation["eraseRoomLooseValidation"]
    end
    style SG7 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG8 ["apps/meteor/app/api/server/lib/getUploadFormData.ts"]
        getUploadFormData["Parses multipart/form-data from an HTTP request to extract file data and fields."]
    end
    style SG8 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG9 ["apps/meteor/app/api/server/lib/isValidQuery.ts"]
        ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts["Top-level logic providing a utility to validate query objects against allowed attributes and operations."]
    end
    style SG9 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG10 ["apps/meteor/app/api/server/lib/users.ts"]
        findUsersToAutocomplete["Provides autocomplete suggestions for users based on search terms and access permissions."]
        getInclusiveFields["Filters a query object to retain only inclusive fields."]
        getNonEmptyFields["Returns default fields if the provided fields object is empty or invalid."]
        getNonEmptyQuery["Returns a default query if the provided query object is empty, optionally including email searches."]
        findPaginatedUsersByStatus["Retrieves a paginated list of users filtered by status, roles, and other criteria."]
    end
    style SG10 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG11 ["apps/meteor/app/api/server/v1/users.ts"]
        get["get"]
        action{{"action"}}
    end
    style SG11 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG12 ["apps/meteor/app/authentication/server/startup/index.js"]
        Accounts.insertUserDoc["Accounts.insertUserDoc"]
    end
    style SG12 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG13 ["apps/meteor/app/authorization/server/functions/hasPermission.ts"]
        hasPermissionAsync["Checks if a user has a specific permission, optionally within a room scope."]
    end
    style SG13 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG14 ["apps/meteor/app/authorization/server/index.ts"]
        ._Rocket.Chat_apps_meteor_app_authorization_server_index.ts["Exports authorization-related functions and registers server methods."]
    end
    style SG14 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG15 ["apps/meteor/app/file-upload/server/lib/FileUpload.ts"]
        FileUpload.getStore["Retrieves a file storage handler by model name."]
        FileUpload.getStoreByName["FileUpload.getStoreByName"]
        FileUpload.get["FileUpload.get"]
        FileUpload.removeFilesByRoomId["FileUpload.removeFilesByRoomId"]
        FileUploadClass.deleteById["FileUploadClass.deleteById"]
    end
    style SG15 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG16 ["apps/meteor/app/file/server/file.server.ts"]
        RocketChatFile.dataURIParse["RocketChatFile.dataURIParse"]
    end
    style SG16 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG17 ["apps/meteor/app/invites/server/functions/validateInviteToken.ts"]
        validateInviteToken["validateInviteToken"]
    end
    style SG17 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG18 ["apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts"]
        addUserToDefaultChannels["addUserToDefaultChannels"]
    end
    style SG18 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG19 ["apps/meteor/app/lib/server/functions/addUserToRoom.ts"]
        addUserToRoom["addUserToRoom"]
    end
    style SG19 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG20 ["apps/meteor/app/lib/server/functions/checkEmailAvailability.ts"]
        checkEmailAvailability["Checks if an email address is available in the database."]
    end
    style SG20 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG21 ["apps/meteor/app/lib/server/functions/checkUsernameAvailability.ts"]
        toRegExp["toRegExp"]
        usernameIsBlocked["usernameIsBlocked"]
        checkUsernameAvailabilityWithValidation["Checks if a username is available with validation logic for user editing."]
        checkUsernameAvailability["Checks if a username is available in the system, considering blocked lists and existing users/teams."]
    end
    style SG21 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG22 ["apps/meteor/app/lib/server/functions/deleteRoom.ts"]
        deleteRoom["deleteRoom"]
    end
    style SG22 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG23 ["apps/meteor/app/lib/server/functions/deleteUser.ts"]
        deleteUser["Deletes a user account, cleans up associated data (messages, subscriptions, files, roles), and triggers cleanup callbacks."]
    end
    style SG23 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG24 ["apps/meteor/app/lib/server/functions/getAvatarSuggestionForUser.ts"]
        getAvatarSuggestionForUser["Fetches and processes potential user avatar URLs from various social/OAuth providers and Gravatar."]
    end
    style SG24 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG25 ["apps/meteor/app/lib/server/functions/getDefaultChannels.ts"]
        getDefaultChannels["getDefaultChannels"]
    end
    style SG25 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG26 ["apps/meteor/app/lib/server/functions/getFullUserData.ts"]
        getCustomFields["getCustomFields"]
        getFields["getFields"]
        findTargetUser["findTargetUser"]
        getFullUserDataByUniqueSearchTerm["Retrieves a full user object based on a unique search term, enforcing authorization checks and filtering sensitive fields."]
    end
    style SG26 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG27 ["apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts"]
        shouldRemoveOrChangeOwner["shouldRemoveOrChangeOwner"]
        getSubscribedRoomsForUserWithDetails["getSubscribedRoomsForUserWithDetails"]
    end
    style SG27 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG28 ["apps/meteor/app/lib/server/functions/getUserSingleOwnedRooms.ts"]
        getUserSingleOwnedRooms["getUserSingleOwnedRooms"]
    end
    style SG28 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG29 ["apps/meteor/app/lib/server/functions/getUsernameSuggestion.ts"]
        slug["slug"]
        usernameIsAvailable["usernameIsAvailable"]
        name["name"]
        generateUsernameSuggestion["Generates a unique username suggestion for a user based on their name, email, or OAuth services."]
    end
    style SG29 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG30 ["apps/meteor/app/lib/server/functions/joinDefaultChannels.ts"]
        joinDefaultChannels["joinDefaultChannels"]
    end
    style SG30 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG31 ["apps/meteor/app/lib/server/functions/relinquishRoomOwnerships.ts"]
        bulkTeamCleanup["bulkTeamCleanup"]
        bulkRoomCleanUp["bulkRoomCleanUp"]
        relinquishRoomOwnerships["relinquishRoomOwnerships"]
    end
    style SG31 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG32 ["apps/meteor/app/lib/server/functions/saveCustomFields.ts"]
        saveCustomFields["Validates and saves custom user profile fields to the database."]
    end
    style SG32 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG33 ["apps/meteor/app/lib/server/functions/saveCustomFieldsWithoutValidation.ts"]
        getCustomFieldsMeta["getCustomFieldsMeta"]
        saveCustomFieldsWithoutValidation["Saves custom user fields to the database without performing additional validation."]
    end
    style SG33 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG34 ["apps/meteor/app/lib/server/functions/saveUser/index.ts"]
        ._Rocket.Chat_apps_meteor_app_lib_server_functions_saveUser_index.ts["Exports core user saving and validation functions for use in other modules."]
    end
    style SG34 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG35 ["apps/meteor/app/lib/server/functions/saveUser/sendUserEmail.ts"]
        sendUserEmail["sendUserEmail"]
        sendWelcomeEmail["Sends a welcome email to a new user upon registration."]
    end
    style SG35 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG36 ["apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts"]
        canEditExtension["Validates if a specific VoIP extension can be assigned to a user."]
    end
    style SG36 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG37 ["apps/meteor/app/lib/server/functions/saveUserIdentity.ts"]
        saveUserIdentity["saveUserIdentity"]
        updateUsernameReferences["updateUsernameReferences"]
    end
    style SG37 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG38 ["apps/meteor/app/lib/server/functions/setRealName.ts"]
        setRealName["setRealName"]
    end
    style SG38 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG39 ["apps/meteor/app/lib/server/functions/setStatusText.ts"]
        setStatusText["Updates a user's status text, truncates it to 120 characters, persists to the database, and broadcasts the update."]
    end
    style SG39 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG40 ["apps/meteor/app/lib/server/functions/setUserAvatar.ts"]
        setAvatarFromServiceWithValidation["setAvatarFromServiceWithValidation"]
        setUserAvatar["Sets a user's avatar from a data URI, URL, or REST input, handling file storage and database updates."]
    end
    style SG40 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG41 ["apps/meteor/app/lib/server/functions/setUsername.ts"]
        isUserInFederatedRooms["isUserInFederatedRooms"]
        setUsernameWithValidation["Validates and updates a user's username, ensuring uniqueness and compliance with federation rules."]
        setUsername["_setUsername"]
    end
    style SG41 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG42 ["apps/meteor/app/lib/server/functions/updateGroupDMsName.ts"]
        getFname["getFname"]
        getName["getName"]
        getUsersWhoAreInTheSameGroupDMsAs["getUsersWhoAreInTheSameGroupDMsAs"]
        updateGroupDMsName["updateGroupDMsName"]
        getMembers["getMembers"]
    end
    style SG42 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG43 ["apps/meteor/app/lib/server/functions/validateCustomFields.js"]
        validateCustomFields["Validates user-defined custom fields against configured schema, ensuring required fields, types, and lengths are respected."]
    end
    style SG43 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG44 ["apps/meteor/app/lib/server/functions/validateName.ts"]
        validateName["validateName"]
    end
    style SG44 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG45 ["apps/meteor/app/lib/server/functions/validateNameChars.ts"]
        validateNameChars["Validates a name string for invalid characters, including URI-decoded checks."]
    end
    style SG45 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG46 ["apps/meteor/app/lib/server/functions/validateUsername.ts"]
        validateUsername["Validates a username string against a configurable regular expression pattern."]
    end
    style SG46 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG47 ["apps/meteor/app/lib/server/index.ts"]
        ._Rocket.Chat_apps_meteor_app_lib_server_index.ts["./Rocket.Chat/apps/meteor/app/lib/server/index.ts"]
    end
    style SG47 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG48 ["apps/meteor/app/lib/server/lib/deprecationWarningLogger.ts"]
        compareVersions["Compares a version string against the threshold for throwing deprecation errors."]
        method["Logs deprecation warnings for methods, including replacement information and metrics."]
        warn["Logs a generic deprecation warning."]
    end
    style SG48 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG49 ["apps/meteor/app/lib/server/lib/notifyListener.ts"]
        notifyOnRoomChangedById["notifyOnRoomChangedById"]
        notifyOnRoomChangedByUsernamesOrUids["notifyOnRoomChangedByUsernamesOrUids"]
        notifyOnIntegrationChangedByUserId["notifyOnIntegrationChangedByUserId"]
        notifyOnLivechatDepartmentAgentChanged["notifyOnLivechatDepartmentAgentChanged"]
        notifyOnSettingChanged["notifyOnSettingChanged"]
        notifyOnSettingChangedById["notifyOnSettingChangedById"]
        notifyOnUserChange["Broadcasts a notification about a user record change."]
        notifyOnUserChangeAsync["Executes a callback and notifies on user change if watchers are disabled."]
        notifyOnSubscriptionChanged["Broadcasts a notification when a subscription is changed."]
        notifyOnSubscriptionChangedByRoomIdAndUserId["Broadcasts a notification for a subscription identified by room ID and user ID."]
        notifyOnSubscriptionChangedById["notifyOnSubscriptionChangedById"]
        notifyOnSubscriptionChangedByUserPreferences["notifyOnSubscriptionChangedByUserPreferences"]
        notifyOnSubscriptionChangedByRoomId["notifyOnSubscriptionChangedByRoomId"]
        notifyOnSubscriptionChangedByAutoTranslateAndUserId["notifyOnSubscriptionChangedByAutoTranslateAndUserId"]
        notifyOnSubscriptionChangedByUserIdAndRoomType["notifyOnSubscriptionChangedByUserIdAndRoomType"]
        notifyOnSubscriptionChangedByNameAndRoomType["notifyOnSubscriptionChangedByNameAndRoomType"]
        notifyOnSubscriptionChangedByUserId["notifyOnSubscriptionChangedByUserId"]
    end
    style SG49 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG50 ["apps/meteor/app/lib/server/methods/createToken.ts"]
        generateAccessToken["Generates an access token for a given user if the provided secret matches the environment variable."]
    end
    style SG50 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG51 ["apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts"]
        deleteUserOwnAccount["Handles the deletion of a user's own account after verifying their password and system settings."]
    end
    style SG51 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG52 ["apps/meteor/app/mailer/server/api.ts"]
        replacekey["replacekey"]
        translate["translate"]
        replace["replace"]
        replaceEscaped["replaceEscaped"]
        wrap["wrap"]
        checkAddressFormat["checkAddressFormat"]
        sendNoWrap["sendNoWrap"]
        send["send"]
    end
    style SG52 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG53 ["apps/meteor/app/utils/lib/getDefaultSubscriptionPref.ts"]
        getDefaultSubscriptionPref["getDefaultSubscriptionPref"]
    end
    style SG53 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG54 ["apps/meteor/app/utils/lib/getURL.ts"]
        getCloudUrl["getCloudUrl"]
        getURL["_getURL"]
        getURLWithoutSettings["getURLWithoutSettings"]
    end
    style SG54 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG55 ["apps/meteor/app/utils/lib/mimeTypes.ts"]
        getMimeTypeFromFileName["getMimeTypeFromFileName"]
        getMimeType["getMimeType"]
    end
    style SG55 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG56 ["apps/meteor/app/utils/server/functions/isSMTPConfigured.ts"]
        isSMTPConfigured["Checks if SMTP is configured by verifying the MAIL_URL environment variable or SMTP_Host setting."]
    end
    style SG56 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG57 ["apps/meteor/app/utils/server/getURL.ts"]
        getURL_2["Generates a URL for a given path, incorporating site URL and CDN settings."]
    end
    style SG57 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG58 ["apps/meteor/app/utils/server/lib/getUserPreference.ts"]
        getUserPreference["getUserPreference"]
    end
    style SG58 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG59 ["apps/meteor/client/meteor/overrides/userAndUsers.ts"]
        Meteor.userId["Overrides Meteor.userId to return the current user ID from the local store."]
    end
    style SG59 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG60 ["apps/meteor/client/meteor/user.ts"]
        watchUserId["Watches and returns the current user ID."]
    end
    style SG60 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG61 ["apps/meteor/client/meteor/watch.ts"]
        watch["watch"]
    end
    style SG61 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG62 ["apps/meteor/imports/personal-access-tokens/server/api/methods/generateToken.ts"]
        generatePersonalAccessTokenOfUser["Generates a new personal access token for a user after checking permissions and ensuring uniqueness."]
    end
    style SG62 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG63 ["apps/meteor/imports/personal-access-tokens/server/api/methods/regenerateToken.ts"]
        regeneratePersonalAccessTokenOfUser["Regenerates a personal access token for a user after verifying authorization and token existence."]
    end
    style SG63 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG64 ["apps/meteor/imports/personal-access-tokens/server/api/methods/removeToken.ts"]
        removePersonalAccessTokenOfUser["Removes a personal access token for a user after verifying authorization."]
    end
    style SG64 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG65 ["apps/meteor/lib/roles/calculateRoomRolePriorityFromRoles.ts"]
        getRoomRolePriorityForRole["getRoomRolePriorityForRole"]
        calculateRoomRolePriorityFromRoles["calculateRoomRolePriorityFromRoles"]
    end
    style SG65 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG66 ["apps/meteor/lib/utils/isObject.ts"]
        isObject["isObject"]
    end
    style SG66 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG67 ["apps/meteor/lib/utils/parseCSV.ts"]
        parseCSV["parseCSV"]
    end
    style SG67 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG68 ["apps/meteor/lib/utils/stringUtils.ts"]
        makeString["makeString"]
        defaultToWhiteSpace["defaultToWhiteSpace"]
        trim["trim"]
        ltrim["ltrim"]
        rtrim["rtrim"]
        strLeft["strLeft"]
        strRightBack["strRightBack"]
    end
    style SG68 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG69 ["apps/meteor/server/database/utils.ts"]
        isExtendedSession["isExtendedSession"]
        onceTransactionCommitedSuccessfully["onceTransactionCommitedSuccessfully"]
        withError["withError"]
    end
    style SG69 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG70 ["apps/meteor/server/hooks/userLogoutCleanUp.ts"]
        runUserLogoutCleanUp["Triggers cleanup operations after a user logs out."]
    end
    style SG70 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG71 ["apps/meteor/server/lib/getSubscriptionAutotranslateDefaultConfig.ts"]
        getSubscriptionAutotranslateDefaultConfig["getSubscriptionAutotranslateDefaultConfig"]
    end
    style SG71 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG72 ["apps/meteor/server/lib/isUserIdFederated.ts"]
        isUserIdFederated["isUserIdFederated"]
    end
    style SG72 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG73 ["apps/meteor/server/lib/resetUserE2EKey.ts"]
        sendResetNotification["sendResetNotification"]
        resetUserE2EEncriptionKey["Resets a user's E2E encryption key and forces a re-login."]
    end
    style SG73 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG74 ["apps/meteor/server/lib/roles/addUserRoles.ts"]
        addUserRolesAsync["Adds roles to a user, optionally scoped to a specific room."]
    end
    style SG74 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG75 ["apps/meteor/server/lib/roles/removeUserFromRoles.ts"]
        removeUserFromRolesAsync["Removes specified roles from a user, optionally scoped to a room."]
    end
    style SG75 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG76 ["apps/meteor/server/lib/roles/syncRoomRolePriority.ts"]
        syncRoomRolePriorityForUserAndRoom["Synchronizes the room role priority for a user in a specific room."]
        updateRolePriority["updateRolePriority"]
    end
    style SG76 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG77 ["apps/meteor/server/lib/roles/validateRoleList.ts"]
        validateRoleList["Validates a list of role IDs against existing roles in the database."]
    end
    style SG77 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG78 ["apps/meteor/server/lib/rooms/roomCoordinator.ts"]
        RoomCoordinatorServer.allowMemberAction["RoomCoordinatorServer.allowMemberAction"]
        RoomCoordinatorServer.getRoomDirectives["Retrieves the server-side directives for a specific room type."]
    end
    style SG78 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG79 ["apps/meteor/server/lib/users/runAfterVerifyEmail.ts"]
        runAfterVerifyEmail["Updates user roles after email verification."]
    end
    style SG79 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG80 ["apps/meteor/server/methods/registerUser.ts"]
        registerUser["Registers a new user, handles anonymous access, and validates registration requirements."]
    end
    style SG80 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG81 ["apps/meteor/server/methods/requestDataDownload.ts"]
        requestDataDownload["Requests a data export operation for the user."]
    end
    style SG81 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG82 ["apps/meteor/server/methods/resetAvatar.ts"]
        resetAvatar["Resets a user's avatar after authorization checks."]
        userId["userId"]
    end
    style SG82 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG83 ["apps/meteor/server/methods/saveUserPreferences.ts"]
        updateNotificationPreferences["updateNotificationPreferences"]
        saveUserPreferences["Updates user preferences and propagates notification changes."]
    end
    style SG83 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG84 ["apps/meteor/server/methods/sendConfirmationEmail.ts"]
        sendConfirmationEmail["Sends a verification email to a user based on their email address."]
    end
    style SG84 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG85 ["apps/meteor/server/methods/sendForgotPasswordEmail.ts"]
        sendForgotPasswordEmail["Sends a password reset email to a user if they exist."]
    end
    style SG85 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG86 ["apps/meteor/server/methods/setUserActiveStatus.ts"]
        executeSetUserActiveStatus["Updates the active status of a user, requiring administrative permissions."]
        setUserActiveStatus["setUserActiveStatus"]
    end
    style SG86 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG87 ["apps/meteor/server/services/authorization/service.ts"]
        Authorization.hasPermission["Checks if a user has a specific permission, optionally scoped to a room."]
        Authorization.all["Authorization.all"]
    end
    style SG87 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG88 ["apps/meteor/server/services/user/lib/getNewUserRoles.ts"]
        getNewUserRoles["getNewUserRoles"]
    end
    style SG88 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG89 ["ee/apps/account-service/src/lib/utils.ts"]
        generateStampedLoginToken["_generateStampedLoginToken"]
        hashLoginToken["Hashes a login token using SHA-256 for secure storage."]
    end
    style SG89 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG90 ["packages/models/src/models/BaseRaw.ts"]
        BaseRaw.doNotMixInclusionAndExclusionFields["BaseRaw.doNotMixInclusionAndExclusionFields"]
        BaseRaw.ensureDefaultFields["BaseRaw.ensureDefaultFields"]
        BaseRaw.find["BaseRaw.find"]
        BaseRaw.updateOne["BaseRaw.updateOne"]
        BaseRaw.updateMany["BaseRaw.updateMany"]
        BaseRaw.deleteMany["BaseRaw.deleteMany"]
        BaseRaw.setUpdatedAt["BaseRaw.setUpdatedAt"]
    end
    style SG90 fill:#2a2a2a,stroke:#444,color:#aaa
    subgraph SG91 ["packages/models/src/models/LivechatDepartmentAgents.ts"]
        LivechatDepartmentAgentsRaw.findByAgentId["LivechatDepartmentAgentsRaw.findByAgentId"]
        LivechatDepartmentAgentsRaw.removeByAgentId["LivechatDepartmentAgentsRaw.removeByAgentId"]
        LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId["LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId"]
    end
    style SG91 fill:#2a2a2a,stroke:#444,color:#aaa
    action --> hashLoginToken
    action --> getFullUserDataByUniqueSearchTerm
    action --> deleteUser
    action --> generateUsernameSuggestion
    action --> validateCustomFields
    action --> saveCustomFields
    action --> getAvatarSuggestionForUser
    action --> checkUsernameAvailabilityWithValidation
    action --> checkUsernameAvailability
    action --> validateUsername
    action --> validateNameChars
    action --> saveCustomFieldsWithoutValidation
    action --> canEditExtension
    action --> ._Rocket.Chat_apps_meteor_app_lib_server_functions_saveUser_index.ts
    action --> sendWelcomeEmail
    action --> setUserAvatar
    action --> setStatusText
    action --> checkEmailAvailability
    action --> setUsernameWithValidation
    action --> notifyOnUserChange
    action --> notifyOnUserChangeAsync
    action --> deleteUserOwnAccount
    action --> generateAccessToken
    action --> resetUserE2EEncriptionKey
    action --> sendForgotPasswordEmail
    action --> executeSetUserActiveStatus
    action --> resetAvatar
    action --> sendConfirmationEmail
    action --> requestDataDownload
    action --> registerUser
    action --> saveUserPreferences
    action --> removePersonalAccessTokenOfUser
    action --> generatePersonalAccessTokenOfUser
    action --> regeneratePersonalAccessTokenOfUser
    action --> resetTOTP
    action --> getUserForCheck
    action --> isSMTPConfigured
    action --> hasPermissionAsync
    action --> findUsersToAutocomplete
    action --> findPaginatedUsersByStatus
    action --> ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts
    action --> getUploadFormData
    action --> APIClass.success
    action --> APIClass.failure
    action --> APIClass.forbidden
    action --> APIClass.this.parseJsonQuery
    action --> getUserInfo
    action --> getPaginationItems
    action --> getUserFromParams
    action --> isUserFromParams
    action --> runUserLogoutCleanUp
    action --> runAfterVerifyEmail
    action --> get
    getFullUserDataByUniqueSearchTerm --> getFields
    getFullUserDataByUniqueSearchTerm --> findTargetUser
    getFullUserDataByUniqueSearchTerm --> hasPermissionAsync
    deleteUser --> shouldRemoveOrChangeOwner
    deleteUser --> getSubscribedRoomsForUserWithDetails
    deleteUser --> getUserSingleOwnedRooms
    deleteUser --> updateGroupDMsName
    deleteUser --> relinquishRoomOwnerships
    deleteUser --> notifyOnRoomChangedById
    deleteUser --> notifyOnIntegrationChangedByUserId
    deleteUser --> notifyOnLivechatDepartmentAgentChanged
    deleteUser --> notifyOnUserChange
    deleteUser --> FileUpload.getStore
    deleteUser --> LivechatDepartmentAgentsRaw.findByAgentId
    deleteUser --> LivechatDepartmentAgentsRaw.removeByAgentId
    generateUsernameSuggestion --> slug
    generateUsernameSuggestion --> usernameIsAvailable
    generateUsernameSuggestion --> name
    validateCustomFields --> trim
    saveCustomFields --> validateCustomFields
    saveCustomFields --> saveCustomFieldsWithoutValidation
    saveCustomFields --> trim
    checkUsernameAvailabilityWithValidation --> checkUsernameAvailability
    checkUsernameAvailability --> toRegExp
    checkUsernameAvailability --> usernameIsBlocked
    checkUsernameAvailability --> validateName
    saveCustomFieldsWithoutValidation --> getCustomFieldsMeta
    saveCustomFieldsWithoutValidation --> notifyOnSubscriptionChangedByUserIdAndRoomType
    saveCustomFieldsWithoutValidation --> onceTransactionCommitedSuccessfully
    saveCustomFieldsWithoutValidation --> trim
    sendWelcomeEmail --> sendUserEmail
    setUserAvatar --> onceTransactionCommitedSuccessfully
    setUserAvatar --> FileUpload.getStore
    setUserAvatar --> RocketChatFile.dataURIParse
    setStatusText --> onceTransactionCommitedSuccessfully
    setUsernameWithValidation --> checkUsernameAvailability
    setUsernameWithValidation --> saveUserIdentity
    setUsernameWithValidation --> validateUsername
    setUsernameWithValidation --> joinDefaultChannels
    setUsernameWithValidation --> isUserInFederatedRooms
    setUsernameWithValidation --> notifyOnUserChange
    notifyOnUserChangeAsync --> notifyOnUserChange
    deleteUserOwnAccount --> deleteUser
    deleteUserOwnAccount --> method
    deleteUserOwnAccount --> deleteUserOwnAccount
    deleteUserOwnAccount --> trim
    deleteUserOwnAccount --> Meteor.userId
    generateAccessToken --> generateStampedLoginToken
    resetUserE2EEncriptionKey --> isUserIdFederated
    resetUserE2EEncriptionKey --> notifyOnUserChange
    resetUserE2EEncriptionKey --> notifyOnSubscriptionChangedByUserId
    resetUserE2EEncriptionKey --> sendResetNotification
    sendForgotPasswordEmail --> sendForgotPasswordEmail
    executeSetUserActiveStatus --> setUserActiveStatus
    executeSetUserActiveStatus --> hasPermissionAsync
    resetAvatar --> method
    resetAvatar --> resetAvatar
    resetAvatar --> userId
    resetAvatar --> hasPermissionAsync
    requestDataDownload --> method
    requestDataDownload --> requestDataDownload
    registerUser --> generateStampedLoginToken
    registerUser --> ._Rocket.Chat_apps_meteor_app_lib_server_index.ts
    registerUser --> Accounts.insertUserDoc
    registerUser --> registerUser
    registerUser --> validateInviteToken
    registerUser --> trim
    saveUserPreferences --> notifyOnUserChange
    saveUserPreferences --> notifyOnSubscriptionChangedByAutoTranslateAndUserId
    saveUserPreferences --> notifyOnSubscriptionChangedByUserId
    saveUserPreferences --> method
    saveUserPreferences --> updateNotificationPreferences
    saveUserPreferences --> saveUserPreferences
    saveUserPreferences --> Meteor.userId
    removePersonalAccessTokenOfUser --> removePersonalAccessTokenOfUser
    removePersonalAccessTokenOfUser --> hasPermissionAsync
    generatePersonalAccessTokenOfUser --> hashLoginToken
    generatePersonalAccessTokenOfUser --> hasPermissionAsync
    regeneratePersonalAccessTokenOfUser --> removePersonalAccessTokenOfUser
    regeneratePersonalAccessTokenOfUser --> generatePersonalAccessTokenOfUser
    regeneratePersonalAccessTokenOfUser --> hasPermissionAsync
    resetTOTP --> isUserIdFederated
    resetTOTP --> notifyOnUserChange
    resetTOTP --> sendResetNotification_2
    hasPermissionAsync --> Authorization.hasPermission
    findUsersToAutocomplete --> hasPermissionAsync
    findPaginatedUsersByStatus --> hasPermissionAsync
    getUploadFormData --> getMimeType
    APIClass.success --> isObject
    APIClass.failure --> isObject
    APIClass.this.parseJsonQuery --> APIClass.this.parseJsonQuery
    getUserInfo --> getURL_2
    getUserInfo --> isVerifiedEmail
    getUserInfo --> getUserPreferences
    getUserInfo --> filterOutdatedVersionUpdateBanners
    getUserInfo --> getUserCalendar
    runAfterVerifyEmail --> removeUserFromRolesAsync
    runAfterVerifyEmail --> addUserRolesAsync
    get --> getURL_2
    get --> hasPermissionAsync
    get --> getInclusiveFields
    get --> getNonEmptyFields
    get --> getNonEmptyQuery
    get --> ._Rocket.Chat_apps_meteor_app_api_server_lib_isValidQuery.ts
    get --> APIClass.success
    get --> APIClass.forbidden
    get --> APIClass.this.parseJsonQuery
    get --> getPaginationItems
    get --> getUserFromParams
    get --> get
    getFields --> getCustomFields
    getSubscribedRoomsForUserWithDetails --> ._Rocket.Chat_apps_meteor_app_authorization_server_index.ts
    updateGroupDMsName --> getFname
    updateGroupDMsName --> getName
    updateGroupDMsName --> getUsersWhoAreInTheSameGroupDMsAs
    updateGroupDMsName --> getMembers
    updateGroupDMsName --> notifyOnSubscriptionChangedByRoomId
    relinquishRoomOwnerships --> bulkRoomCleanUp
    relinquishRoomOwnerships --> addUserRolesAsync
    FileUpload.getStore --> FileUpload.getStoreByName
    FileUpload.getStore --> FileUpload.get
    LivechatDepartmentAgentsRaw.findByAgentId --> BaseRaw.find
    LivechatDepartmentAgentsRaw.removeByAgentId --> BaseRaw.deleteMany
    name --> slug
    trim --> makeString
    trim --> defaultToWhiteSpace
    onceTransactionCommitedSuccessfully --> isExtendedSession
    onceTransactionCommitedSuccessfully --> withError
    sendUserEmail --> send
    saveUserIdentity --> updateUsernameReferences
    saveUserIdentity --> validateName
    saveUserIdentity --> setRealName
    saveUserIdentity --> setUsername
    saveUserIdentity --> onceTransactionCommitedSuccessfully
    joinDefaultChannels --> addUserToDefaultChannels
    method --> compareVersions
    method --> warn
    Meteor.userId --> watchUserId
    sendResetNotification --> send
    setUserActiveStatus --> executeSetUserActiveStatus
    setUserActiveStatus --> Meteor.userId
    Accounts.insertUserDoc --> getAvatarSuggestionForUser
    Accounts.insertUserDoc --> setAvatarFromServiceWithValidation
    Accounts.insertUserDoc --> joinDefaultChannels
    Accounts.insertUserDoc --> notifyOnSettingChangedById
    Accounts.insertUserDoc --> addUserRolesAsync
    Accounts.insertUserDoc --> getNewUserRoles
    Accounts.insertUserDoc --> parseCSV
    updateNotificationPreferences --> notifyOnSubscriptionChangedByUserPreferences
    sendResetNotification_2 --> send
    Authorization.hasPermission --> Authorization.all
    getMimeType --> getMimeTypeFromFileName
    getURL_2 --> getURLWithoutSettings
    getUserPreferences --> getUserPreference
    removeUserFromRolesAsync --> notifyOnSubscriptionChangedByRoomIdAndUserId
    removeUserFromRolesAsync --> syncRoomRolePriorityForUserAndRoom
    removeUserFromRolesAsync --> validateRoleList
    addUserRolesAsync --> notifyOnSubscriptionChangedByRoomIdAndUserId
    addUserRolesAsync --> syncRoomRolePriorityForUserAndRoom
    addUserRolesAsync --> validateRoleList
    bulkRoomCleanUp --> bulkTeamCleanup
    bulkRoomCleanUp --> notifyOnSubscriptionChanged
    bulkRoomCleanUp --> FileUpload.removeFilesByRoomId
    bulkRoomCleanUp --> eraseRoomLooseValidation
    FileUpload.get --> FileUpload.getStoreByName
    FileUpload.get --> FileUpload.get
    BaseRaw.find --> BaseRaw.doNotMixInclusionAndExclusionFields
    BaseRaw.find --> BaseRaw.find
    BaseRaw.deleteMany --> BaseRaw.find
    BaseRaw.deleteMany --> BaseRaw.updateOne
    BaseRaw.deleteMany --> BaseRaw.deleteMany
    defaultToWhiteSpace --> makeString
    send --> replace
    send --> wrap
    send --> sendNoWrap
    updateUsernameReferences --> updateGroupDMsName
    updateUsernameReferences --> notifyOnRoomChangedByUsernamesOrUids
    updateUsernameReferences --> notifyOnSubscriptionChangedByNameAndRoomType
    updateUsernameReferences --> notifyOnSubscriptionChangedByUserId
    updateUsernameReferences --> FileUpload.getStore
    updateUsernameReferences --> LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId
    setRealName --> onceTransactionCommitedSuccessfully
    setUsername --> addUserToRoom
    setUsername --> getAvatarSuggestionForUser
    setUsername --> checkUsernameAvailability
    setUsername --> validateUsername
    setUsername --> setUserAvatar
    setUsername --> isUserInFederatedRooms
    setUsername --> onceTransactionCommitedSuccessfully
    addUserToDefaultChannels --> getDefaultChannels
    addUserToDefaultChannels --> notifyOnSubscriptionChangedById
    addUserToDefaultChannels --> getSubscriptionAutotranslateDefaultConfig
    addUserToDefaultChannels --> getDefaultSubscriptionPref
    warn --> compareVersions
    warn --> warn
    watchUserId --> watch
    setAvatarFromServiceWithValidation --> setUserAvatar
    setAvatarFromServiceWithValidation --> hasPermissionAsync
    getNewUserRoles --> parseCSV
    getURLWithoutSettings --> getURL
    syncRoomRolePriorityForUserAndRoom --> updateRolePriority
    syncRoomRolePriorityForUserAndRoom --> calculateRoomRolePriorityFromRoles
    bulkTeamCleanup --> eraseTeamOnRelinquishRoomOwnerships
    FileUpload.removeFilesByRoomId --> FileUpload.getStore
    FileUpload.removeFilesByRoomId --> FileUploadClass.deleteById
    eraseRoomLooseValidation --> deleteRoom
    BaseRaw.doNotMixInclusionAndExclusionFields --> BaseRaw.ensureDefaultFields
    BaseRaw.updateOne --> BaseRaw.updateOne
    BaseRaw.updateOne --> BaseRaw.setUpdatedAt
    replace --> replacekey
    replace --> translate
    replace --> replace
    replace --> strLeft
    replace --> strRightBack
    wrap --> replace
    wrap --> replaceEscaped
    sendNoWrap --> notifyOnSettingChanged
    sendNoWrap --> checkAddressFormat
    LivechatDepartmentAgentsRaw.replaceUsernameOfAgentByUserId --> BaseRaw.updateMany
    addUserToRoom --> notifyOnRoomChangedById
    addUserToRoom --> notifyOnSubscriptionChanged
    addUserToRoom --> RoomCoordinatorServer.allowMemberAction
    addUserToRoom --> RoomCoordinatorServer.getRoomDirectives
    getURL --> getCloudUrl
    getURL --> trim
    getURL --> ltrim
    getURL --> rtrim
    updateRolePriority --> calculateRoomRolePriorityFromRoles
    calculateRoomRolePriorityFromRoles --> getRoomRolePriorityForRole
    eraseTeamOnRelinquishRoomOwnerships --> eraseRoomLooseValidation
    FileUploadClass.deleteById --> FileUpload.getStoreByName
    deleteRoom --> notifyOnRoomChangedById
    deleteRoom --> notifyOnSubscriptionChanged
    deleteRoom --> FileUpload.getStore
    deleteRoom --> FileUpload.removeFilesByRoomId
    BaseRaw.setUpdatedAt --> BaseRaw.setUpdatedAt
    replacekey --> replace
    strLeft --> makeString
    strRightBack --> makeString
    replaceEscaped --> replace
    BaseRaw.updateMany --> BaseRaw.updateMany
    BaseRaw.updateMany --> BaseRaw.setUpdatedAt
    getCloudUrl --> ltrim
    getCloudUrl --> rtrim
    ltrim --> makeString
    ltrim --> defaultToWhiteSpace
    rtrim --> makeString
    rtrim --> defaultToWhiteSpace
Loading
Fix with AI

Open in Cursor Open in Claude

Fix the following security vulnerability found by Hacktron.

File: apps/meteor/app/api/server/v1/users.ts
Lines: 2118-2130
Severity: high

Vulnerability: Missing authorization check in users.update API allows account takeover and privilege escalation

Description:
The `users.update` API endpoint does not enforce sufficient authorization checks before calling `saveUser` to update user information. While it requires 2FA, it lacks explicit permission requirements (e.g., `edit-other-user-info`), potentially allowing authenticated users to modify the profiles of other users.

Affected Code:
API.v1.post(
	'users.update',
	{
		authRequired: true,
		twoFactorRequired: true,
		body: isUsersUpdateParamsPOST,
		response: {
			200: userObjectResponse,
			400: validateBadRequestErrorResponse,
			401: validateUnauthorizedErrorResponse,
		},
	},
	async function action() {
		// ...
		await saveUser(this.userId, userData, { auditStore });
        // ...
	},
);

Fix this vulnerability. Only change what's necessary - don't modify unrelated code.

Triage: Reply !fp <reason> (false positive), !valid (confirmed), or !accepted_risk <reason>. Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing. Any other reply is saved as a triage note.

View finding in Hacktron

);
Comment on lines +2093 to +2123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

User lookup should occur after verifyEmail succeeds to avoid skipping cleanup.

The user is looked up before Meteor.callAsync('verifyEmail', token), but the token might be valid even if no user was found due to a race or projection mismatch. If verifyEmail succeeds (marks email verified) but the prior lookup returned null, runAfterVerifyEmail is skipped—leaving the anonymous→user role swap incomplete.

Consider moving the user lookup after the verifyEmail call or re-fetching the user after verification succeeds.

Proposed fix
 async function action() {
 	const { token } = this.bodyParams;

-	const user = await Users.findOne<Pick<IUser, '_id'>>({ 'services.email.verificationTokens.token': token }, { projection: { _id: 1 } });
-
 	await Meteor.callAsync('verifyEmail', token);

+	const user = await Users.findOne<Pick<IUser, '_id'>>({ 'emails.verified': true, 'services.email.verificationTokens.token': { $exists: false } }, { projection: { _id: 1 } });
+	// Alternative: lookup by the token that was just consumed (if Meteor stores it differently post-verification)
+	// Or pass userId from verifyEmail if it returns it
+
 	if (user) {
 		await runAfterVerifyEmail(user._id);
 	}

 	return API.v1.success();
 }

Note: The exact query after verification depends on how verifyEmail modifies the user document. Verify that the lookup correctly identifies the user whose email was just verified.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/meteor/app/api/server/v1/users.ts` around lines 2101 - 2131, The user
lookup using Users.findOne({ 'services.email.verificationTokens.token': token })
is performed before calling Meteor.callAsync('verifyEmail', token), which can
cause runAfterVerifyEmail(user._id) to be skipped if the pre-verify query
returned null; move the Users.findOne call to after the
Meteor.callAsync('verifyEmail', token) (or re-fetch the user after verifyEmail
returns) so you always look up the user post-verification and then call
runAfterVerifyEmail with the found user's _id; update the users.verifyEmail
handler accordingly to ensure the lookup and runAfterVerifyEmail happen only
after Meteor.callAsync('verifyEmail', token) succeeds.


settings.watch<number>('Rate_Limiter_Limit_RegisterUser', (value) => {
const userRegisterRoute = '/api/v1/users.registerpost';

Expand Down
78 changes: 78 additions & 0 deletions apps/meteor/app/authorization/server/functions/permissionRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { License } from '@rocket.chat/core-services';
import { Permissions } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { hasPermissionAsync } from './hasPermission';
import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener';
import { CONSTANTS, AuthorizationUtils } from '../../lib';

export const addPermissionToRoleMethod = async (uid: string | null, permissionId: string, role: string): Promise<void> => {
if (role === 'guest' && !AuthorizationUtils.hasRestrictionsToRole(role) && (await License.hasValidLicense())) {
AuthorizationUtils.addRolePermissionWhiteList(role, await License.getGuestPermissions());
}

if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) {
throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', {
method: 'authorization:addPermissionToRole',
action: 'Adding_permission',
});
}

const permission = await Permissions.findOneById(permissionId);

if (!permission) {
throw new Meteor.Error('error-invalid-permission', 'Permission does not exist', {
method: 'authorization:addPermissionToRole',
action: 'Adding_permission',
});
}

if (
!uid ||
!(await hasPermissionAsync(uid, 'access-permissions')) ||
(permission.level === CONSTANTS.SETTINGS_LEVEL && !(await hasPermissionAsync(uid, 'access-setting-permissions')))
) {
throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', {
method: 'authorization:addPermissionToRole',
action: 'Adding_permission',
});
}

if (permission.groupPermissionId) {
await Permissions.addRole(permission.groupPermissionId, role);
void notifyOnPermissionChangedById(permission.groupPermissionId);
}

await Permissions.addRole(permission._id, role);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Validate the role exists before writing it to permission roles, otherwise invalid role IDs can be persisted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/authorization/server/functions/permissionRole.ts, line 46:

<comment>Validate the role exists before writing it to permission roles, otherwise invalid role IDs can be persisted.</comment>

<file context>
@@ -0,0 +1,78 @@
+		void notifyOnPermissionChangedById(permission.groupPermissionId);
+	}
+
+	await Permissions.addRole(permission._id, role);
+
+	void notifyOnPermissionChangedById(permission._id);
</file context>


void notifyOnPermissionChangedById(permission._id);
};

export const removeRoleFromPermissionMethod = async (uid: string | null, permissionId: string, role: string): Promise<void> => {
const permission = await Permissions.findOneById(permissionId);

if (!permission) {
throw new Meteor.Error('error-permission-not-found', 'Permission not found', {
method: 'authorization:removeRoleFromPermission',
});
}

if (
!uid ||
!(await hasPermissionAsync(uid, 'access-permissions')) ||
(permission.level === CONSTANTS.SETTINGS_LEVEL && !(await hasPermissionAsync(uid, 'access-setting-permissions')))
) {
throw new Meteor.Error('error-action-not-allowed', 'Removing permission is not allowed', {
method: 'authorization:removeRoleFromPermission',
action: 'Removing_permission',
});
}

if (permission.groupPermissionId) {
await Permissions.removeRole(permission.groupPermissionId, role);
void notifyOnPermissionChangedById(permission.groupPermissionId);
}

await Permissions.removeRole(permission._id, role);
void notifyOnPermissionChangedById(permission._id);
};
Loading
Loading