Skip to content

Commit f4f3612

Browse files
feat: custom-sounds.delete endpoint (#40532)
1 parent 1d94933 commit f4f3612

8 files changed

Lines changed: 191 additions & 35 deletions

File tree

.changeset/neat-planets-hope.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/rest-typings': minor
3+
'@rocket.chat/meteor': minor
4+
---
5+
6+
Adds custom-sounds.delete API endpoint.

apps/meteor/app/api/server/v1/custom-sounds.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ import {
55
isCustomSoundsGetOneProps,
66
isCustomSoundsListProps,
77
isCustomSoundsCreateProps,
8+
isCustomSoundsDeleteProps,
89
isCustomSoundsUpdateProps,
910
ajv,
1011
validateBadRequestErrorResponse,
1112
validateNotFoundErrorResponse,
1213
validateForbiddenErrorResponse,
1314
validateUnauthorizedErrorResponse,
15+
validateInternalErrorResponse,
1416
} from '@rocket.chat/rest-typings';
1517
import { escapeRegExp } from '@rocket.chat/string-helpers';
1618

1719
import { MAX_CUSTOM_SOUND_SIZE_BYTES, CUSTOM_SOUND_ALLOWED_MIME_TYPES } from '../../../../lib/constants';
1820
import { SystemLogger } from '../../../../server/lib/logger/system';
21+
import { deleteCustomSound } from '../../../custom-sounds/server/lib/deleteCustomSound';
1922
import { insertOrUpdateSound } from '../../../custom-sounds/server/lib/insertOrUpdateSound';
2023
import { uploadCustomSound } from '../../../custom-sounds/server/lib/uploadCustomSound';
2124
import { getExtension, getMimeTypeFromFileName } from '../../../utils/lib/mimeTypes';
@@ -58,6 +61,18 @@ const updateCustomSoundsResponse = ajv.compile<{ success: boolean }>({
5861
required: ['success'],
5962
});
6063

64+
const deleteCustomSoundsResponse = ajv.compile<void>({
65+
additionalProperties: false,
66+
type: 'object',
67+
properties: {
68+
success: {
69+
type: 'boolean',
70+
description: 'Indicates if the request was successful.',
71+
},
72+
},
73+
required: ['success'],
74+
});
75+
6176
const customSoundsEndpoints = API.v1
6277
.get(
6378
'custom-sounds.list',
@@ -280,6 +295,39 @@ const customSoundsEndpoints = API.v1
280295
return API.v1.failure(error instanceof Error ? error.message : 'Unknown error');
281296
}
282297
},
298+
)
299+
.post(
300+
'custom-sounds.delete',
301+
{
302+
response: {
303+
200: deleteCustomSoundsResponse,
304+
400: validateBadRequestErrorResponse,
305+
401: validateUnauthorizedErrorResponse,
306+
403: validateForbiddenErrorResponse,
307+
404: validateNotFoundErrorResponse,
308+
500: validateInternalErrorResponse,
309+
},
310+
authRequired: true,
311+
body: isCustomSoundsDeleteProps,
312+
permissionsRequired: ['manage-sounds'],
313+
},
314+
async function action() {
315+
const { _id } = this.bodyParams;
316+
317+
try {
318+
await deleteCustomSound(_id);
319+
320+
return API.v1.success();
321+
} catch (error: unknown) {
322+
this.logger.error({ error });
323+
324+
if (error instanceof Meteor.Error && error.error === 'Custom_Sound_Error_Invalid_Sound') {
325+
return API.v1.failure(error.error);
326+
}
327+
328+
return API.v1.internalError();
329+
}
330+
},
283331
);
284332

