Skip to content

Commit 5dc4b44

Browse files
committed
refactor: Migrate livechat/users endpoints to OpenAPI with AJV validation
1 parent 1cac8c3 commit 5dc4b44

File tree

3 files changed

+180
-153
lines changed

3 files changed

+180
-153
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/rest-typings": minor
4+
---
5+
6+
Add OpenAPI support for the livechat/users/:type and livechat/users/:type/:_id API endpoints by migrating to chained route definitions with AJV body, query, and response validation.

apps/meteor/app/livechat/imports/server/rest/users.ts

Lines changed: 165 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,138 @@
11
import { Users } from '@rocket.chat/models';
2-
import { isLivechatUsersManagerGETProps, isPOSTLivechatUsersTypeProps } from '@rocket.chat/rest-typings';
3-
import { check } from 'meteor/check';
2+
import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse, validateForbiddenErrorResponse } from '@rocket.chat/rest-typings';
43

54
import { API } from '../../../../api/server';
65
import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems';
76
import { hasAtLeastOnePermissionAsync } from '../../../../authorization/server/functions/hasPermission';
87
import { findAgents, findManagers } from '../../../server/api/lib/users';
98
import { addManager, addAgent, removeAgent, removeManager } from '../../../server/lib/omni-users';
109

10+
// --- Local types and AJV schemas (moved from rest-typings) ---
11+
12+
type LivechatUsersManagerGETLocal = {
13+
text?: string;
14+
fields?: string;
15+
onlyAvailable?: boolean;
16+
excludeId?: string;
17+
showIdleAgents?: boolean;
18+
count?: number;
19+
offset?: number;
20+
sort?: string;
21+
query?: string;
22+
};
23+
24+
const LivechatUsersManagerGETLocalSchema = {
25+
type: 'object',
26+
properties: {
27+
text: { type: 'string', nullable: true },
28+
onlyAvailable: { type: 'boolean', nullable: true },
29+
excludeId: { type: 'string', nullable: true },
30+
showIdleAgents: { type: 'boolean', nullable: true },
31+
count: { type: 'number', nullable: true },
32+
offset: { type: 'number', nullable: true },
33+
sort: { type: 'string', nullable: true },
34+
query: { type: 'string', nullable: true },
35+
fields: { type: 'string', nullable: true },
36+
},
37+
required: [],
38+
additionalProperties: false,
39+
};
40+
41+
const isLivechatUsersManagerGETLocal = ajv.compile<LivechatUsersManagerGETLocal>(LivechatUsersManagerGETLocalSchema);
42+
43+
type POSTLivechatUsersTypeLocal = {
44+
username: string;
45+
};
46+
47+
const POSTLivechatUsersTypeLocalSchema = {
48+
type: 'object',
49+
properties: {
50+
username: { type: 'string' },
51+
},
52+
required: ['username'],
53+
additionalProperties: false,
54+
};
55+
56+
const isPOSTLivechatUsersTypeLocal = ajv.compile<POSTLivechatUsersTypeLocal>(POSTLivechatUsersTypeLocalSchema);
57+
58+
// --- Response schemas ---
59+
60+
const paginatedUsersResponseSchema = ajv.compile<{
61+
users: object[];
62+
count: number;
63+
offset: number;
64+
total: number;
65+
}>({
66+
type: 'object',
67+
properties: {
68+
// ILivechatAgent is not registered in typia (extends IUser with livechat-specific fields),
69+
// so we use { type: 'object' } as a fallback.
70+
users: { type: 'array', items: { type: 'object' } },
71+
count: { type: 'number' },
72+
offset: { type: 'number' },
73+
total: { type: 'number' },
74+
success: { type: 'boolean', enum: [true] },
75+
},
76+
required: ['users', 'count', 'offset', 'total', 'success'],
77+
additionalProperties: false,
78+
});
79+
80+
const postUserResponseSchema = ajv.compile<{ user: object }>({
81+
type: 'object',
82+
properties: {
83+
user: { type: 'object' },
84+
success: { type: 'boolean', enum: [true] },
85+
},
86+
required: ['user', 'success'],
87+
additionalProperties: false,
88+
});
89+
90+
const successOnlyResponseSchema = ajv.compile<void>({
91+
type: 'object',
92+
properties: {
93+
success: { type: 'boolean', enum: [true] },
94+
},
95+
required: ['success'],
96+
additionalProperties: false,
97+
});
98+
99+
const getUserByIdResponseSchema = ajv.compile<{ user: object | null }>({
100+
type: 'object',
101+
properties: {
102+
user: { type: ['object', 'null'] },
103+
success: { type: 'boolean', enum: [true] },
104+
},
105+
required: ['user', 'success'],
106+
additionalProperties: false,
107+
});
108+
11109
const emptyStringArray: string[] = [];
12110

