From adc5b677a7183105f7087e05284ff657d575227a Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 24 Feb 2026 17:41:20 +0200 Subject: [PATCH 1/3] chore: Add OpenAPI support for the Rocket.Chat users.getAvatarSuggestion API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. --- .changeset/rare-waves-help.md | 6 + apps/meteor/app/api/server/v1/users.ts | 189 ++++++++++++++++--------- packages/rest-typings/src/v1/users.ts | 14 -- 3 files changed, 125 insertions(+), 84 deletions(-) create mode 100644 .changeset/rare-waves-help.md diff --git a/.changeset/rare-waves-help.md b/.changeset/rare-waves-help.md new file mode 100644 index 0000000000000..476f7e0839153 --- /dev/null +++ b/.changeset/rare-waves-help.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Add OpenAPI support for the Rocket.Chat users.getAvatarSuggestion API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 88b6b67e3b442..101ffb705fe80 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -19,7 +19,10 @@ import { isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, ajv, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, } from '@rocket.chat/rest-typings'; +import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; @@ -97,20 +100,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'users.getAvatarSuggestion', - { - authRequired: true, - }, - { - async get() { - const suggestions = await getAvatarSuggestionForUser(this.user); - - return API.v1.success({ suggestions }); - }, - }, -); - API.v1.addRoute( 'users.update', { authRequired: true, twoFactorRequired: true, validateParams: isUsersUpdateParamsPOST }, @@ -764,72 +753,132 @@ API.v1.addRoute( }, ); -const usersEndpoints = API.v1.post( - 'users.createToken', - { - authRequired: true, - body: ajv.compile<{ userId: string; secret: string }>({ - type: 'object', - properties: { - userId: { - type: 'string', - minLength: 1, - }, - secret: { - type: 'string', - minLength: 1, - }, - }, - required: ['userId', 'secret'], - additionalProperties: false, - }), - response: { - 200: ajv.compile<{ data: { userId: string; authToken: string } }>({ +const usersEndpoints = API.v1 + .post( + 'users.createToken', + { + authRequired: true, + body: ajv.compile<{ userId: string; secret: string }>({ type: 'object', properties: { - data: { - type: 'object', - properties: { - userId: { - type: 'string', - minLength: 1, - }, - authToken: { - type: 'string', - minLength: 1, - }, - }, - required: ['userId'], - additionalProperties: false, + userId: { + type: 'string', + minLength: 1, }, - success: { - type: 'boolean', - enum: [true], + secret: { + type: 'string', + minLength: 1, }, }, - required: ['data', 'success'], - additionalProperties: false, - }), - 400: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - error: { type: 'string' }, - errorType: { type: 'string' }, - }, - required: ['success'], + required: ['userId', 'secret'], additionalProperties: false, }), + response: { + 200: ajv.compile<{ data: { userId: string; authToken: string } }>({ + type: 'object', + properties: { + data: { + type: 'object', + properties: { + userId: { + type: 'string', + minLength: 1, + }, + authToken: { + type: 'string', + minLength: 1, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['data', 'success'], + additionalProperties: false, + }), + 400: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, + }), + }, }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); + async function action() { + const user = await getUserFromParams(this.bodyParams); - const data = await generateAccessToken(user._id, this.bodyParams.secret); + const data = await generateAccessToken(user._id, this.bodyParams.secret); - return API.v1.success({ data }); - }, -); + return API.v1.success({ data }); + }, + ) + .get( + 'users.getAvatarSuggestion', + { + authRequired: true, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ + suggestions: Record< + string, + { + blob: string; + contentType: string; + service: string; + url: string; + } + >; + }>({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + suggestions: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + blob: { + type: 'string', + }, + contentType: { + type: 'string', + }, + service: { + type: 'string', + }, + url: { + type: 'string', + format: 'uri', + }, + }, + required: ['blob', 'contentType', 'service', 'url'], + additionalProperties: false, + }, + }, + }, + required: ['success', 'suggestions'], + additionalProperties: false, + }), + }, + }, + async function action() { + const suggestions = await getAvatarSuggestionForUser(this.user); + + return API.v1.success({ suggestions }); + }, + ); API.v1.addRoute( 'users.getPreferences', diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index f0aec0a85aff4..565620d31ba5e 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -255,20 +255,6 @@ export type UsersEndpoints = { }; }; - '/v1/users.getAvatarSuggestion': { - GET: () => { - suggestions: Record< - string, - { - blob: string; - contentType: string; - service: string; - url: string; - } - >; - }; - }; - '/v1/users.checkUsernameAvailability': { GET: (params: { username: string }) => { result: boolean; From 97c0906c1f8e41d52ab997158afb4ae792172040 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 24 Feb 2026 18:40:26 +0200 Subject: [PATCH 2/3] chore(api): Remove unused AJV import from users endpoint --- apps/meteor/app/api/server/v1/users.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 101ffb705fe80..d9e37b1f7b8b2 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -22,7 +22,6 @@ import { validateBadRequestErrorResponse, validateUnauthorizedErrorResponse, } from '@rocket.chat/rest-typings'; -import { ajv } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; From dfb7f5be11e74f7dd61aaf4cb6b6a485989ceb9c Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 24 Feb 2026 18:40:41 +0200 Subject: [PATCH 3/3] chore(deps): Bump @rocket.chat/ui-contexts to 27.0.1 --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index a93a67032f4eb..f219de83d458d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10437,7 +10437,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.4 - "@rocket.chat/ui-contexts": 27.0.0 + "@rocket.chat/ui-contexts": 27.0.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*"