285333
export type CustomSoundEndpoints = ExtractRoutesFromAPI<typeof customSoundsEndpoints>;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { api } from '@rocket.chat/core-services';
2+
import { CustomSounds } from '@rocket.chat/models';
3+
import { Meteor } from 'meteor/meteor';
4+
5+
import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds';
6+
7+
export const deleteCustomSound = async (_id: string): Promise<void> => {
8+
const sound = await CustomSounds.findOneById(_id);
9+
10+
if (!sound) {
11+
throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', {
12+
method: 'deleteCustomSound',
13+
});
14+
}
15+
16+
await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`);
17+
await CustomSounds.removeById(_id);
18+
19+
void api.broadcast('notify.deleteCustomSound', { soundData: sound });
20+
};
Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { api } from '@rocket.chat/core-services';
21
import type { ICustomSound } from '@rocket.chat/core-typings';
32
import type { ServerMethods } from '@rocket.chat/ddp-client';
4-
import { CustomSounds } from '@rocket.chat/models';
3+
import { check } from 'meteor/check';
54
import { Meteor } from 'meteor/meteor';
65

76
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
8-
import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds';
7+
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
8+
import { deleteCustomSound } from '../lib/deleteCustomSound';
99

1010
declare module '@rocket.chat/ddp-client' {
1111
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -16,24 +16,12 @@ declare module '@rocket.chat/ddp-client' {
1616

1717
Meteor.methods<ServerMethods>({
1818
async deleteCustomSound(_id) {
19-
let sound = null;
20-
21-
if (this.userId && (await hasPermissionAsync(this.userId, 'manage-sounds'))) {
22-
sound = await CustomSounds.findOneById(_id);
23-
} else {
19+
methodDeprecationLogger.method('deleteCustomSound', '9.0.0', '/v1/custom-sounds.delete');
20+
if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) {
2421
throw new Meteor.Error('not_authorized');
2522
}
26-
27-
if (sound == null) {
28-
throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', {
29-
method: 'deleteCustomSound',
30-
});
31-
}
32-
33-
await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`);
34-
await CustomSounds.removeById(_id);
35-
void api.broadcast('notify.deleteCustomSound', { soundData: sound });
36-
23+
check(_id, String);
24+
await deleteCustomSound(_id);
3725
return true;
3826
},
3927
});

apps/meteor/client/views/admin/customSounds/EditSound.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldRow, IconButton } from '@rocket.chat/fuselage';
22
import { GenericModal, ContextualbarScrollableContent, ContextualbarFooter } from '@rocket.chat/ui-client';
3-
import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts';
3+
import { useSetModal, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
44
import fileSize from 'filesize';
55
import type { ReactElement, SyntheticEvent } from 'react';
66
import { useCallback, useState, useMemo, useEffect } from 'react';
@@ -36,7 +36,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl
3636
setFile(undefined);
3737
}, [_id, previousName]);
3838

39-
const deleteCustomSound = useMethod('deleteCustomSound');
39+
const deleteCustomSoundEndpoint = useEndpoint('POST', '/v1/custom-sounds.delete');
4040