13-
API.v1.addRoute(
14-
'livechat/users/:type',
15-
{
16-
authRequired: true,
17-
permissionsRequired: {
18-
'POST': ['view-livechat-manager'],
19-
'*': emptyStringArray,
20-
},
21-
validateParams: {
22-
GET: isLivechatUsersManagerGETProps,
23-
POST: isPOSTLivechatUsersTypeProps,
111+
API.v1
112+
.get(
113+
'livechat/users/:type',
114+
{
115+
authRequired: true,
116+
permissionsRequired: emptyStringArray,
117+
query: isLivechatUsersManagerGETLocal,
118+
response: {
119+
200: paginatedUsersResponseSchema,
120+
400: validateBadRequestErrorResponse,
121+
401: validateUnauthorizedErrorResponse,
122+
403: validateForbiddenErrorResponse,
123+
},
24124
},
25-
},
26-
{
27-
async get() {
28-
check(this.urlParams, {
29-
type: String,
30-
});
125+
async function action() {
31126
const { offset, count } = await getPaginationItems(this.queryParams);
32127
const { sort } = await this.parseJsonQuery();
33128
const { text } = this.queryParams;
34129

35130
if (this.urlParams.type === 'agent') {
36131
if (!(await hasAtLeastOnePermissionAsync(this.userId, ['transfer-livechat-guest', 'edit-omnichannel-contact']))) {
37-
return API.v1.forbidden();
132+
return API.v1.forbidden('error-not-authorized');
38133
}
39134

40-
const { onlyAvailable, excludeId, showIdleAgents } = this.queryParams;
135+
const { onlyAvailable = false, excludeId, showIdleAgents } = this.queryParams;
41136
return API.v1.success(
42137
await findAgents({
43138
text,
@@ -54,7 +149,7 @@ API.v1.addRoute(
54149
}
55150
if (this.urlParams.type === 'manager') {
56151
if (!(await hasAtLeastOnePermissionAsync(this.userId, ['view-livechat-manager']))) {
57-
return API.v1.forbidden();
152+
return API.v1.forbidden('error-not-authorized');
58153
}
59154

60155
return API.v1.success(
@@ -70,7 +165,21 @@ API.v1.addRoute(
70165
}
71166
throw new Error('Invalid type');
72167
},
73-
async post() {
168+
)
169+
.post(
170+
'livechat/users/:type',
171+
{
172+
authRequired: true,
173+
permissionsRequired: ['view-livechat-manager'],
174+
body: isPOSTLivechatUsersTypeLocal,
175+
response: {
176+
200: postUserResponseSchema,
177+
400: validateBadRequestErrorResponse,
178+
401: validateUnauthorizedErrorResponse,
179+
403: validateForbiddenErrorResponse,
180+
},
181+
},
182+
async function action() {
74183
if (this.urlParams.type === 'agent') {
75184
const user = await addAgent(this.bodyParams.username);
76185
if (user) {
@@ -87,14 +196,21 @@ API.v1.addRoute(
87196

88197
return API.v1.failure();
89198
},
90-
},
91-
);
92-
93-
API.v1.addRoute(
94-
'livechat/users/:type/:_id',
95-
{ authRequired: true, permissionsRequired: ['view-livechat-manager'] },
96-
{
97-
async get() {
199+
)
200+
.get(
201+
'livechat/users/:type/:_id',
202+
{
203+
authRequired: true,
204+
permissionsRequired: ['view-livechat-manager'],
205+
query: undefined,
206+
response: {
207+
200: getUserByIdResponseSchema,
208+
400: validateBadRequestErrorResponse,
209+
401: validateUnauthorizedErrorResponse,
210+
403: validateForbiddenErrorResponse,
211+
},
212+
},
213+
async function action() {
98214
if (!['agent', 'manager'].includes(this.urlParams.type)) {
99215
throw new Error('Invalid type');
100216
}
@@ -107,7 +223,21 @@ API.v1.addRoute(
107223
// TODO: throw error instead of returning null
108224
return API.v1.success({ user });
109225
},
110-
async delete() {
226+
)
227+
.delete(
228+
'livechat/users/:type/:_id',
229+
{
230+
authRequired: true,
231+
permissionsRequired: ['view-livechat-manager'],
232+
query: undefined,
233+
response: {
234+
200: successOnlyResponseSchema,
235+
400: validateBadRequestErrorResponse,
236+
401: validateUnauthorizedErrorResponse,
237+
403: validateForbiddenErrorResponse,
238+
},
239+
},
240+
async function action() {
111241
if (this.urlParams.type === 'agent') {
112242
if (await removeAgent(this.urlParams._id)) {
113243
return API.v1.success();
@@ -120,7 +250,7 @@ API.v1.addRoute(
120250
throw new Error('Invalid type');
121251
}
122252

123-
return API.v1.failure();
253+
return API.v1.failure('error-removing-user');
124254
},
125-
},
126-
);
255+
);
256+

0 commit comments

Comments
 (0)