Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/users-info-openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use minor instead of patch for this project

---

chore: migrate users.info endpoint to new OpenAPI pattern with AJV validation
120 changes: 78 additions & 42 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
isUserCreateParamsPOST,
isUserSetActiveStatusParamsPOST,
isUserDeactivateIdleParamsPOST,
isUsersInfoParamsGetProps,
isUsersListStatusProps,
isUsersSendWelcomeEmailProps,
isUserRegisterParamsPOST,
Expand Down Expand Up @@ -426,52 +425,89 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
const usersInfoEndpoint = API.v1.get(
'users.info',
{ authRequired: true, validateParams: isUsersInfoParamsGetProps },
{
async get() {
const searchTerms: [string, 'id' | 'username' | 'importId'] | false =
('userId' in this.queryParams && !!this.queryParams.userId && [this.queryParams.userId, 'id']) ||
('username' in this.queryParams && !!this.queryParams.username && [this.queryParams.username, 'username']) ||
('importId' in this.queryParams && !!this.queryParams.importId && [this.queryParams.importId, 'importId']);

if (!searchTerms) {
return API.v1.failure('Invalid search query.');
}
authRequired: true,
query: ajv.compile<
| { userId: string; username?: never; importId?: never; includeUserRooms?: string }
| { username: string; userId?: never; importId?: never; includeUserRooms?: string }
| { importId: string; userId?: never; username?: never; includeUserRooms?: string }
>({
anyOf: [
{
type: 'object',
properties: {
userId: { type: 'string' },
includeUserRooms: { type: 'string' },
},
required: ['userId'],
additionalProperties: false,
},
{
type: 'object',
properties: {
username: { type: 'string' },
includeUserRooms: { type: 'string' },
},
required: ['username'],
additionalProperties: false,
},
{
type: 'object',
properties: {
importId: { type: 'string' },
includeUserRooms: { type: 'string' },
},
required: ['importId'],
additionalProperties: false,
},
],
}),
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: ajv.compile<{ user: IUser; success: true }>({
type: 'object',
properties: {
user: { type: 'object' },
success: { type: 'boolean', enum: [true] },
},
required: ['user', 'success'],
additionalProperties: false,
}),
},
},
async function action() {
const searchTerms: [string, 'id' | 'username' | 'importId'] | false =
('userId' in this.queryParams && !!this.queryParams.userId && [this.queryParams.userId, 'id']) ||
('username' in this.queryParams && !!this.queryParams.username && [this.queryParams.username, 'username']) ||
('importId' in this.queryParams && !!this.queryParams.importId && [this.queryParams.importId, 'importId']);

if (!searchTerms) {
return API.v1.failure('Invalid search query.');
}

const user = await getFullUserDataByIdOrUsernameOrImportId(this.userId, ...searchTerms);
const user = await getFullUserDataByIdOrUsernameOrImportId(this.userId, ...searchTerms);

if (!user) {
return API.v1.failure('User not found.');
}
const myself = user._id === this.userId;
if (this.queryParams.includeUserRooms === 'true' && (myself || (await hasPermissionAsync(this.userId, 'view-other-user-channels')))) {
return API.v1.success({
user: {
...user,
rooms: await Subscriptions.findByUserId(user._id, {
projection: {
rid: 1,
name: 1,
t: 1,
roles: 1,
unread: 1,
federated: 1,
},
sort: {
t: 1,
name: 1,
},
}).toArray(),
},
});
}
if (!user) {
return API.v1.failure('User not found.');
}

const myself = user._id === this.userId;
if (this.queryParams.includeUserRooms === 'true' && (myself || (await hasPermissionAsync(this.userId, 'view-other-user-channels')))) {
return API.v1.success({
user,
user: {
...user,
rooms: await Subscriptions.findByUserId(user._id, {
projection: { rid: 1, name: 1, t: 1, roles: 1, unread: 1, federated: 1 },
sort: { t: 1, name: 1 },
}).toArray(),
},
});
},
}

return API.v1.success({ user });
},
);

Expand Down Expand Up @@ -1555,8 +1591,8 @@ settings.watch<number>('Rate_Limiter_Limit_RegisterUser', (value) => {
});

type UsersEndpoints = ExtractRoutesFromAPI<typeof usersEndpoints>;

type UsersInfoEndpoint = ExtractRoutesFromAPI<typeof usersInfoEndpoint>;
declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends UsersEndpoints {}
interface Endpoints extends UsersEndpoints, UsersInfoEndpoint {}
}