Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/ddp-migrate-batch3-callers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@rocket.chat/meteor': patch
---

Migrate four client DDP callers to their REST equivalents (the DDP methods stay registered on the server for external SDK/mobile clients, with a deprecation log pointing at the REST route until 9.0.0 removes them):

- `deleteCustomSound` → `POST /v1/custom-sounds.delete`
- `blockUser` / `unblockUser` → `POST /v1/im.blockUser` (single toggle with `{ roomId, block: boolean }`)
- `saveSettings` → `POST /v1/settings`
- `e2e.requestSubscriptionKeys` → `POST /v1/e2e.requestSubscriptionKeys`
5 changes: 5 additions & 0 deletions .changeset/loose-knives-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Fixes the behavior when the login token expires to redirect the user to the login page
6 changes: 6 additions & 0 deletions .changeset/rest-e2e-request-subscription-keys.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/e2e.requestSubscriptionKeys` (replaces the deprecated `e2e.requestSubscriptionKeys` DDP method). Auth-gated, no body. Broadcasts `notify.e2e.keyRequest` for every encrypted room the caller is subscribed to without an E2E key, matching the DDP method's behavior. The legacy DDP method remains registered until 9.0.0 with a deprecation log pointing at the new route.
6 changes: 6 additions & 0 deletions .changeset/rest-im-block-user.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/im.blockUser` (replaces the deprecated `blockUser` / `unblockUser` DDP methods). Body is `{ roomId, block: boolean }` — `block: true` blocks the other DM participant, `block: false` unblocks. Auth-gated and per-room via the `RoomMemberActions.BLOCK` directive (DM-only). Both legacy DDP methods remain registered until 9.0.0 with deprecation logs pointing at the new route.
6 changes: 6 additions & 0 deletions .changeset/rest-settings-post.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/settings` for batched admin setting updates (replaces the deprecated `saveSettings` DDP method). Body is `{ settings: { _id, value }[] }`. The endpoint requires authentication, enforces 2FA (`twoFactorRequired: true`), and runs the same per-setting permission chain (`edit-privileged-setting` OR `manage-selected-settings` + per-id permission) and audit/notify side effects the DDP method already performed. The legacy DDP method remains registered until 9.0.0 with a deprecation log pointing at the new route.
19 changes: 19 additions & 0 deletions apps/meteor/app/api/server/v1/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSug
import { provideUsersSuggestedGroupKeys } from '../../../e2e/server/functions/provideUsersSuggestedGroupKeys';
import { resetRoomKey } from '../../../e2e/server/functions/resetRoomKey';
import { getUsersOfRoomWithoutKeyMethod } from '../../../e2e/server/methods/getUsersOfRoomWithoutKey';
import { requestSubscriptionKeysMethod } from '../../../e2e/server/methods/requestSubscriptionKeys';
import { setRoomKeyIDMethod } from '../../../e2e/server/methods/setRoomKeyID';
import { setUserPublicAndPrivateKeysMethod } from '../../../e2e/server/methods/setUserPublicAndPrivateKeys';
import { updateGroupKey } from '../../../e2e/server/methods/updateGroupKey';
Expand Down Expand Up @@ -196,6 +197,24 @@ const e2eEndpoints = API.v1
return API.v1.success();
},
)
.post(
'e2e.requestSubscriptionKeys',
{
authRequired: true,
response: {
401: validateUnauthorizedErrorResponse,
200: ajv.compile<void>({
type: 'object',
}),
},
},

async function action() {
await requestSubscriptionKeysMethod(this.userId);

return API.v1.success();
},
)
.get(
'e2e.fetchMyKeys',
{
Expand Down
42 changes: 41 additions & 1 deletion apps/meteor/app/api/server/v1/im.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
validateUnauthorizedErrorResponse,
validateForbiddenErrorResponse,
validateBadRequestErrorResponse,
isDmBlockUserProps,
isDmFileProps,
isDmMemberProps,
isDmMessagesProps,
Expand All @@ -26,7 +27,9 @@ import { hideRoomMethod } from '../../../../server/methods/hideRoom';
import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { saveRoomSettings } from '../../../channel-settings/server/methods/saveRoomSettings';
import { blockUserMethod } from '../../../lib/server/functions/blockUser';
import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin';
import { unblockUserMethod } from '../../../lib/server/functions/unblockUser';
import { getChannelHistory } from '../../../lib/server/methods/getChannelHistory';
import { settings } from '../../../settings/server';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
Expand Down Expand Up @@ -924,6 +927,42 @@ const dmCreateAction = <Path extends string>(_path: Path): TypedAction<typeof dm
});
};

const dmBlockUserEndpointsProps = {
authRequired: true,
body: isDmBlockUserProps,
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: ajv.compile<void>({
type: 'object',
properties: {
success: { type: 'boolean', enum: [true] },
},
required: ['success'],
additionalProperties: false,
}),
},
} as const;

const dmBlockUserAction = <Path extends string>(_path: Path): TypedAction<typeof dmBlockUserEndpointsProps, Path> =>
async function action() {
const { roomId, block } = this.bodyParams;
const { room } = await findDirectMessageRoom({ roomId }, this.userId);

const blocked = room.uids?.find((uid) => uid !== this.userId);
if (!blocked) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'im.blockUser' });
}

if (block) {
await blockUserMethod(this.userId, { rid: room._id, blocked });
} else {
await unblockUserMethod(this.userId, { rid: room._id, blocked });
}

return API.v1.success();
};

const dmEndpoints = API.v1
.post('im.delete', dmDeleteEndpointsProps, dmDeleteAction('im.delete'))
.post('dm.delete', dmDeleteEndpointsProps, dmDeleteAction('dm.delete'))
Expand All @@ -950,7 +989,8 @@ const dmEndpoints = API.v1
.get('dm.list', dmListEndpointsProps, dmListAction('dm.list'))
.get('im.list', dmListEndpointsProps, dmListAction('im.list'))
.get('dm.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('dm.list.everyone'))
.get('im.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('im.list.everyone'));
.get('im.list.everyone', dmListEveryoneEndpointsProps, dmListEveryoneAction('im.list.everyone'))
.post('im.blockUser', dmBlockUserEndpointsProps, dmBlockUserAction('im.blockUser'));

export type DmEndpoints = ExtractRoutesFromAPI<typeof dmEndpoints>;

Expand Down
28 changes: 28 additions & 0 deletions apps/meteor/app/api/server/v1/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import {
isSettingsUpdatePropsColor,
isSettingsPublicWithPaginationProps,
isSettingsGetParams,
isSettingsBulkProps,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
validateBadRequestErrorResponse,
} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';
import type { FindOptions } from 'mongodb';
Expand All @@ -25,6 +27,7 @@ import _ from 'underscore';
import { updateAuditedByUser } from '../../../../server/settings/lib/auditedSettingUpdates';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { disableCustomScripts } from '../../../lib/server/functions/disableCustomScripts';
import { saveSettingsBulk } from '../../../lib/server/functions/saveSettingsBulk';
import { checkSettingValueBounds } from '../../../lib/server/lib/checkSettingValueBonds';
import { notifyOnSettingChanged, notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListener';
import { addOAuthServiceMethod } from '../../../lib/server/methods/addOAuthService';
Expand Down Expand Up @@ -404,6 +407,31 @@ API.v1.post(
},
);

API.v1.post(
'settings',
{
authRequired: true,
twoFactorRequired: true,
twoFactorOptions: { disableRememberMe: true },
body: isSettingsBulkProps,
response: {
200: settingByIdPostResponseSchema,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
},
},
async function action() {
await saveSettingsBulk(this.userId, this.bodyParams.settings, {
username: this.user.username ?? '',
ip: this.requestIp ?? '',
useragent: this.request.headers.get('user-agent') ?? '',
});

return API.v1.success();
},
);

API.v1.get(
'service.configurations',
{
Expand Down
44 changes: 26 additions & 18 deletions apps/meteor/app/e2e/server/methods/requestSubscriptionKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,48 @@ import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Subscriptions, Rooms } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
'e2e.requestSubscriptionKeys'(): boolean;
}
}

export const requestSubscriptionKeysMethod = async (userId: string): Promise<void> => {
// Get all encrypted rooms that the user is subscribed to and has no E2E key yet
const subscriptions = await Subscriptions.findByUserIdWithoutE2E(userId).toArray();
const roomIds = subscriptions.map((subscription) => subscription.rid);

// For all subscriptions without E2E key, get the rooms that have encryption enabled
const query = {
e2eKeyId: {
$exists: true,
},
_id: {
$in: roomIds,
},
};

const rooms = Rooms.find(query);
await rooms.forEach((room) => {
void api.broadcast('notify.e2e.keyRequest', room._id, room.e2eKeyId);
});
};

Meteor.methods<ServerMethods>({
async 'e2e.requestSubscriptionKeys'() {
methodDeprecationLogger.method('e2e.requestSubscriptionKeys', '9.0.0', '/v1/e2e.requestSubscriptionKeys');

const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'requestSubscriptionKeys',
});
}

// Get all encrypted rooms that the user is subscribed to and has no E2E key yet
const subscriptions = await Subscriptions.findByUserIdWithoutE2E(userId).toArray();
const roomIds = subscriptions.map((subscription) => subscription.rid);

// For all subscriptions without E2E key, get the rooms that have encryption enabled
const query = {
e2eKeyId: {
$exists: true,
},
_id: {
$in: roomIds,
},
};

const rooms = Rooms.find(query);
await rooms.forEach((room) => {
void api.broadcast('notify.e2e.keyRequest', room._id, room.e2eKeyId);
});
await requestSubscriptionKeysMethod(userId);

return true;
},
Expand Down
35 changes: 35 additions & 0 deletions apps/meteor/app/lib/server/functions/blockUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Subscriptions, Rooms } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig';
import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator';
import { notifyOnSubscriptionChangedByRoomIdAndUserIds } from '../lib/notifyListener';

export const blockUserMethod = async (userId: string, { rid, blocked }: { rid: string; blocked: string }): Promise<void> => {
const room = await Rooms.findOne({ _id: rid });

if (!room) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' });
}

if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, userId))) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' });
}

const [blockedUser, blockerUser] = await Promise.all([
Subscriptions.findOneByRoomIdAndUserId(rid, blocked, { projection: { _id: 1 } }),
Subscriptions.findOneByRoomIdAndUserId(rid, userId, { projection: { _id: 1 } }),
]);

if (!blockedUser || !blockerUser) {
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'blockUser' });
}

const [blockedResponse, blockerResponse] = await Subscriptions.setBlockedByRoomId(rid, blocked, userId);

const listenerUsers = [...(blockedResponse?.modifiedCount ? [blocked] : []), ...(blockerResponse?.modifiedCount ? [userId] : [])];

if (listenerUsers.length) {
void notifyOnSubscriptionChangedByRoomIdAndUserIds(rid, listenerUsers);
}
};
Loading
Loading