diff --git a/.changeset/rest-batch4-logout-cleanup.md b/.changeset/rest-batch4-logout-cleanup.md new file mode 100644 index 0000000000000..f9d11607b7a0e --- /dev/null +++ b/.changeset/rest-batch4-logout-cleanup.md @@ -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`. diff --git a/.changeset/rest-cloud-connect-workspace.md b/.changeset/rest-cloud-connect-workspace.md new file mode 100644 index 0000000000000..63f5ac3751f7a --- /dev/null +++ b/.changeset/rest-cloud-connect-workspace.md @@ -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. diff --git a/.changeset/rest-integrations-history-clear-replay.md b/.changeset/rest-integrations-history-clear-replay.md new file mode 100644 index 0000000000000..c1537cdc3d79e --- /dev/null +++ b/.changeset/rest-integrations-history-clear-replay.md @@ -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. diff --git a/.changeset/rest-permissions-add-remove-role.md b/.changeset/rest-permissions-add-remove-role.md new file mode 100644 index 0000000000000..f4a6b916a2724 --- /dev/null +++ b/.changeset/rest-permissions-add-remove-role.md @@ -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. diff --git a/.changeset/rest-users-setavatar-service.md b/.changeset/rest-users-setavatar-service.md new file mode 100644 index 0000000000000..7d4ef483f395f --- /dev/null +++ b/.changeset/rest-users-setavatar-service.md @@ -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. diff --git a/.changeset/rest-users-verify-email.md b/.changeset/rest-users-verify-email.md new file mode 100644 index 0000000000000..ffca4eac98bff --- /dev/null +++ b/.changeset/rest-users-verify-email.md @@ -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. diff --git a/apps/meteor/app/api/server/v1/cloud.ts b/apps/meteor/app/api/server/v1/cloud.ts index 1338192c00ec5..44a898d18d1c6 100644 --- a/apps/meteor/app/api/server/v1/cloud.ts +++ b/apps/meteor/app/api/server/v1/cloud.ts @@ -1,6 +1,7 @@ import type { CloudRegistrationIntentData, CloudConfirmationPollData, CloudRegistrationStatus } from '@rocket.chat/core-typings'; import { isCloudConfirmationPollProps, + isCloudConnectWorkspaceProps, isCloudCreateRegistrationIntentProps, isCloudManualRegisterProps, ajv, @@ -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 { @@ -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(); + }, +); + API.v1.get( 'cloud.checkoutUrl', { diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 2870fdb8236e8..b8998b622973c 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -8,6 +8,8 @@ import { isIntegrationsGetProps, isIntegrationsUpdateProps, isIntegrationsListProps, + isIntegrationsClearHistoryProps, + isIntegrationsReplayProps, validateBadRequestErrorResponse, validateForbiddenErrorResponse, validateUnauthorizedErrorResponse, @@ -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, @@ -363,3 +369,47 @@ API.v1.put( } }, ); + +const voidIntegrationsResponse = ajv.compile({ + 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(); + }, +); diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index 38c63b1f22a4c..ad289482464a3 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -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'; @@ -62,6 +63,25 @@ const isPermissionsListAll = ajvQuery.compile(permissio const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); +type PermissionRolePayload = { permissionId: string; role: string }; + +const isPermissionRolePayload = ajv.compile({ + type: 'object', + properties: { + permissionId: { type: 'string', minLength: 1 }, + role: { type: 'string', minLength: 1 }, + }, + required: ['permissionId', 'role'], + additionalProperties: false, +}); + +const voidPermissionResponse = ajv.compile({ + type: 'object', + properties: { success: { type: 'boolean', enum: [true] } }, + required: ['success'], + additionalProperties: false, +}); + const permissionsEndpoints = API.v1 .get( 'permissions.listAll', @@ -185,6 +205,42 @@ const permissionsEndpoints = API.v1 permissions: result, }); }, + ) + .post( + 'permissions.addRole', + { + authRequired: true, + 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, + 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; diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index cab77ce07ef90..be21f5fd7f344 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -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'; @@ -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'); return API.v1.success(); }, @@ -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'); @@ -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!`, }); @@ -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>({ 'services.email.verificationTokens.token': token }, { projection: { _id: 1 } }); + + await Meteor.callAsync('verifyEmail', token); + + if (user) { + await runAfterVerifyEmail(user._id); + } + + return API.v1.success(); + }, +); + settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { const userRegisterRoute = '/api/v1/users.registerpost'; diff --git a/apps/meteor/app/authorization/server/functions/permissionRole.ts b/apps/meteor/app/authorization/server/functions/permissionRole.ts new file mode 100644 index 0000000000000..f5ed30bc133d0 --- /dev/null +++ b/apps/meteor/app/authorization/server/functions/permissionRole.ts @@ -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 => { + 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); + + void notifyOnPermissionChangedById(permission._id); +}; + +export const removeRoleFromPermissionMethod = async (uid: string | null, permissionId: string, role: string): Promise => { + 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); +}; diff --git a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts index 3f5f7bfc1a263..2e8f2a66af9bf 100644 --- a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts @@ -1,11 +1,8 @@ -import { License } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Permissions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; -import { CONSTANTS, AuthorizationUtils } from '../../lib'; -import { hasPermissionAsync } from '../functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { addPermissionToRoleMethod } from '../functions/permissionRole'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -16,46 +13,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async 'authorization:addPermissionToRole'(permissionId, role) { - 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 uid = Meteor.userId(); - 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', - }); - } - - // for setting-based-permissions, authorize the group access as well - if (permission.groupPermissionId) { - await Permissions.addRole(permission.groupPermissionId, role); - void notifyOnPermissionChangedById(permission.groupPermissionId); - } - - await Permissions.addRole(permission._id, role); - - void notifyOnPermissionChangedById(permission._id); + methodDeprecationLogger.method('authorization:addPermissionToRole', '9.0.0', '/v1/permissions.addRole'); + await addPermissionToRoleMethod(Meteor.userId(), permissionId, role); }, }); diff --git a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts index 68ca11ef9fb5f..2a1d53933b208 100644 --- a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts +++ b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts @@ -1,10 +1,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Permissions } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; -import { CONSTANTS } from '../../lib'; -import { hasPermissionAsync } from '../functions/hasPermission'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { removeRoleFromPermissionMethod } from '../functions/permissionRole'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,34 +13,7 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async 'authorization:removeRoleFromPermission'(permissionId, role) { - const uid = Meteor.userId(); - 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', - }); - } - - // for setting based permissions, revoke the group permission once all setting permissions - // related to this group have been removed - if (permission.groupPermissionId) { - await Permissions.removeRole(permission.groupPermissionId, role); - void notifyOnPermissionChangedById(permission.groupPermissionId); - } - - await Permissions.removeRole(permission._id, role); - void notifyOnPermissionChangedById(permission._id); + methodDeprecationLogger.method('authorization:removeRoleFromPermission', '9.0.0', '/v1/permissions.removeRole'); + await removeRoleFromPermissionMethod(Meteor.userId(), permissionId, role); }, }); diff --git a/apps/meteor/app/cloud/server/methods.ts b/apps/meteor/app/cloud/server/methods.ts index ef6e6b34927ea..9dccd0fe013a9 100644 --- a/apps/meteor/app/cloud/server/methods.ts +++ b/apps/meteor/app/cloud/server/methods.ts @@ -112,6 +112,7 @@ Meteor.methods({ return true; }, async 'cloud:connectWorkspace'(token) { + methodDeprecationLogger.method('cloud:connectWorkspace', '9.0.0', '/v1/cloud.connectWorkspace'); check(token, String); const uid = Meteor.userId(); diff --git a/apps/meteor/app/integrations/server/functions/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/functions/clearIntegrationHistory.ts new file mode 100644 index 0000000000000..34b2c63508e2c --- /dev/null +++ b/apps/meteor/app/integrations/server/functions/clearIntegrationHistory.ts @@ -0,0 +1,70 @@ +import type { IOutgoingIntegration, IIntegration } from '@rocket.chat/core-typings'; +import { Integrations, IntegrationHistory } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; + +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import notifications from '../../../notifications/server/lib/Notifications'; +import { triggerHandler } from '../lib/triggerHandler'; + +export const clearIntegrationHistoryMethod = async (userId: string | null, integrationId: string): Promise => { + if (!userId) { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' }); + } + + let integration: IIntegration | null = null; + + if (await hasPermissionAsync(userId, 'manage-outgoing-integrations')) { + integration = await Integrations.findOneById(integrationId); + } else if (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')) { + integration = await Integrations.findOne({ + '_id': integrationId, + '_createdBy._id': userId, + }); + } else { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' }); + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'clearIntegrationHistory' }); + } + + await IntegrationHistory.removeByIntegrationId(integrationId); + + notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed', id: integrationId }); +}; + +export const replayOutgoingIntegrationMethod = async ( + userId: string | null, + { integrationId, historyId }: { integrationId: string; historyId: string }, +): Promise => { + if (!userId) { + throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'replayOutgoingIntegration' }); + } + + let integration: IOutgoingIntegration | null = null; + + if (await hasPermissionAsync(userId, 'manage-outgoing-integrations')) { + integration = await Integrations.findOneById(integrationId); + } else if (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')) { + const found = await Integrations.findOne({ + '_id': integrationId, + '_createdBy._id': userId, + }); + + if (found && 'event' in found) { + integration = found; + } + } + + if (!integration) { + throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'replayOutgoingIntegration' }); + } + + const history = await IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); + + if (!history) { + throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { method: 'replayOutgoingIntegration' }); + } + + await triggerHandler.replay(integration, history); +}; diff --git a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts index a30bb9b5ee9a3..93e3bf7b4c320 100644 --- a/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts +++ b/apps/meteor/app/integrations/server/methods/clearIntegrationHistory.ts @@ -1,9 +1,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import notifications from '../../../notifications/server/lib/Notifications'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; +import { clearIntegrationHistoryMethod } from '../functions/clearIntegrationHistory'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,38 +13,8 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async clearIntegrationHistory(integrationId) { - let integration; - - if (!this.userId) { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'clearIntegrationHistory', - }); - } - - if (await hasPermissionAsync(this.userId, 'manage-outgoing-integrations')) { - integration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-outgoing-integrations')) { - integration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - } else { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'clearIntegrationHistory', - }); - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'clearIntegrationHistory', - }); - } - - // Don't sending to IntegrationHistory listener since it don't waits for 'removed' events. - await IntegrationHistory.removeByIntegrationId(integrationId); - - notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed', id: integrationId }); - + methodDeprecationLogger.method('clearIntegrationHistory', '9.0.0', '/v1/integrations.history.clear'); + await clearIntegrationHistoryMethod(this.userId, integrationId); return true; }, }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts index c8108bfc2acda..0176e1a59e7ba 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts @@ -1,10 +1,8 @@ -import type { IOutgoingIntegration } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; -import { triggerHandler } from '../../lib/triggerHandler'; +import { methodDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; +import { replayOutgoingIntegrationMethod } from '../../functions/clearIntegrationHistory'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,43 +13,8 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async replayOutgoingIntegration({ integrationId, historyId }) { - let integration: IOutgoingIntegration | null = null; - - if (!this.userId) { - throw new Meteor.Error('not_authorized', 'Unauthorized', { - method: 'replayOutgoingIntegration', - }); - } - - if (await hasPermissionAsync(this.userId, 'manage-outgoing-integrations')) { - integration = await Integrations.findOneById(integrationId); - } else if (await hasPermissionAsync(this.userId, 'manage-own-outgoing-integrations')) { - const foundIntegration = await Integrations.findOne({ - '_id': integrationId, - '_createdBy._id': this.userId, - }); - - if (foundIntegration && 'event' in foundIntegration) { - integration = foundIntegration; - } - } - - if (!integration) { - throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { - method: 'replayOutgoingIntegration', - }); - } - - const history = await IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); - - if (!history) { - throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { - method: 'replayOutgoingIntegration', - }); - } - - await triggerHandler.replay(integration, history); - + methodDeprecationLogger.method('replayOutgoingIntegration', '9.0.0', '/v1/integrations.outgoing.replay'); + await replayOutgoingIntegrationMethod(this.userId, { integrationId, historyId }); return true; }, }); diff --git a/apps/meteor/client/hooks/useUpdateAvatar.ts b/apps/meteor/client/hooks/useUpdateAvatar.ts index d8a122683215a..0a03f1b267d18 100644 --- a/apps/meteor/client/hooks/useUpdateAvatar.ts +++ b/apps/meteor/client/hooks/useUpdateAvatar.ts @@ -1,5 +1,5 @@ import type { AvatarObject, AvatarServiceObject, AvatarReset, AvatarUrlObj, IUser } from '@rocket.chat/core-typings'; -import { useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -17,7 +17,6 @@ export const useUpdateAvatar = (avatarObj: AvatarObject, userId: IUser['_id']) = const avatarUrl = isAvatarUrl(avatarObj) ? avatarObj.avatarUrl : ''; const successMessage = t('Avatar_changed_successfully'); - const setAvatarFromService = useMethod('setAvatarFromService'); const dispatchToastMessage = useToastMessageDispatch(); @@ -54,30 +53,19 @@ export const useUpdateAvatar = (avatarObj: AvatarObject, userId: IUser['_id']) = } if (isServiceObject(avatarObj)) { - const { blob, contentType, service } = avatarObj; - try { - await setAvatarFromService(blob, contentType, service); - dispatchToastMessage({ type: 'success', message: successMessage }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } + const { blob, service } = avatarObj; + const formData = new FormData(); + formData.append('userId', userId); + formData.append('service', service); + formData.append('image', blob); + await saveAvatarAction(formData); return; } if (avatarObj instanceof FormData) { avatarObj.set('userId', userId); await saveAvatarAction(avatarObj); } - }, [ - avatarObj, - avatarUrl, - dispatchToastMessage, - resetAvatarAction, - saveAvatarAction, - saveAvatarUrlAction, - setAvatarFromService, - successMessage, - userId, - ]); + }, [avatarObj, avatarUrl, resetAvatarAction, saveAvatarAction, saveAvatarUrlAction, successMessage, userId]); return updateAvatar; }; diff --git a/apps/meteor/client/meteor/startup/accounts.ts b/apps/meteor/client/meteor/startup/accounts.ts index 6971f594db49b..5554798a53f0f 100644 --- a/apps/meteor/client/meteor/startup/accounts.ts +++ b/apps/meteor/client/meteor/startup/accounts.ts @@ -75,9 +75,8 @@ settings.observe(FORGET_SESSION_SETTING_ID, applyForgetSessionOnWindowClose); getDdpSdk().account.onEmailVerificationLink(async (token: string) => { try { - await sdk.call('verifyEmail', token); + await sdk.rest.post('/v1/users.verifyEmail', { token }); await whenMainReady(); - void sdk.call('afterVerifyEmail'); dispatchToastMessage({ type: 'success', message: t('Email_verified') }); } catch (error) { await whenMainReady(); diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 5ef3566201b02..d5a12606c3249 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -16,7 +16,6 @@ import { useDeleteUser } from './hooks/useDeleteUser'; import { useEmailVerificationWarning } from './hooks/useEmailVerificationWarning'; import { useReloadAfterLogin } from './hooks/useReloadAfterLogin'; import { useUpdateAvatar } from './hooks/useUpdateAvatar'; -import { sdk } from '../../../app/utils/client/lib/SDKClient'; import { useIdleConnection } from '../../hooks/useIdleConnection'; import type { IDocumentMapStore } from '../../lib/cachedStores/DocumentMapStore'; import { applyQueryOptions } from '../../lib/cachedStores/applyQueryOptions'; @@ -30,18 +29,14 @@ type UserProviderProps = { children: ReactNode; }; +// Local logout broadcaster — `onLogout(cb)` consumers (e.g. e2ee cleanup) still +// subscribe to this. The post-logout side effects that used to require a +// `sdk.call('logoutCleanUp')` round-trip (afterLogoutCleanUpCallback + +// Apps.IPostUserLoggedOut) now fire server-side via `Accounts.onLogout` and +// `POST /v1/users.logout`, so this emitter is purely client-side fan-out. const ee = new Emitter(); getDdpSdk().account.onLogout(() => ee.emit('logout')); -ee.on('logout', async () => { - const userId = userIdStore.getState(); - if (!userId) return; - const user = Users.state.get(userId); - if (!user) return; - - await sdk.call('logoutCleanUp', user); -}); - const queryRoom = ( query: Filter>, ): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => IRoom | undefined] => { diff --git a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx index 513f60429469b..04f7dda1ed2bd 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/history/HistoryItem.tsx @@ -1,7 +1,7 @@ import type { IIntegrationHistory, Serialized } from '@rocket.chat/core-typings'; import { Button, Icon, Box, AccordionItem, Field, FieldGroup, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useStableCallback } from '@rocket.chat/fuselage-hooks'; -import { useMethod } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import DOMPurify from 'dompurify'; import type { MouseEvent } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +13,7 @@ import { useHighlightedCode } from '../../../../../hooks/useHighlightedCode'; const HistoryItem = ({ data }: { data: Serialized }) => { const { t } = useTranslation(); - const replayOutgoingIntegration = useMethod('replayOutgoingIntegration'); + const replayOutgoingIntegration = useEndpoint('POST', '/v1/integrations.outgoing.replay'); const { _id, diff --git a/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx b/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx index 2784d26eb6c48..1452cacf54613 100644 --- a/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx +++ b/apps/meteor/client/views/admin/integrations/outgoing/history/OutgoingWebhookHistoryPage.tsx @@ -1,6 +1,6 @@ import { Button, ButtonGroup, Pagination } from '@rocket.chat/fuselage'; import { CustomScrollbars, usePagination, Page, PageHeader, PageContent } from '@rocket.chat/ui-client'; -import { useToastMessageDispatch, useRouteParameter, useMethod, useTranslation, useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useRouteParameter, useTranslation, useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { ComponentProps } from 'react'; import { useMemo, useState, useEffect } from 'react'; @@ -18,7 +18,7 @@ const OutgoingWebhookHistoryPage = (props: ComponentProps) => { const [mounted, setMounted] = useState(false); const [total, setTotal] = useState(0); - const clearIntegrationHistory = useMethod('clearIntegrationHistory'); + const clearIntegrationHistory = useEndpoint('POST', '/v1/integrations.history.clear'); const id = useRouteParameter('id') as string; @@ -52,7 +52,7 @@ const OutgoingWebhookHistoryPage = (props: ComponentProps) => { const handleClearHistory = async (): Promise => { try { - await clearIntegrationHistory(id); + await clearIntegrationHistory({ integrationId: id }); dispatchToastMessage({ type: 'success', message: t('Integration_History_Cleared') }); refetch(); setMounted(false); diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx index 64171fb39e39a..7be98ee3886a3 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx @@ -3,7 +3,8 @@ import { css } from '@rocket.chat/css-in-js'; import { Pagination, Palette } from '@rocket.chat/fuselage'; import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '@rocket.chat/ui-client'; import type { usePagination } from '@rocket.chat/ui-client'; -import { useMethod } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import PermissionRow from './PermissionRow'; @@ -22,8 +23,22 @@ type PermissionsTableProps = { const PermissionsTable = ({ roleList, permissions, setFilter, total, paginationData }: PermissionsTableProps) => { const { t } = useTranslation(); - const grantRole = useMethod('authorization:addPermissionToRole'); - const removeRole = useMethod('authorization:removeRoleFromPermission'); + const addPermissionRole = useEndpoint('POST', '/v1/permissions.addRole'); + const removePermissionRole = useEndpoint('POST', '/v1/permissions.removeRole'); + + const grantRole = useCallback( + async (permissionId: IPermission['_id'], role: IRole['_id']) => { + await addPermissionRole({ permissionId, role }); + }, + [addPermissionRole], + ); + + const removeRole = useCallback( + async (permissionId: IPermission['_id'], role: IRole['_id']) => { + await removePermissionRole({ permissionId, role }); + }, + [removePermissionRole], + ); const { current, itemsPerPage, setCurrent, setItemsPerPage, ...paginationProps } = paginationData; diff --git a/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceTokenModal.tsx b/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceTokenModal.tsx index 9d156e965991a..d3de17a9ebbcb 100644 --- a/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceTokenModal.tsx +++ b/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceTokenModal.tsx @@ -15,7 +15,7 @@ import { ModalContent, ModalFooter, } from '@rocket.chat/fuselage'; -import { useMethod, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; import { useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; @@ -31,7 +31,7 @@ const RegisterWorkspaceTokenModal = ({ onClose, onStatusChange, ...props }: Regi const setModal = useSetModal(); const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const connectWorkspace = useMethod('cloud:connectWorkspace'); + const connectWorkspace = useEndpoint('POST', '/v1/cloud.connectWorkspace'); const [token, setToken] = useState(''); const [processing, setProcessing] = useState(false); @@ -53,11 +53,7 @@ const RegisterWorkspaceTokenModal = ({ onClose, onStatusChange, ...props }: Regi setError(false); try { - const isConnected = await connectWorkspace(token); - - if (!isConnected) { - throw Error(t('RegisterWorkspace_Connection_Error')); - } + await connectWorkspace({ token }); setModal(null); diff --git a/apps/meteor/server/hooks/index.ts b/apps/meteor/server/hooks/index.ts index 64d500b223f25..1a9f55034ccbd 100644 --- a/apps/meteor/server/hooks/index.ts +++ b/apps/meteor/server/hooks/index.ts @@ -1 +1,2 @@ import './sauMonitorHooks'; +import './userLogoutCleanUp'; diff --git a/apps/meteor/server/hooks/userLogoutCleanUp.ts b/apps/meteor/server/hooks/userLogoutCleanUp.ts new file mode 100644 index 0000000000000..c725cdb6c61ee --- /dev/null +++ b/apps/meteor/server/hooks/userLogoutCleanUp.ts @@ -0,0 +1,21 @@ +import { AppEvents, Apps } from '@rocket.chat/apps'; +import type { IUser } from '@rocket.chat/core-typings'; +import { Accounts } from 'meteor/accounts-base'; + +import { afterLogoutCleanUpCallback } from '../lib/callbacks/afterLogoutCleanUpCallback'; + +export const runUserLogoutCleanUp = async (user: IUser): Promise => { + setImmediate(() => { + void afterLogoutCleanUpCallback.run(user); + }); + + await Apps.self?.triggerEvent(AppEvents.IPostUserLoggedOut, user); +}; + +Accounts.onLogout(async ({ user }: { user?: IUser }) => { + if (!user) { + return; + } + + await runUserLogoutCleanUp(user); +}); diff --git a/apps/meteor/server/lib/users/runAfterVerifyEmail.ts b/apps/meteor/server/lib/users/runAfterVerifyEmail.ts new file mode 100644 index 0000000000000..f2bf9ef2a33af --- /dev/null +++ b/apps/meteor/server/lib/users/runAfterVerifyEmail.ts @@ -0,0 +1,31 @@ +import type { IRole, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { addUserRolesAsync } from '../roles/addUserRoles'; +import { removeUserFromRolesAsync } from '../roles/removeUserFromRoles'; + +const rolesToChangeTo: Map = new Map([['anonymous', ['user']]]); + +export const runAfterVerifyEmail = async (userId: IUser['_id']): Promise => { + const user = await Users.findOneById(userId); + if (!user?.emails || !Array.isArray(user.emails)) { + return; + } + + const verifiedEmail = user.emails.find((email) => email.verified); + if (!verifiedEmail) { + return; + } + + const rolesThatNeedChanges = user.roles.filter((role) => rolesToChangeTo.has(role)); + + await Promise.all( + rolesThatNeedChanges.map(async (role) => { + const rolesToAdd = rolesToChangeTo.get(role); + if (rolesToAdd) { + await addUserRolesAsync(userId, rolesToAdd); + } + await removeUserFromRolesAsync(user._id, [role]); + }), + ); +}; diff --git a/apps/meteor/server/methods/afterVerifyEmail.ts b/apps/meteor/server/methods/afterVerifyEmail.ts index 9e1589c1895a8..f8e5cdb16babe 100644 --- a/apps/meteor/server/methods/afterVerifyEmail.ts +++ b/apps/meteor/server/methods/afterVerifyEmail.ts @@ -1,12 +1,8 @@ -import type { IRole } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { addUserRolesAsync } from '../lib/roles/addUserRoles'; -import { removeUserFromRolesAsync } from '../lib/roles/removeUserFromRoles'; - -const rolesToChangeTo: Map = new Map([['anonymous', ['user']]]); +import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger'; +import { runAfterVerifyEmail } from '../lib/users/runAfterVerifyEmail'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -17,6 +13,8 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async afterVerifyEmail() { + methodDeprecationLogger.method('afterVerifyEmail', '9.0.0', '/v1/users.verifyEmail'); + const userId = Meteor.userId(); if (!userId) { @@ -25,23 +23,6 @@ Meteor.methods({ }); } - const user = await Users.findOneById(userId); - if (user?.emails && Array.isArray(user.emails)) { - const verifiedEmail = user.emails.find((email) => email.verified); - - const rolesThatNeedChanges = user.roles.filter((role) => rolesToChangeTo.has(role)); - - if (verifiedEmail) { - await Promise.all( - rolesThatNeedChanges.map(async (role) => { - const rolesToAdd = rolesToChangeTo.get(role); - if (rolesToAdd) { - await addUserRolesAsync(userId, rolesToAdd); - } - await removeUserFromRolesAsync(user._id, [role]); - }), - ); - } - } + await runAfterVerifyEmail(userId); }, }); diff --git a/apps/meteor/server/methods/logoutCleanUp.ts b/apps/meteor/server/methods/logoutCleanUp.ts index 396311161d7f7..02c3b4ffc0226 100644 --- a/apps/meteor/server/methods/logoutCleanUp.ts +++ b/apps/meteor/server/methods/logoutCleanUp.ts @@ -1,10 +1,10 @@ -import { AppEvents, Apps } from '@rocket.chat/apps'; import type { IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; -import { afterLogoutCleanUpCallback } from '../lib/callbacks/afterLogoutCleanUpCallback'; +import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger'; +import { runUserLogoutCleanUp } from '../hooks/userLogoutCleanUp'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -15,13 +15,9 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async logoutCleanUp(user) { + methodDeprecationLogger.method('logoutCleanUp', '9.0.0', '/v1/users.logout'); check(user, Object); - setImmediate(() => { - void afterLogoutCleanUpCallback.run(user); - }); - - // App IPostUserLogout event hook - await Apps.self?.triggerEvent(AppEvents.IPostUserLoggedOut, user); + await runUserLogoutCleanUp(user); }, }); diff --git a/apps/meteor/server/methods/setAvatarFromService.ts b/apps/meteor/server/methods/setAvatarFromService.ts index b59100e4a6270..8a3319d991fa2 100644 --- a/apps/meteor/server/methods/setAvatarFromService.ts +++ b/apps/meteor/server/methods/setAvatarFromService.ts @@ -4,6 +4,7 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { Meteor } from 'meteor/meteor'; import { setAvatarFromServiceWithValidation } from '../../app/lib/server/functions/setUserAvatar'; +import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -14,6 +15,8 @@ declare module '@rocket.chat/ddp-client' { Meteor.methods({ async setAvatarFromService(dataURI, contentType, service, targetUserId) { + methodDeprecationLogger.method('setAvatarFromService', '9.0.0', '/v1/users.setAvatar'); + check(dataURI, String); check(contentType, Match.Optional(String)); check(service, Match.Optional(String)); diff --git a/packages/rest-typings/src/v1/cloud.ts b/packages/rest-typings/src/v1/cloud.ts index 435beb8739e3c..45d4716b1bdf6 100644 --- a/packages/rest-typings/src/v1/cloud.ts +++ b/packages/rest-typings/src/v1/cloud.ts @@ -59,6 +59,22 @@ const CloudConfirmationPollSchema = { export const isCloudConfirmationPollProps = ajvQuery.compile(CloudConfirmationPollSchema); +type CloudConnectWorkspace = { token: string }; + +const CloudConnectWorkspaceSchema = { + type: 'object', + properties: { + token: { + type: 'string', + minLength: 1, + }, + }, + required: ['token'], + additionalProperties: false, +}; + +export const isCloudConnectWorkspaceProps = ajv.compile(CloudConnectWorkspaceSchema); + export type CloudEndpoints = { '/v1/cloud.manualRegister': { POST: (params: CloudManualRegister) => void; @@ -87,4 +103,7 @@ export type CloudEndpoints = { '/v1/cloud.removeLicense': { POST: () => { success: boolean }; }; + '/v1/cloud.connectWorkspace': { + POST: (params: CloudConnectWorkspace) => { success: boolean }; + }; }; diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsClearHistoryProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsClearHistoryProps.ts new file mode 100644 index 0000000000000..7c6fd3a91db52 --- /dev/null +++ b/packages/rest-typings/src/v1/integrations/IntegrationsClearHistoryProps.ts @@ -0,0 +1,16 @@ +import { ajv } from '../Ajv'; + +export type IntegrationsClearHistoryProps = { + integrationId: string; +}; + +const IntegrationsClearHistorySchema = { + type: 'object', + properties: { + integrationId: { type: 'string', minLength: 1 }, + }, + required: ['integrationId'], + additionalProperties: false, +}; + +export const isIntegrationsClearHistoryProps = ajv.compile(IntegrationsClearHistorySchema); diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsReplayProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsReplayProps.ts new file mode 100644 index 0000000000000..a4715e894fc29 --- /dev/null +++ b/packages/rest-typings/src/v1/integrations/IntegrationsReplayProps.ts @@ -0,0 +1,18 @@ +import { ajv } from '../Ajv'; + +export type IntegrationsReplayProps = { + integrationId: string; + historyId: string; +}; + +const IntegrationsReplaySchema = { + type: 'object', + properties: { + integrationId: { type: 'string', minLength: 1 }, + historyId: { type: 'string', minLength: 1 }, + }, + required: ['integrationId', 'historyId'], + additionalProperties: false, +}; + +export const isIntegrationsReplayProps = ajv.compile(IntegrationsReplaySchema); diff --git a/packages/rest-typings/src/v1/integrations/index.ts b/packages/rest-typings/src/v1/integrations/index.ts index 2e63fcc78b1c2..d6201606147cc 100644 --- a/packages/rest-typings/src/v1/integrations/index.ts +++ b/packages/rest-typings/src/v1/integrations/index.ts @@ -1,8 +1,10 @@ export type * from './integrations'; +export * from './IntegrationsClearHistoryProps'; export * from './IntegrationsCreateProps'; export * from './IntegrationsHistoryProps'; export * from './IntegrationsRemoveProps'; +export * from './IntegrationsReplayProps'; export * from './IntegrationsGetProps'; export * from './IntegrationsUpdateProps'; export * from './IntegrationsListProps'; diff --git a/packages/rest-typings/src/v1/integrations/integrations.ts b/packages/rest-typings/src/v1/integrations/integrations.ts index c2833f20bdbe0..24c9355ca38a4 100644 --- a/packages/rest-typings/src/v1/integrations/integrations.ts +++ b/packages/rest-typings/src/v1/integrations/integrations.ts @@ -1,10 +1,12 @@ import type { IIntegration, IIntegrationHistory } from '@rocket.chat/core-typings'; +import type { IntegrationsClearHistoryProps } from './IntegrationsClearHistoryProps'; import type { IntegrationsCreateProps } from './IntegrationsCreateProps'; import type { IntegrationsGetProps } from './IntegrationsGetProps'; import type { IntegrationsHistoryProps } from './IntegrationsHistoryProps'; import type { IntegrationsListProps } from './IntegrationsListProps'; import type { IntegrationsRemoveProps } from './IntegrationsRemoveProps'; +import type { IntegrationsReplayProps } from './IntegrationsReplayProps'; import type { IntegrationsUpdateProps } from './IntegrationsUpdateProps'; import type { PaginatedResult } from '../../helpers/PaginatedResult'; @@ -40,4 +42,12 @@ export type IntegrationsEndpoints = { '/v1/integrations.update': { PUT: (params: IntegrationsUpdateProps) => { integration: IIntegration | null }; }; + + '/v1/integrations.history.clear': { + POST: (params: IntegrationsClearHistoryProps) => void; + }; + + '/v1/integrations.outgoing.replay': { + POST: (params: IntegrationsReplayProps) => void; + }; }; diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 884fe2eeb7735..4c261210dc6eb 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -379,6 +379,10 @@ export type UsersEndpoints = { '/v1/users.deleteOwnAccount': { POST: (params: { password: string; confirmRelinquish?: boolean }) => void; }; + + '/v1/users.verifyEmail': { + POST: (params: { token: string }) => void; + }; }; export * from './users/UserCreateParamsPOST';