diff --git a/.changeset/tough-ravens-shop.md b/.changeset/tough-ravens-shop.md new file mode 100644 index 0000000000000..fddfc8d053399 --- /dev/null +++ b/.changeset/tough-ravens-shop.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat Permissions API endpoints by migrating to a centralized syntax and utilizing shared AJV schemas for validation. This will enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index 9b7cee5d3c7b5..750ebe7cb2e43 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -1,17 +1,101 @@ import type { IPermission } from '@rocket.chat/core-typings'; import { Permissions, Roles } from '@rocket.chat/models'; -import { isBodyParamsValidPermissionUpdate } from '@rocket.chat/rest-typings'; +import { + ajv, + validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { permissionsGetMethod } from '../../../authorization/server/streamer/permissions'; import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; -API.v1.addRoute( - 'permissions.listAll', - { authRequired: true }, - { - async get() { +type PermissionsListAllProps = { + updatedSince?: string; +}; + +type PermissionsUpdateProps = { + permissions: { _id: string; roles: string[] }[]; +}; + +const permissionListAllSchema = { + type: 'object', + properties: { + updatedSince: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +const permissionUpdatePropsSchema = { + type: 'object', + properties: { + permissions: { + type: 'array', + items: { + type: 'object', + properties: { + _id: { type: 'string' }, + roles: { + type: 'array', + items: { type: 'string' }, + uniqueItems: true, + }, + }, + additionalProperties: false, + required: ['_id', 'roles'], + }, + }, + }, + required: ['permissions'], + additionalProperties: false, +}; + +const isPermissionsListAll = ajv.compile(permissionListAllSchema); + +const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); + +const permissionsEndpoints = API.v1 + .get( + 'permissions.listAll', + { + authRequired: true, + query: isPermissionsListAll, + response: { + 200: ajv.compile<{ + update: IPermission[]; + remove: IPermission[]; + }>({ + type: 'object', + properties: { + update: { + type: 'array', + items: { $ref: '#/components/schemas/IPermission' }, + }, + remove: { + type: 'array', + items: { $ref: '#/components/schemas/IPermission' }, + }, + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['update', 'remove', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { updatedSince } = this.queryParams; let updatedSinceDate: Date | undefined; @@ -36,14 +120,38 @@ API.v1.addRoute( return API.v1.success(result); }, - }, -); - -API.v1.addRoute( - 'permissions.update', - { authRequired: true, permissionsRequired: ['access-permissions'] }, - { - async post() { + ) + .post( + 'permissions.update', + { + authRequired: true, + permissionsRequired: ['access-permissions'], + body: isBodyParamsValidPermissionUpdate, + response: { + 200: ajv.compile<{ + permissions: IPermission[]; + }>({ + type: 'object', + properties: { + permissions: { + type: 'array', + items: { $ref: '#/components/schemas/IPermission' }, + }, + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['permissions', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + }, + async function action() { const { bodyParams } = this; if (!isBodyParamsValidPermissionUpdate(bodyParams)) { @@ -76,5 +184,11 @@ API.v1.addRoute( permissions: result, }); }, - }, -); + ); + +export type PermissionsEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends PermissionsEndpoints {} +} diff --git a/packages/core-typings/src/Ajv.ts b/packages/core-typings/src/Ajv.ts index fea0f7b8084ea..8d828e041b1bd 100644 --- a/packages/core-typings/src/Ajv.ts +++ b/packages/core-typings/src/Ajv.ts @@ -4,6 +4,7 @@ import type { ICustomSound } from './ICustomSound'; import type { IInvite } from './IInvite'; import type { IMessage } from './IMessage'; import type { IOAuthApps } from './IOAuthApps'; +import type { IPermission } from './IPermission'; import type { ISubscription } from './ISubscription'; -export const schemas = typia.json.schemas<[ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps], '3.0'>(); +export const schemas = typia.json.schemas<[ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission], '3.0'>(); diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 5b87db07ab261..8198854729d41 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -35,7 +35,6 @@ import type { MiscEndpoints } from './v1/misc'; import type { ModerationEndpoints } from './v1/moderation'; import type { OAuthAppsEndpoint } from './v1/oauthapps'; import type { OmnichannelEndpoints } from './v1/omnichannel'; -import type { PermissionsEndpoints } from './v1/permissions'; import type { PresenceEndpoints } from './v1/presence'; import type { PushEndpoints } from './v1/push'; import type { RolesEndpoints } from './v1/roles'; @@ -79,7 +78,6 @@ export interface Endpoints StatisticsEndpoints, LicensesEndpoints, MiscEndpoints, - PermissionsEndpoints, PresenceEndpoints, InstancesEndpoints, IntegrationsEndpoints, @@ -214,7 +212,6 @@ export type UrlParams = string extends T export type MethodOf = TPathPattern extends any ? keyof Endpoints[TPathPattern] : never; export * from './apps'; -export * from './v1/permissions'; export * from './v1/presence'; export * from './v1/roles'; export * from './v1/settings'; diff --git a/packages/rest-typings/src/v1/permissions.ts b/packages/rest-typings/src/v1/permissions.ts deleted file mode 100644 index 27ff0d28ded71..0000000000000 --- a/packages/rest-typings/src/v1/permissions.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { IPermission } from '@rocket.chat/core-typings'; -import Ajv from 'ajv'; - -const ajv = new Ajv({ - coerceTypes: true, -}); - -type PermissionsListAllProps = { - updatedSince?: string; -}; - -const permissionListAllSchema = { - type: 'object', - properties: { - updatedSince: { - type: 'string', - nullable: true, - }, - }, - required: [], - additionalProperties: false, -}; - -export const isPermissionsListAll = ajv.compile(permissionListAllSchema); - -type PermissionsUpdateProps = { - permissions: { _id: string; roles: string[] }[]; -}; - -const permissionUpdatePropsSchema = { - type: 'object', - properties: { - permissions: { - type: 'array', - items: { - type: 'object', - properties: { - _id: { type: 'string' }, - roles: { - type: 'array', - items: { type: 'string' }, - uniqueItems: true, - }, - }, - additionalProperties: false, - required: ['_id', 'roles'], - }, - }, - }, - required: ['permissions'], - additionalProperties: false, -}; - -export const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); - -export type PermissionsEndpoints = { - '/v1/permissions.listAll': { - GET: (params: PermissionsListAllProps) => { - update: IPermission[]; - remove: IPermission[]; - }; - }; - '/v1/permissions.update': { - POST: (params: PermissionsUpdateProps) => { - permissions: IPermission[]; - }; - }; -};