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
6 changes: 6 additions & 0 deletions .changeset/neat-planets-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Adds custom-sounds.delete API endpoint.
48 changes: 48 additions & 0 deletions apps/meteor/app/api/server/v1/custom-sounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import {
isCustomSoundsGetOneProps,
isCustomSoundsListProps,
isCustomSoundsCreateProps,
isCustomSoundsDeleteProps,
isCustomSoundsUpdateProps,
ajv,
validateBadRequestErrorResponse,
validateNotFoundErrorResponse,
validateForbiddenErrorResponse,
validateUnauthorizedErrorResponse,
validateInternalErrorResponse,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';

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

const deleteCustomSoundsResponse = ajv.compile<void>({
additionalProperties: false,
type: 'object',
properties: {
success: {
type: 'boolean',
description: 'Indicates if the request was successful.',
},
},
required: ['success'],
});

const customSoundsEndpoints = API.v1
.get(
'custom-sounds.list',
Expand Down Expand Up @@ -280,6 +295,39 @@ const customSoundsEndpoints = API.v1
return API.v1.failure(error instanceof Error ? error.message : 'Unknown error');
}
},
)
.post(
'custom-sounds.delete',
{
response: {
200: deleteCustomSoundsResponse,
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
403: validateForbiddenErrorResponse,
404: validateNotFoundErrorResponse,
500: validateInternalErrorResponse,
},
authRequired: true,
body: isCustomSoundsDeleteProps,
permissionsRequired: ['manage-sounds'],
},
async function action() {
const { _id } = this.bodyParams;

try {
await deleteCustomSound(_id);

return API.v1.success();
} catch (error: unknown) {
this.logger.error({ error });

if (error instanceof Meteor.Error && error.error === 'Custom_Sound_Error_Invalid_Sound') {
return API.v1.failure(error.error);
}

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

export type CustomSoundEndpoints = ExtractRoutesFromAPI<typeof customSoundsEndpoints>;
Expand Down
20 changes: 20 additions & 0 deletions apps/meteor/app/custom-sounds/server/lib/deleteCustomSound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { api } from '@rocket.chat/core-services';
import { CustomSounds } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds';

export const deleteCustomSound = async (_id: string): Promise<void> => {
const sound = await CustomSounds.findOneById(_id);

if (!sound) {
throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', {
method: 'deleteCustomSound',
});
}

await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`);
await CustomSounds.removeById(_id);

void api.broadcast('notify.deleteCustomSound', { soundData: sound });
};
26 changes: 7 additions & 19 deletions apps/meteor/app/custom-sounds/server/methods/deleteCustomSound.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { api } from '@rocket.chat/core-services';
import type { ICustomSound } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { CustomSounds } from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

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

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

Meteor.methods<ServerMethods>({
async deleteCustomSound(_id) {
let sound = null;

if (this.userId && (await hasPermissionAsync(this.userId, 'manage-sounds'))) {
sound = await CustomSounds.findOneById(_id);
} else {
methodDeprecationLogger.method('deleteCustomSound', '9.0.0', '/v1/custom-sounds.delete');
if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) {
throw new Meteor.Error('not_authorized');
}

if (sound == null) {
throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', {
method: 'deleteCustomSound',
});
}

await RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`);
await CustomSounds.removeById(_id);
void api.broadcast('notify.deleteCustomSound', { soundData: sound });

check(_id, String);
await deleteCustomSound(_id);
return true;
},
});
8 changes: 4 additions & 4 deletions apps/meteor/client/views/admin/customSounds/EditSound.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldRow, IconButton } from '@rocket.chat/fuselage';
import { GenericModal, ContextualbarScrollableContent, ContextualbarFooter } from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useMethod } from '@rocket.chat/ui-contexts';
import { useSetModal, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
import fileSize from 'filesize';
import type { ReactElement, SyntheticEvent } from 'react';
import { useCallback, useState, useMemo, useEffect } from 'react';
Expand Down Expand Up @@ -36,7 +36,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl
setFile(undefined);
}, [_id, previousName]);

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

