Skip to content

Commit 5f3ac3a

Browse files
refactor: enhance API error response validation and accessibility (RocketChat#36574)
1 parent 0ca189f commit 5f3ac3a

5 files changed

Lines changed: 90 additions & 118 deletions

File tree

apps/meteor/app/api/server/v1/chat.ts

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import {
2929
isChatSyncThreadMessagesProps,
3030
isChatGetStarredMessagesProps,
3131
isChatGetDiscussionsProps,
32+
validateBadRequestErrorResponse,
33+
validateUnauthorizedErrorResponse,
3234
} from '@rocket.chat/rest-typings';
3335
import { escapeRegExp } from '@rocket.chat/string-helpers';
3436
import { Meteor } from 'meteor/meteor';
@@ -197,35 +199,8 @@ const chatPinMessageEndpoints = API.v1.post(
197199
authRequired: true,
198200
body: isChatPinMessageProps,
199201
response: {
200-
400: ajv.compile<{
201-
error?: string;
202-
errorType?: string;
203-
stack?: string;
204-
details?: string;
205-
}>({
206-
type: 'object',
207-
properties: {
208-
success: { type: 'boolean', enum: [false] },
209-
stack: { type: 'string' },
210-
error: { type: 'string' },
211-
errorType: { type: 'string' },
212-
details: { type: 'string' },
213-
},
214-
required: ['success'],
215-
additionalProperties: false,
216-
}),
217-
401: ajv.compile({
218-
type: 'object',
219-
properties: {
220-
success: { type: 'boolean', enum: [false] },
221-
status: { type: 'string' },
222-
message: { type: 'string' },
223-
error: { type: 'string' },
224-
errorType: { type: 'string' },
225-
},
226-
required: ['success'],
227-
additionalProperties: false,
228-
}),
202+
400: validateBadRequestErrorResponse,
203+
401: validateUnauthorizedErrorResponse,
229204
200: ajv.compile<{ message: IMessage }>({
230205
type: 'object',
231206
properties: {

apps/meteor/app/api/server/v1/oauthapps.ts

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import type { IOAuthApps } from '@rocket.chat/core-typings';
22
import { OAuthApps } from '@rocket.chat/models';
3-
import { ajv, isUpdateOAuthAppParams, isOauthAppsGetParams, isDeleteOAuthAppParams } from '@rocket.chat/rest-typings';
3+
import {
4+
ajv,
5+
isUpdateOAuthAppParams,
6+
isOauthAppsGetParams,
7+
isDeleteOAuthAppParams,
8+
validateUnauthorizedErrorResponse,
9+
validateBadRequestErrorResponse,
10+
validateForbiddenErrorResponse,
11+
} from '@rocket.chat/rest-typings';
412

513
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
614
import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
@@ -117,47 +125,9 @@ const oauthAppsCreateEndpoints = API.v1.post(
117125
body: isOauthAppsAddParams,
118126
permissionsRequired: ['manage-oauth-apps'],
119127
response: {
120-
400: ajv.compile<{
121-
error?: string;
122-
errorType?: string;
123-
stack?: string;
124-
details?: object;
125-
}>({
126-
type: 'object',
127-
properties: {
128-
success: { type: 'boolean', enum: [false] },
129-
stack: { type: 'string' },
130-
error: { type: 'string' },
131-
errorType: { type: 'string' },
132-
details: { type: 'object' },
133-
},
134-
required: ['success'],
135-
additionalProperties: false,
136-
}),
137-
401: ajv.compile({
138-
type: 'object',
139-
properties: {
140-
success: { type: 'boolean', enum: [false] },
141-
status: { type: 'string' },
142-
message: { type: 'string' },
143-
error: { type: 'string' },
144-
errorType: { type: 'string' },
145-
},
146-
required: ['success'],
147-
additionalProperties: false,
148-
}),
149-
403: ajv.compile({
150-
type: 'object',
151-
properties: {
152-
success: { type: 'boolean', enum: [false] },
153-
status: { type: 'string' },
154-
message: { type: 'string' },
155-
error: { type: 'string' },
156-
errorType: { type: 'string' },
157-
},
158-
required: ['success'],
159-
additionalProperties: false,
160-
}),
128+
400: validateBadRequestErrorResponse,
129+
401: validateUnauthorizedErrorResponse,
130+
403: validateForbiddenErrorResponse,
161131
200: ajv.compile<{ application: IOAuthApps }>({
162132
type: 'object',
163133
properties: {

apps/meteor/app/api/server/v1/webdav.ts

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { api } from '@rocket.chat/core-services';
22
import type { IWebdavAccount, IWebdavAccountIntegration } from '@rocket.chat/core-typings';
33
import { WebdavAccounts } from '@rocket.chat/models';
4-
import { ajv } from '@rocket.chat/rest-typings';
4+
import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
55
import type { DeleteResult } from 'mongodb';
66

77
import type { ExtractRoutesFromAPI } from '../ApiClass';
@@ -48,20 +48,7 @@ const webdavGetMyAccountsEndpoints = API.v1.get(
4848
required: ['success', 'accounts'],
4949
additionalProperties: false,
5050
}),
51-
401: ajv.compile({
52-
type: 'object',
53-
properties: {
54-
message: {
55-
type: 'string',
56-
},
57-
success: {
58-
type: 'boolean',
59-
description: 'Indicates if the request was successful.',
60-
},
61-
},
62-
required: ['success', 'message'],
63-
additionalProperties: false,
64-
}),
51+
401: validateUnauthorizedErrorResponse,
6552
},
6653
},
6754
async function action() {
@@ -121,37 +108,8 @@ const webdavRemoveAccountEndpoints = API.v1.post(
121108
required: ['result', 'success'],
122109
additionalProperties: false,
123110
}),
124-
400: ajv.compile({
125-
type: 'object',
126-
properties: {
127-
errorType: {
128-
type: 'string',
129-
},
130-
error: {
131-
type: 'string',
132-
},
133-
success: {
134-
type: 'boolean',
135-
description: 'Indicates if the request was successful.',
136-
},
137-
},
138-
required: ['success', 'errorType', 'error'],
139-
additionalProperties: false,
140-
}),
141-
401: ajv.compile({
142-
type: 'object',
143-
properties: {
144-
message: {
145-
type: 'string',
146-
},
147-
success: {
148-
type: 'boolean',
149-
description: 'Indicates if the request was successful.',
150-
},
151-
},
152-
required: ['success', 'message'],
153-
additionalProperties: false,
154-
}),
111+
400: validateBadRequestErrorResponse,
112+
401: validateUnauthorizedErrorResponse,
155113
},
156114
},
157115
async function action() {

packages/rest-typings/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,4 @@ export * from './v1/banners';
277277
export * from './default';
278278

279279
// Export the ajv instance for use in other packages
280-
export { ajv } from './v1/Ajv';
280+
export * from './v1/Ajv';

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,72 @@ ajv.addKeyword({
2121
validate: (_schema: unknown, data: unknown): boolean => typeof data === 'string' && !!data.trim(),
2222
});
2323
export { ajv };
24+
25+
type BadRequestErrorResponse = {
26+
success: false;
27+
error?: string;
28+
errorType?: string;
29+
stack?: string;
30+
details?: string | object;
31+
};
32+
33+
const BadRequestErrorResponseSchema = {
34+
type: 'object',
35+
properties: {
36+
success: { type: 'boolean', enum: [false] },
37+
stack: { type: 'string' },
38+
error: { type: 'string' },
39+
errorType: { type: 'string' },
40+
details: { anyOf: [{ type: 'string' }, { type: 'object' }] },
41+
},
42+
required: ['success'],
43+
additionalProperties: false,
44+
};
45+
46+
export const validateBadRequestErrorResponse = ajv.compile<BadRequestErrorResponse>(BadRequestErrorResponseSchema);
47+
48+
type UnauthorizedErrorResponse = {
49+
success: false;
50+
status?: string;
51+
message?: string;
52+
error?: string;
53+
errorType?: string;
54+
};
55+
56+
const UnauthorizedErrorResponseSchema = {
57+
type: 'object',
58+
properties: {
59+
success: { type: 'boolean', enum: [false] },
60+
status: { type: 'string' },
61+
message: { type: 'string' },
62+
error: { type: 'string' },
63+
errorType: { type: 'string' },
64+
},
65+
required: ['success'],
66+
additionalProperties: false,
67+
};
68+
69+
export const validateUnauthorizedErrorResponse = ajv.compile<UnauthorizedErrorResponse>(UnauthorizedErrorResponseSchema);
70+
71+
type ForbiddenErrorResponse = {
72+
success: false;
73+
status?: string;
74+
message?: string;
75+
error?: string;
76+
errorType?: string;
77+
};
78+
79+
const ForbiddenErrorResponseSchema = {
80+
type: 'object',
81+
properties: {
82+
success: { type: 'boolean', enum: [false] },
83+
status: { type: 'string' },
84+
message: { type: 'string' },
85+
error: { type: 'string' },
86+
errorType: { type: 'string' },
87+
},
88+
required: ['success'],
89+
additionalProperties: false,
90+
};
91+
92+
export const validateForbiddenErrorResponse = ajv.compile<ForbiddenErrorResponse>(ForbiddenErrorResponseSchema);

0 commit comments

Comments
 (0)