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
6 changes: 6 additions & 0 deletions .changeset/migrate-users-create-openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/rest-typings": minor
---

Add OpenAPI support for the `users.create` API endpoint by migrating to chained route definition with AJV body and response validation.
155 changes: 108 additions & 47 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { MeteorError, Team, api, Calendar } from '@rocket.chat/core-services';
import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, UserStatus } from '@rocket.chat/core-typings';
import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser, IUserSettings, UserStatus } from '@rocket.chat/core-typings';
import { Users, Subscriptions, Sessions } from '@rocket.chat/models';
import {
isUserCreateParamsPOST,
isUserSetActiveStatusParamsPOST,
isUserDeactivateIdleParamsPOST,
isUsersInfoParamsGetProps,
Expand Down Expand Up @@ -300,51 +299,6 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'users.create',
{ authRequired: true, validateParams: isUserCreateParamsPOST },
{
async post() {
// New change made by pull request #5152
if (typeof this.bodyParams.joinDefaultChannels === 'undefined') {
this.bodyParams.joinDefaultChannels = true;
}

if (this.bodyParams.name && !validateNameChars(this.bodyParams.name)) {
return API.v1.failure('Name contains invalid characters');
}

if (this.bodyParams.customFields) {
validateCustomFields(this.bodyParams.customFields);
}

if (this.bodyParams.freeSwitchExtension && !(await canEditExtension(this.bodyParams.freeSwitchExtension))) {
return API.v1.failure('Setting user voice call extension is not allowed', 'error-action-not-allowed');
}

const newUserId = await saveUser(this.userId, this.bodyParams);
const userId = typeof newUserId !== 'string' ? this.userId : newUserId;

if (this.bodyParams.customFields) {
await saveCustomFieldsWithoutValidation(userId, this.bodyParams.customFields);
}

if (typeof this.bodyParams.active !== 'undefined') {
await executeSetUserActiveStatus(this.userId, userId, this.bodyParams.active);
}

const { fields } = await this.parseJsonQuery();

const user = await Users.findOneById(userId, { projection: fields });
if (!user) {
return API.v1.failure('User not found');
}

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

API.v1.addRoute(
'users.delete',
{ authRequired: true, permissionsRequired: ['delete-user'] },
Expand Down Expand Up @@ -753,6 +707,55 @@ API.v1.addRoute(
},
);

type UserCreateParamsPOST = {
email: string;
name: string;
password: string;
username: string;
active?: boolean;
bio?: string;
nickname?: string;
statusText?: string;
roles?: string[];
joinDefaultChannels?: boolean;
requirePasswordChange?: boolean;
setRandomPassword?: boolean;
sendWelcomeEmail?: boolean;
verified?: boolean;
customFields?: Record<string, any>;
settings?: IUserSettings;
Comment thread
This conversation was marked as resolved.
Resolved
freeSwitchExtension?: string;
/* @deprecated */
fields: string;
};

const userCreateParamsPostSchema = {
type: 'object',
properties: {
email: { type: 'string' },
name: { type: 'string' },
password: { type: 'string' },
username: { type: 'string' },
active: { type: 'boolean', nullable: true },
bio: { type: 'string', nullable: true },
nickname: { type: 'string', nullable: true },
statusText: { type: 'string', nullable: true },
roles: { type: 'array', items: { type: 'string' } },
joinDefaultChannels: { type: 'boolean', nullable: true },
requirePasswordChange: { type: 'boolean', nullable: true },
setRandomPassword: { type: 'boolean', nullable: true },
sendWelcomeEmail: { type: 'boolean', nullable: true },
verified: { type: 'boolean', nullable: true },
customFields: { type: 'object' },
fields: { type: 'string', nullable: true },
freeSwitchExtension: { type: 'string', nullable: true },
},
additionalProperties: false,
required: ['email', 'name', 'password', 'username'],
};

const isUserCreateParamsPOST = ajv.compile<UserCreateParamsPOST>(userCreateParamsPostSchema);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

const usersEndpoints = API.v1
.post(
'users.createToken',
Expand Down Expand Up @@ -878,6 +881,64 @@ const usersEndpoints = API.v1

return API.v1.success({ suggestions });
},
)
.post(
'users.create',
{
authRequired: true,
body: isUserCreateParamsPOST,
response: {
200: ajv.compile<{ user: IUser }>({
type: 'object',
properties: {
user: { $ref: '#/components/schemas/IUser' },
Comment thread
This conversation was marked as resolved.
Resolved
success: { type: 'boolean', enum: [true] },
},
required: ['user', 'success'],
additionalProperties: false,
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function action() {
// New change made by pull request #5152
if (typeof this.bodyParams.joinDefaultChannels === 'undefined') {
this.bodyParams.joinDefaultChannels = true;
}

if (this.bodyParams.name && !validateNameChars(this.bodyParams.name)) {
return API.v1.failure('Name contains invalid characters');
}

if (this.bodyParams.customFields) {
validateCustomFields(this.bodyParams.customFields);
}

if (this.bodyParams.freeSwitchExtension && !(await canEditExtension(this.bodyParams.freeSwitchExtension))) {
return API.v1.failure('Setting user voice call extension is not allowed', 'error-action-not-allowed');
}

const newUserId = await saveUser(this.userId, this.bodyParams);
const userId = typeof newUserId !== 'string' ? this.userId : newUserId;

if (this.bodyParams.customFields) {
await saveCustomFieldsWithoutValidation(userId, this.bodyParams.customFields);
}

if (typeof this.bodyParams.active !== 'undefined') {
await executeSetUserActiveStatus(this.userId, userId, this.bodyParams.active);
}

const { fields } = await this.parseJsonQuery();

const user = await Users.findOneById(userId, { projection: fields });
if (!user) {
return API.v1.failure('User not found');
}

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

API.v1.addRoute(
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/views/admin/users/AdminUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
} from '@rocket.chat/fuselage';
import type { SelectOption } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { UserCreateParamsPOST } from '@rocket.chat/rest-typings';
import type { OperationParams } from '@rocket.chat/rest-typings';
import { validateEmail } from '@rocket.chat/tools';
import { CustomFieldsForm, ContextualbarScrollableContent, ContextualbarFooter } from '@rocket.chat/ui-client';
import {
Expand Down Expand Up @@ -55,7 +55,7 @@ type AdminUserFormProps = {
};

export type UserFormProps = Omit<
UserCreateParamsPOST & { avatar: AvatarObject; passwordConfirmation: string; freeSwitchExtension?: string },
OperationParams<'POST', '/v1/users.create'> & { avatar: AvatarObject; passwordConfirmation: string; freeSwitchExtension?: string },
'fields'
>;

Expand Down
8 changes: 0 additions & 8 deletions packages/rest-typings/src/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { IExportOperation, ISubscription, ITeam, IUser, IPersonalAccessToke
import { ajv } from './Ajv';
import type { PaginatedRequest } from '../helpers/PaginatedRequest';
import type { PaginatedResult } from '../helpers/PaginatedResult';
import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST';
import type { UserDeactivateIdleParamsPOST } from './users/UserDeactivateIdleParamsPOST';
import type { UserLogoutParamsPOST } from './users/UserLogoutParamsPOST';
import type { UserRegisterParamsPOST } from './users/UserRegisterParamsPOST';
Expand Down Expand Up @@ -271,12 +270,6 @@ export type UsersEndpoints = {
};
};

'/v1/users.create': {
POST: (params: UserCreateParamsPOST) => {
user: IUser;
};
};

'/v1/users.update': {
POST: (params: UsersUpdateParamsPOST) => {
Comment thread
This conversation was marked as resolved.
Resolved
user: IUser;
Expand Down Expand Up @@ -370,7 +363,6 @@ export type UsersEndpoints = {
};
};

export * from './users/UserCreateParamsPOST';
export * from './users/UserSetActiveStatusParamsPOST';
export * from './users/UserDeactivateIdleParamsPOST';
export * from './users/UsersInfoParamsGet';
Expand Down
52 changes: 0 additions & 52 deletions packages/rest-typings/src/v1/users/UserCreateParamsPOST.ts

This file was deleted.