diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 586a139dc36cd..5b920c8a77063 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import Ajv from 'ajv'; import { API } from '../api'; +import { eraseRoom } from '../../../../server/methods/eraseRoom'; // TO-DO: Replace this instance by only one Ajv import const ajv = new Ajv({ coerceTypes: true }); @@ -37,3 +38,23 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'rooms.delete', + { + authRequired: true, + }, + { + async post() { + const { roomId } = this.bodyParams; + + if (!roomId) { + return API.v1.failure("The 'roomId' param is required"); + } + + await eraseRoom(roomId, this.userId); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/client/views/admin/rooms/EditRoom.tsx b/apps/meteor/client/views/admin/rooms/EditRoom.tsx index da0b8e02ea42b..61fb56bb55cf3 100644 --- a/apps/meteor/client/views/admin/rooms/EditRoom.tsx +++ b/apps/meteor/client/views/admin/rooms/EditRoom.tsx @@ -1,15 +1,7 @@ import { IRoom, RoomAdminFieldsType } from '@rocket.chat/core-typings'; import { Box, Button, ButtonGroup, TextInput, Field, ToggleSwitch, Icon, TextAreaInput } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { - useSetModal, - useToastMessageDispatch, - useRoute, - usePermission, - useEndpoint, - useMethod, - useTranslation, -} from '@rocket.chat/ui-contexts'; +import { useSetModal, useToastMessageDispatch, useRoute, usePermission, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useState, useMemo, ReactElement } from 'react'; import { RoomSettingsEnum } from '../../../../definition/IRoomTypeConfig'; @@ -148,7 +140,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => handleRoomType(roomType === 'p' ? 'c' : 'p'); }); - const eraseRoom = useMethod('eraseRoom'); + const deleteRoom = useEndpoint('POST', '/v1/rooms.delete'); const deleteTeam = useEndpoint('POST', '/v1/teams.delete'); const handleDelete = useMutableCallback(() => { @@ -186,7 +178,7 @@ const EditRoom = ({ room, onChange, onDelete }: EditRoomProps): ReactElement => try { setDeleting(true); setModal(null); - await eraseRoom(room._id); + await deleteRoom({ roomId: room._id }); dispatchToastMessage({ type: 'success', message: t('Room_has_been_deleted') }); roomsRoute.push({}); } catch (error) { diff --git a/apps/meteor/server/methods/eraseRoom.js b/apps/meteor/server/methods/eraseRoom.js deleted file mode 100644 index 162fee597fcf7..0000000000000 --- a/apps/meteor/server/methods/eraseRoom.js +++ /dev/null @@ -1,66 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { deleteRoom } from '../../app/lib'; -import { hasPermission } from '../../app/authorization/server'; -import { Rooms, Messages } from '../../app/models/server'; -import { Apps } from '../../app/apps/server'; -import { roomCoordinator } from '../lib/rooms/roomCoordinator'; -import { Team } from '../sdk'; - -Meteor.methods({ - eraseRoom(rid) { - check(rid, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'eraseRoom', - }); - } - - const room = Rooms.findOneById(rid); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'eraseRoom', - }); - } - - if (room.federated) { - throw new Meteor.Error('error-cannot-delete-federated-room', 'Cannot delete federated room', { - method: 'eraseRoom', - }); - } - - if ( - !roomCoordinator - .getRoomDirectives(room.t) - ?.canBeDeleted((permissionId, rid) => hasPermission(Meteor.userId(), permissionId, rid), room) - ) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'eraseRoom', - }); - } - - if (Apps && Apps.isLoaded()) { - const prevent = Promise.await(Apps.getBridges().getListenerBridge().roomEvent('IPreRoomDeletePrevent', room)); - if (prevent) { - throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the room erasing.'); - } - } - - const result = deleteRoom(rid); - - if (room.teamId) { - const team = Promise.await(Team.getOneById(room.teamId)); - const user = Meteor.user(); - Messages.createUserDeleteRoomFromTeamWithRoomIdAndUser(team.roomId, room.name, user); - } - - if (Apps && Apps.isLoaded()) { - Apps.getBridges().getListenerBridge().roomEvent('IPostRoomDeleted', room); - } - - return result; - }, -}); diff --git a/apps/meteor/server/methods/eraseRoom.ts b/apps/meteor/server/methods/eraseRoom.ts new file mode 100644 index 0000000000000..7610f937a6272 --- /dev/null +++ b/apps/meteor/server/methods/eraseRoom.ts @@ -0,0 +1,72 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger'; +import { deleteRoom } from '../../app/lib/server/functions/deleteRoom'; +import { hasPermission } from '../../app/authorization/server'; +import { Rooms, Messages } from '../../app/models/server'; +import { Apps } from '../../app/apps/server'; +import { roomCoordinator } from '../lib/rooms/roomCoordinator'; +import { Team } from '../sdk'; + +export async function eraseRoom(rid: string, uid: string): Promise { + const room = Rooms.findOneById(rid); + + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'eraseRoom', + }); + } + + if (room.federated) { + throw new Meteor.Error('error-cannot-delete-federated-room', 'Cannot delete federated room', { + method: 'eraseRoom', + }); + } + + if (!roomCoordinator.getRoomDirectives(room.t)?.canBeDeleted((permissionId, rid) => hasPermission(uid, permissionId, rid), room)) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'eraseRoom', + }); + } + + if (Apps?.isLoaded()) { + const prevent = Promise.await(Apps.getBridges()?.getListenerBridge().roomEvent('IPreRoomDeletePrevent', room)); + if (prevent) { + throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the room erasing.'); + } + } + + deleteRoom(rid); + + const team = room.teamId && (await Team.getOneById(room.teamId)); + + if (team) { + const user = Meteor.user(); + Messages.createUserDeleteRoomFromTeamWithRoomIdAndUser(team.roomId, room.name, user); + } + + if (Apps?.isLoaded()) { + Apps.getBridges()?.getListenerBridge().roomEvent('IPostRoomDeleted', room); + } +} + +Meteor.methods({ + async eraseRoom(rid: string) { + methodDeprecationLogger.warn('eraseRoom is deprecated and will be removed in future versions of Rocket.Chat'); + + check(rid, String); + + const uid = Meteor.userId(); + + if (!uid) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'eraseRoom', + }); + } + + await eraseRoom(rid, uid); + + return true; + }, +}); diff --git a/apps/meteor/tests/end-to-end/api/09-rooms.js b/apps/meteor/tests/end-to-end/api/09-rooms.js index 09bd1aa5037e7..29f5203c98a8f 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -1300,4 +1300,49 @@ describe('[Rooms]', function () { }); }); }); + + describe('/rooms.delete', () => { + let testChannel; + before('create an channel', async () => { + const result = await createRoom({ type: 'c', name: `channel.test.${Date.now()}-${Math.random()}` }); + testChannel = result.body.channel; + }); + it('should throw an error when roomId is not provided', (done) => { + request + .post(api('rooms.delete')) + .set(credentials) + .send({}) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', "The 'roomId' param is required"); + }) + .end(done); + }); + it('should delete a room when the request is correct', (done) => { + request + .post(api('rooms.delete')) + .set(credentials) + .send({ roomId: testChannel._id }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }) + .end(done); + }); + it('should throw an error when the room id doesn exist', (done) => { + request + .post(api('rooms.delete')) + .set(credentials) + .send({ roomId: 'invalid' }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + }) + .end(done); + }); + }); }); diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 358c4bc7035ed..e070248597075 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -490,6 +490,7 @@ export type RoomsEndpoints = { success: boolean; }; }; + '/v1/rooms.favorite': { POST: ( params: @@ -503,9 +504,14 @@ export type RoomsEndpoints = { }, ) => void; }; + '/v1/rooms.nameExists': { GET: (params: { roomName: string }) => { exists: boolean; }; }; + + '/v1/rooms.delete': { + POST: (params: { roomId: string }) => void; + }; }; diff --git a/packages/rest-typings/src/v1/teams/TeamsAddMembersProps.ts b/packages/rest-typings/src/v1/teams/TeamsAddMembersProps.ts index b6dbdbf48072c..ea412998309d7 100644 --- a/packages/rest-typings/src/v1/teams/TeamsAddMembersProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsAddMembersProps.ts @@ -1,4 +1,3 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; import type { ITeamMemberParams } from './ITeamMemberParams'; @@ -8,7 +7,7 @@ const ajv = new Ajv(); export type TeamsAddMembersProps = ({ teamId: string } | { teamName: string }) & { members: ITeamMemberParams[]; }; -const teamsAddMembersPropsSchema: JSONSchemaType = { +const teamsAddMembersPropsSchema = { oneOf: [ { type: 'object', @@ -77,4 +76,4 @@ const teamsAddMembersPropsSchema: JSONSchemaType = { ], }; -export const isTeamsAddMembersProps = ajv.compile(teamsAddMembersPropsSchema); +export const isTeamsAddMembersProps = ajv.compile(teamsAddMembersPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/TeamsConvertToChannelProps.ts b/packages/rest-typings/src/v1/teams/TeamsConvertToChannelProps.ts index 0b36167b2c671..79004f0a89a9c 100644 --- a/packages/rest-typings/src/v1/teams/TeamsConvertToChannelProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsConvertToChannelProps.ts @@ -1,4 +1,3 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; const ajv = new Ajv(); @@ -7,7 +6,7 @@ export type TeamsConvertToChannelProps = { roomsToRemove?: string[]; } & ({ teamId: string } | { teamName: string }); -const teamsConvertToTeamsPropsSchema: JSONSchemaType = { +const teamsConvertToTeamsPropsSchema = { oneOf: [ { type: 'object', @@ -47,4 +46,4 @@ const teamsConvertToTeamsPropsSchema: JSONSchemaType ], }; -export const isTeamsConvertToChannelProps = ajv.compile(teamsConvertToTeamsPropsSchema); +export const isTeamsConvertToChannelProps = ajv.compile(teamsConvertToTeamsPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/TeamsDeleteProps.ts b/packages/rest-typings/src/v1/teams/TeamsDeleteProps.ts index e517056e29a71..8f5be6a0c3b51 100644 --- a/packages/rest-typings/src/v1/teams/TeamsDeleteProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsDeleteProps.ts @@ -1,4 +1,3 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; const ajv = new Ajv(); @@ -7,7 +6,7 @@ export type TeamsDeleteProps = ({ teamId: string } | { teamName: string }) & { roomsToRemove?: string[]; }; -const teamsDeletePropsSchema: JSONSchemaType = { +const teamsDeletePropsSchema = { oneOf: [ { type: 'object', @@ -50,4 +49,4 @@ const teamsDeletePropsSchema: JSONSchemaType = { ], }; -export const isTeamsDeleteProps = ajv.compile(teamsDeletePropsSchema); +export const isTeamsDeleteProps = ajv.compile(teamsDeletePropsSchema); diff --git a/packages/rest-typings/src/v1/teams/TeamsLeaveProps.ts b/packages/rest-typings/src/v1/teams/TeamsLeaveProps.ts index 11880b580b449..7dacb010621db 100644 --- a/packages/rest-typings/src/v1/teams/TeamsLeaveProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsLeaveProps.ts @@ -1,11 +1,10 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; const ajv = new Ajv(); export type TeamsLeaveProps = ({ teamId: string } | { teamName: string }) & { rooms?: string[] }; -const teamsLeavePropsSchema: JSONSchemaType = { +const teamsLeavePropsSchema = { oneOf: [ { type: 'object', @@ -48,4 +47,4 @@ const teamsLeavePropsSchema: JSONSchemaType = { ], }; -export const isTeamsLeaveProps = ajv.compile(teamsLeavePropsSchema); +export const isTeamsLeaveProps = ajv.compile(teamsLeavePropsSchema); diff --git a/packages/rest-typings/src/v1/teams/TeamsRemoveMemberProps.ts b/packages/rest-typings/src/v1/teams/TeamsRemoveMemberProps.ts index 54927ca6ef3aa..44a8e6d64f188 100644 --- a/packages/rest-typings/src/v1/teams/TeamsRemoveMemberProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsRemoveMemberProps.ts @@ -1,4 +1,3 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; const ajv = new Ajv(); @@ -8,7 +7,7 @@ export type TeamsRemoveMemberProps = ({ teamId: string } | { teamName: string }) rooms?: Array; }; -const teamsRemoveMemberPropsSchema: JSONSchemaType = { +const teamsRemoveMemberPropsSchema = { oneOf: [ { type: 'object', @@ -57,4 +56,4 @@ const teamsRemoveMemberPropsSchema: JSONSchemaType = { ], }; -export const isTeamsRemoveMemberProps = ajv.compile(teamsRemoveMemberPropsSchema); +export const isTeamsRemoveMemberProps = ajv.compile(teamsRemoveMemberPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/TeamsRemoveRoomProps.ts b/packages/rest-typings/src/v1/teams/TeamsRemoveRoomProps.ts index 7568025aea1a7..5c1d398b478ad 100644 --- a/packages/rest-typings/src/v1/teams/TeamsRemoveRoomProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsRemoveRoomProps.ts @@ -1,4 +1,3 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; import type { IRoom } from '@rocket.chat/core-typings'; @@ -8,7 +7,7 @@ export type TeamsRemoveRoomProps = ({ teamId: string } | { teamName: string }) & roomId: IRoom['_id']; }; -export const teamsRemoveRoomPropsSchema: JSONSchemaType = { +export const teamsRemoveRoomPropsSchema = { oneOf: [ { type: 'object', @@ -39,4 +38,4 @@ export const teamsRemoveRoomPropsSchema: JSONSchemaType = ], }; -export const isTeamsRemoveRoomProps = ajv.compile(teamsRemoveRoomPropsSchema); +export const isTeamsRemoveRoomProps = ajv.compile(teamsRemoveRoomPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/TeamsUpdateMemberProps.ts b/packages/rest-typings/src/v1/teams/TeamsUpdateMemberProps.ts index eddd5c179725b..c5a1b1b976d95 100644 --- a/packages/rest-typings/src/v1/teams/TeamsUpdateMemberProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsUpdateMemberProps.ts @@ -1,4 +1,3 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; import type { ITeamMemberParams } from './ITeamMemberParams'; @@ -9,7 +8,7 @@ export type TeamsUpdateMemberProps = ({ teamId: string } | { teamName: string }) member: ITeamMemberParams; }; -const teamsUpdateMemberPropsSchema: JSONSchemaType = { +const teamsUpdateMemberPropsSchema = { oneOf: [ { type: 'object', @@ -68,4 +67,4 @@ const teamsUpdateMemberPropsSchema: JSONSchemaType = { ], }; -export const isTeamsUpdateMemberProps = ajv.compile(teamsUpdateMemberPropsSchema); +export const isTeamsUpdateMemberProps = ajv.compile(teamsUpdateMemberPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/TeamsUpdateProps.ts b/packages/rest-typings/src/v1/teams/TeamsUpdateProps.ts index 8d1bea016827b..cc707451c8bb9 100644 --- a/packages/rest-typings/src/v1/teams/TeamsUpdateProps.ts +++ b/packages/rest-typings/src/v1/teams/TeamsUpdateProps.ts @@ -1,4 +1,3 @@ -import type { JSONSchemaType } from 'ajv'; import Ajv from 'ajv'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; @@ -16,7 +15,7 @@ export type TeamsUpdateProps = ({ teamId: string } | { teamName: string }) & { }; }; -const teamsUpdatePropsSchema: JSONSchemaType = { +const teamsUpdatePropsSchema = { type: 'object', properties: { updateRoom: { @@ -71,4 +70,4 @@ const teamsUpdatePropsSchema: JSONSchemaType = { additionalProperties: false, }; -export const isTeamsUpdateProps = ajv.compile(teamsUpdatePropsSchema); +export const isTeamsUpdateProps = ajv.compile(teamsUpdatePropsSchema);