4141
const { mutate: saveAction } = useEndpointUploadMutation('/v1/custom-sounds.update', {
4242
onSuccess: () => {
@@ -76,7 +76,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl
7676
const handleDeleteButtonClick = useCallback(() => {
7777
const handleDelete = async (): Promise<void> => {
7878
try {
79-
await deleteCustomSound(_id);
79+
await deleteCustomSoundEndpoint({ _id });
8080
dispatchToastMessage({ type: 'success', message: t('Custom_Sound_Has_Been_Deleted') });
8181
} catch (error) {
8282
dispatchToastMessage({ type: 'error', message: error });
@@ -94,7 +94,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl
9494
{t('Custom_Sound_Delete_Warning')}
9595
</GenericModal>,
9696
);
97-
}, [_id, close, deleteCustomSound, dispatchToastMessage, onChange, setModal, t]);
97+
}, [_id, close, deleteCustomSoundEndpoint, dispatchToastMessage, onChange, setModal, t]);
9898

9999
const [clickUpload] = useSingleFileInput(
100100
handleChangeFile,

apps/meteor/tests/end-to-end/api/custom-sounds.ts

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,7 @@ async function createCustomSound(fileName: string, filePath: string): Promise<st
3030
}
3131

3232
async function deleteCustomSound(_id: string) {
33-
await request
34-
.post(api('method.call/deleteCustomSound'))
35-
.set(credentials)
36-
.send({
37-
message: JSON.stringify({
38-
msg: 'method',
39-
id: '1',
40-
method: 'deleteCustomSound',
41-
params: [_id],
42-
}),
43-
})
44-
.expect(200);
33+
await request.post(api('custom-sounds.delete')).set(credentials).send({ _id }).expect(200);
4534
}
4635

4736
describe('[CustomSounds]', () => {
@@ -415,6 +404,78 @@ describe('[CustomSounds]', () => {
415404
});
416405
});
417406

407+
describe('[/custom-sounds.delete]', () => {
408+
let soundToDeleteId: string;
409+
let soundDeleted: boolean = false;
410+
411+
before(async () => {
412+
soundToDeleteId = await createCustomSound(`sound-to-delete-${randomUUID()}`, mockWavAudioPath);
413+
});
414+
415+
after(async () => {
416+
if (soundToDeleteId && !soundDeleted) {
417+
await deleteCustomSound(soundToDeleteId);
418+
}
419+
});
420+
421+
it('should return unauthorized if the user is not authenticated', async () => {
422+
await request.post(api('custom-sounds.delete')).send({ _id: soundToDeleteId }).expect(401);
423+
});
424+
425+
it('should return a 400 if attempting to delete a sound that does not exist', async () => {
426+
await request
427+
.post(api('custom-sounds.delete'))
428+
.set(credentials)
429+
.send({ _id: 'invalid-non-existent-id' })
430+
.expect(400)
431+
.expect((res) => {
432+
expect(res.body).to.have.property('success', false);
433+
expect(res.body.error).to.equal('Custom_Sound_Error_Invalid_Sound');
434+
});
435+
});
436+
437+
it('should reject requests with invalid parameter types', async () => {
438+
await request
439+
.post(api('custom-sounds.delete'))
440+
.set(credentials)
441+
.send({ _id: { $ne: null } })
442+
.expect(400)
443+
.expect((res) => {
444+
expect(res.body).to.have.property('success', false);
445+
});
446+
});
447+
448+
describe('without manage-sounds permission', async () => {
449+
let unauthorizedUser: IUser;
450+
let unauthorizedUserCredentials: Credentials;
451+
452+
before(async () => {
453+
unauthorizedUser = await createUser();
454+
unauthorizedUserCredentials = await login(unauthorizedUser.username, password);
455+
});
456+
457+
after(async () => {
458+
await deleteUser(unauthorizedUser);
459+
});
460+
461+
it('should return forbidden if user does not have the manage-sounds permission', async () => {
462+
await request.post(api('custom-sounds.delete')).set(unauthorizedUserCredentials).send({ _id: soundToDeleteId }).expect(403);
463+
});
464+
});
465+
466+
it('should successfully delete a custom sound when providing a valid _id', async () => {
467+
await request
468+
.post(api('custom-sounds.delete'))
469+
.set(credentials)
470+
.send({ _id: soundToDeleteId })
471+
.expect(200)
472+
.expect((res) => {
473+
expect(res.body).to.have.property('success', true);
474+
});
475+
soundDeleted = true;
476+
});
477+
});
478+
418479
describe('Accessing custom sounds', () => {
419480
it('should return forbidden if the there is no fileId on the url', (done) => {
420481
void request

packages/rest-typings/src/v1/Ajv.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,20 @@ const NotFoundErrorResponseSchema = {
126126
};
127127

128128
export const validateNotFoundErrorResponse = ajv.compile<NotFoundErrorResponse>(NotFoundErrorResponseSchema);
129+
130+
type InternalErrorResponse = {
131+
success: false;
132+
error: string;
133+
};
134+
135+
const InternalErrorResponseSchema = {
136+
type: 'object',
137+
properties: {
138+
success: { type: 'boolean', enum: [false] },
139+
error: { type: 'string' },
140+
},
141+
required: ['success', 'error'],
142+
additionalProperties: false,
143+
};
144+
145+
export const validateInternalErrorResponse = ajv.compile<InternalErrorResponse>(InternalErrorResponseSchema);

packages/rest-typings/src/v1/customSounds.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,19 @@ const CustomSoundsUpdateSchema = {
8686
};
8787

8888
export const isCustomSoundsUpdateProps = ajv.compile<CustomSoundsUpdate>(CustomSoundsUpdateSchema);
89+
90+
type CustomSoundsDelete = { _id: ICustomSound['_id'] };
91+
92+
const CustomSoundsDeleteSchema = {
93+
type: 'object',
94+
properties: {
95+
_id: {
96+
type: 'string',
97+
minLength: 1,
98+
},
99+
},
100+
required: ['_id'],
101+
additionalProperties: false,
102+
};
103+
104+
export const isCustomSoundsDeleteProps = ajv.compile<CustomSoundsDelete>(CustomSoundsDeleteSchema);

0 commit comments

Comments
 (0)