diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index 53d0bbbad0cc..921e55449252 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -29,7 +29,8 @@ import './v1/misc'; import './v1/permissions'; import './v1/push'; import './v1/roles'; -import './v1/rooms'; +import './v1/rooms.js'; +import './v1/rooms.ts'; import './v1/settings'; import './v1/stats'; import './v1/subscriptions'; diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts new file mode 100644 index 000000000000..586a139dc36c --- /dev/null +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor'; +import Ajv from 'ajv'; + +import { API } from '../api'; + +// TO-DO: Replace this instance by only one Ajv import +const ajv = new Ajv({ coerceTypes: true }); + +type GETRoomsNameExists = { + roomName: string; +}; + +const GETRoomsNameExistsSchema = { + type: 'object', + properties: { + roomName: { + type: 'string', + }, + }, + required: ['roomName'], + additionalProperties: false, +}; + +export const isGETRoomsNameExists = ajv.compile(GETRoomsNameExistsSchema); + +API.v1.addRoute( + 'rooms.nameExists', + { + authRequired: true, + validateParams: isGETRoomsNameExists, + }, + { + get() { + const { roomName } = this.queryParams; + + return API.v1.success({ exists: Meteor.call('roomNameExists', roomName) }); + }, + }, +); diff --git a/apps/meteor/client/sidebar/header/CreateChannel.tsx b/apps/meteor/client/sidebar/header/CreateChannel.tsx index c0d356ec6df4..508d3f2d0bed 100644 --- a/apps/meteor/client/sidebar/header/CreateChannel.tsx +++ b/apps/meteor/client/sidebar/header/CreateChannel.tsx @@ -1,6 +1,6 @@ import { Box, Modal, ButtonGroup, Button, TextInput, Icon, Field, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetting, useMethod, useTranslation, TranslationKey } from '@rocket.chat/ui-contexts'; +import { useSetting, useTranslation, TranslationKey, useEndpoint } from '@rocket.chat/ui-contexts'; import React, { ReactElement, useEffect, useMemo, useState } from 'react'; import { useHasLicenseModule } from '../../../ee/client/hooks/useHasLicenseModule'; @@ -61,7 +61,7 @@ const CreateChannel = ({ const namesValidation = useSetting('UTF8_Channel_Names_Validation'); const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); const federationEnabled = useSetting('Federation_Matrix_enabled'); - const channelNameExists = useMethod('roomNameExists'); + const channelNameExists = useEndpoint('GET', '/v1/rooms.nameExists'); const channelNameRegex = useMemo(() => new RegExp(`^${namesValidation}$`), [namesValidation]); @@ -83,8 +83,9 @@ const CreateChannel = ({ if (!allowSpecialNames && !channelNameRegex.test(name)) { return setNameError(t('error-invalid-name')); } - const isNotAvailable = await channelNameExists(name); - if (isNotAvailable) { + const { exists } = await channelNameExists({ roomName: name }); + + if (exists) { return setNameError(t('Channel_already_exist', name)); } }, diff --git a/apps/meteor/client/views/teams/CreateTeamModal/useCreateTeamModalState.ts b/apps/meteor/client/views/teams/CreateTeamModal/useCreateTeamModalState.ts index bd84a2173b44..d2ebce384f13 100644 --- a/apps/meteor/client/views/teams/CreateTeamModal/useCreateTeamModalState.ts +++ b/apps/meteor/client/views/teams/CreateTeamModal/useCreateTeamModalState.ts @@ -1,6 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import { useMutableCallback, useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; -import { useSetting, usePermission, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import { useSetting, usePermission, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useEndpointActionExperimental } from '../../../hooks/useEndpointActionExperimental'; @@ -70,7 +70,8 @@ export const useCreateTeamModalState = (onClose: () => void): CreateTeamModalSta const [nameError, setNameError] = useState(); - const teamNameExists = useMethod('roomNameExists'); + const teamNameExists = useEndpoint('GET', '/v1/rooms.nameExists'); + // const teamNameExists = useMethod('roomNameExists'); const checkName = useDebouncedCallback( async (name: string) => { @@ -90,7 +91,7 @@ export const useCreateTeamModalState = (onClose: () => void): CreateTeamModalSta return; } - const isNotAvailable = await teamNameExists(name); + const isNotAvailable = await teamNameExists({ roomName: name }); if (isNotAvailable) { setNameError(t('Teams_Errors_team_name', { name })); } diff --git a/apps/meteor/server/methods/roomNameExists.js b/apps/meteor/server/methods/roomNameExists.js deleted file mode 100644 index f0e1f3cec39c..000000000000 --- a/apps/meteor/server/methods/roomNameExists.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { Rooms } from '../../app/models/server'; - -Meteor.methods({ - roomNameExists(rid) { - check(rid, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'roomExists', - }); - } - const room = Rooms.findOneByName(rid); - return !!room; - }, -}); diff --git a/apps/meteor/server/methods/roomNameExists.ts b/apps/meteor/server/methods/roomNameExists.ts new file mode 100644 index 000000000000..01885be7760f --- /dev/null +++ b/apps/meteor/server/methods/roomNameExists.ts @@ -0,0 +1,26 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +import { Rooms } from '../../app/models/server'; +import { methodDeprecationLogger } from '../../app/lib/server/lib/deprecationWarningLogger'; + +Meteor.methods({ + roomNameExists(roomName) { + check(roomName, String); + + methodDeprecationLogger.warn('roomNameExists will be deprecated in future versions of Rocket.Chat'); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'roomExists', + }); + } + const room = Rooms.findOneByName(roomName); + + if (room === undefined || room === null) { + return false; + } + + 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 d833fb058cd0..4ea76319a8bd 100644 --- a/apps/meteor/tests/end-to-end/api/09-rooms.js +++ b/apps/meteor/tests/end-to-end/api/09-rooms.js @@ -226,6 +226,58 @@ describe('[Rooms]', function () { }); }); + describe('/rooms.nameExists', () => { + it('should return 401 unauthorized when user is not logged in', (done) => { + request + .get(api('rooms.nameExists')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res) => { + expect(res.body).to.have.property('message'); + }) + .end(done); + }); + + // eslint-disable-next-line no-unused-vars + let testChannel; + const testChannelName = `channel.test.${Date.now()}-${Math.random()}`; + it('create an channel', (done) => { + createRoom({ type: 'c', name: testChannelName }).end((err, res) => { + testChannel = res.body.channel; + done(); + }); + }); + it('should return true if this room name exists', (done) => { + request + .get(api('rooms.nameExists')) + .set(credentials) + .query({ + roomName: testChannelName, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('exists', true); + }) + .end(done); + }); + + it('should return an error when send an invalid room', (done) => { + request + .get(api('rooms.nameExists')) + .set(credentials) + .query({ + roomId: 'foo', + }) + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + }) + .end(done); + }); + }); + describe('[/rooms.cleanHistory]', () => { let publicChannel; let privateChannel; diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index d9305830059a..ad9f9d7dd86c 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -393,21 +393,25 @@ export type RoomsEndpoints = { items: IRoom[]; }; }; + '/v1/rooms.autocomplete.channelAndPrivate.withPagination': { GET: (params: RoomsAutocompleteChannelAndPrivateWithPaginationProps) => PaginatedResult<{ items: IRoom[]; }>; }; + '/v1/rooms.autocomplete.availableForTeams': { GET: (params: RoomsAutocompleteAvailableForTeamsProps) => { items: IRoom[]; }; }; + '/v1/rooms.info': { GET: (params: RoomsInfoProps) => { room: IRoom; }; }; + '/v1/rooms.cleanHistory': { POST: (params: { roomId: IRoom['_id']; @@ -422,34 +426,41 @@ export type RoomsEndpoints = { ignoreThreads?: boolean; }) => { _id: IRoom['_id']; count: number; success: boolean }; }; + '/v1/rooms.createDiscussion': { POST: (params: RoomsCreateDiscussionProps) => { discussion: IRoom; }; }; + '/v1/rooms.export': { POST: (params: RoomsExportProps) => { missing?: []; success: boolean; }; }; + '/v1/rooms.adminRooms': { GET: (params: RoomsAdminRoomsProps) => PaginatedResult<{ rooms: Pick[] }>; }; + '/v1/rooms.adminRooms.getRoom': { GET: (params: RoomsAdminRoomsGetRoomProps) => Pick; }; + '/v1/rooms.saveRoomSettings': { POST: (params: RoomsSaveRoomSettingsProps) => { success: boolean; rid: string; }; }; + '/v1/rooms.changeArchivationState': { POST: (params: RoomsChangeArchivationStateProps) => { success: boolean; }; }; + '/v1/rooms.upload/:rid': { POST: (params: { file: File; @@ -462,6 +473,7 @@ export type RoomsEndpoints = { tmid?: string; }) => { message: IMessage }; }; + '/v1/rooms.saveNotification': { POST: (params: { roomId: string; @@ -478,4 +490,24 @@ export type RoomsEndpoints = { success: boolean; }; }; + + '/v1/rooms.favorite': { + POST: ( + params: + | { + roomId: string; + favorite: boolean; + } + | { + roomName: string; + favorite: boolean; + }, + ) => void; + }; + + '/v1/rooms.nameExists': { + GET: (params: { roomName: string }) => { + exists: boolean; + }; + }; };