const { mutate: saveAction } = useEndpointUploadMutation('/v1/custom-sounds.update', {
onSuccess: () => {
Expand Down Expand Up @@ -76,7 +76,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl
const handleDeleteButtonClick = useCallback(() => {
const handleDelete = async (): Promise<void> => {
try {
await deleteCustomSound(_id);
await deleteCustomSoundEndpoint({ _id });
dispatchToastMessage({ type: 'success', message: t('Custom_Sound_Has_Been_Deleted') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
Expand All @@ -94,7 +94,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl
{t('Custom_Sound_Delete_Warning')}
</GenericModal>,
);
}, [_id, close, deleteCustomSound, dispatchToastMessage, onChange, setModal, t]);
}, [_id, close, deleteCustomSoundEndpoint, dispatchToastMessage, onChange, setModal, t]);

const [clickUpload] = useSingleFileInput(
handleChangeFile,
Expand Down
85 changes: 73 additions & 12 deletions apps/meteor/tests/end-to-end/api/custom-sounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,7 @@ async function createCustomSound(fileName: string, filePath: string): Promise<st
}

async function deleteCustomSound(_id: string) {
await request
.post(api('method.call/deleteCustomSound'))
.set(credentials)
.send({
message: JSON.stringify({
msg: 'method',
id: '1',
method: 'deleteCustomSound',
params: [_id],
}),
})
.expect(200);
await request.post(api('custom-sounds.delete')).set(credentials).send({ _id }).expect(200);
}

describe('[CustomSounds]', () => {
Expand Down Expand Up @@ -415,6 +404,78 @@ describe('[CustomSounds]', () => {
});
});

describe('[/custom-sounds.delete]', () => {
let soundToDeleteId: string;
let soundDeleted: boolean = false;

before(async () => {
soundToDeleteId = await createCustomSound(`sound-to-delete-${randomUUID()}`, mockWavAudioPath);
});

after(async () => {
if (soundToDeleteId && !soundDeleted) {
await deleteCustomSound(soundToDeleteId);
}
});

it('should return unauthorized if the user is not authenticated', async () => {
await request.post(api('custom-sounds.delete')).send({ _id: soundToDeleteId }).expect(401);
});

it('should return a 400 if attempting to delete a sound that does not exist', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: 'invalid-non-existent-id' })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.equal('Custom_Sound_Error_Invalid_Sound');
});
});

it('should reject requests with invalid parameter types', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: { $ne: null } })
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
});
});

describe('without manage-sounds permission', async () => {
let unauthorizedUser: IUser;
let unauthorizedUserCredentials: Credentials;

before(async () => {
unauthorizedUser = await createUser();
unauthorizedUserCredentials = await login(unauthorizedUser.username, password);
});

after(async () => {
await deleteUser(unauthorizedUser);
});

it('should return forbidden if user does not have the manage-sounds permission', async () => {
await request.post(api('custom-sounds.delete')).set(unauthorizedUserCredentials).send({ _id: soundToDeleteId }).expect(403);
});
});

it('should successfully delete a custom sound when providing a valid _id', async () => {
await request
.post(api('custom-sounds.delete'))
.set(credentials)
.send({ _id: soundToDeleteId })
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
soundDeleted = true;
});
});

describe('Accessing custom sounds', () => {
it('should return forbidden if the there is no fileId on the url', (done) => {
void request
Expand Down
17 changes: 17 additions & 0 deletions packages/rest-typings/src/v1/Ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,20 @@ const NotFoundErrorResponseSchema = {
};

export const validateNotFoundErrorResponse = ajv.compile<NotFoundErrorResponse>(NotFoundErrorResponseSchema);

type InternalErrorResponse = {
success: false;
error: string;
};

const InternalErrorResponseSchema = {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
type: 'object',
properties: {
success: { type: 'boolean', enum: [false] },
error: { type: 'string' },
},
required: ['success', 'error'],
additionalProperties: false,
};

export const validateInternalErrorResponse = ajv.compile<InternalErrorResponse>(InternalErrorResponseSchema);
16 changes: 16 additions & 0 deletions packages/rest-typings/src/v1/customSounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,19 @@ const CustomSoundsUpdateSchema = {
};

export const isCustomSoundsUpdateProps = ajv.compile<CustomSoundsUpdate>(CustomSoundsUpdateSchema);

type CustomSoundsDelete = { _id: ICustomSound['_id'] };

const CustomSoundsDeleteSchema = {
type: 'object',
properties: {
_id: {
type: 'string',
minLength: 1,
},
},
required: ['_id'],
additionalProperties: false,
};

export const isCustomSoundsDeleteProps = ajv.compile<CustomSoundsDelete>(CustomSoundsDeleteSchema);
Loading