Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
294 changes: 219 additions & 75 deletions apps/meteor/app/api/server/v1/custom-user-status.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import type { ICustomUserStatus } from '@rocket.chat/core-typings';
import { CustomUserStatus } from '@rocket.chat/models';
import { ajv, ajvQuery, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
import { ajv, ajvQuery, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse, isCustomUserStatusCreateProps, isCustomUserStatusDeleteProps, isCustomUserStatusUpdateProps } from '@rocket.chat/rest-typings';
import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
// import { Match, check } from 'meteor/check'; // No longer needed
import { Meteor } from 'meteor/meteor';

import { deleteCustomUserStatus } from '../../../user-status/server/methods/deleteCustomUserStatus';
import { insertOrUpdateUserStatus } from '../../../user-status/server/methods/insertOrUpdateUserStatus';
import type { ExtractRoutesFromAPI } from '../ApiClass';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';

// This file has been migrated to use modern API registration patterns and AJV validation.
// Redundant local type definitions and module augmentations have been removed to resolve
// a TypeScript circular reference error, relying on the source of truth in `@rocket.chat/rest-typings`.

type CustomUserStatusListProps = PaginatedRequest<{ name?: string; _id?: string; query?: string }>;

const CustomUserStatusListSchema = {
Expand Down Expand Up @@ -48,7 +50,7 @@ const CustomUserStatusListSchema = {

const isCustomUserStatusListProps = ajvQuery.compile<CustomUserStatusListProps>(CustomUserStatusListSchema);

const customUserStatusEndpoints = API.v1.get(
API.v1.get(
'custom-user-status.list',
{
authRequired: true,
Expand Down Expand Up @@ -120,95 +122,237 @@ const customUserStatusEndpoints = API.v1.get(
});
},
);

API.v1.addRoute(
/**
* @openapi
* /api/v1/custom-user-status.update:
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 22, 2026

Choose a reason for hiding this comment

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

P1: OpenAPI annotation for create endpoint has incorrect path and description copied from update endpoint

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/custom-user-status.ts, line 127:

<comment>OpenAPI annotation for create endpoint has incorrect path and description copied from update endpoint</comment>

<file context>
@@ -120,95 +122,237 @@ const customUserStatusEndpoints = API.v1.get(
-API.v1.addRoute(
+/**
+ * @openapi
+ * /api/v1/custom-user-status.update:
+ * post:
+ * description: Update an existing custom user status
</file context>
Fix with Cubic

* post:
* description: Update an existing custom user status
* security:
* - cookieAuth: []
* - x-user-id: []
* - x-auth-token: []
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* _id:
* type: string
* name:
* type: string
* statusType:
* type: string
* required:
* - _id
* - name
* responses:
* 200:
* description: The updated custom user status
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiSuccessV1'
* default:
* $ref: '#/components/schemas/ApiErrorsV1'
*/
Comment on lines +125 to +158
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.

⚠️ Potential issue | 🟠 Major

The OpenAPI blocks are out of sync with the handlers.

The create and delete handlers are both documented as POST /api/v1/custom-user-status.update, so the generated spec will miss the real create/delete operations. The update block is also stricter than the actual AJV contract because it marks name as required even though only _id is required.

Also applies to: 201-234, 264-297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/api/server/v1/custom-user-status.ts` around lines 125 - 158,
The OpenAPI docs for custom-user-status are incorrect and must match the actual
handlers: update/documentation currently describes POST
/api/v1/custom-user-status.update and incorrectly requires name; change the
create and delete blocks so they use the correct paths/methods (e.g., POST
/api/v1/custom-user-status.create and POST or DELETE
/api/v1/custom-user-status.delete as implemented by the corresponding handlers)
and adjust the update block to only require _id (remove name from required) and
align properties with the AJV schema used by the update handler; update all
three OpenAPI blocks (create, update, delete) so their
operationIds/paths/methods and required fields match the real handler
implementations in custom-user-status.ts (the create, update, and delete handler
functions).

API.v1.post(
'custom-user-status.create',
{ authRequired: true },
{
async post() {
check(this.bodyParams, {
name: String,
statusType: Match.Maybe(String),
});

const userStatusData = {
name: this.bodyParams.name,
statusType: this.bodyParams.statusType || '',
};

await insertOrUpdateUserStatus(this.userId, userStatusData);

const customUserStatus = await CustomUserStatus.findOneByName(userStatusData.name);
if (!customUserStatus) {
throw new Meteor.Error('error-creating-custom-user-status', 'Error creating custom user status');
}

return API.v1.success({
customUserStatus,
});
authRequired: true,
body: isCustomUserStatusCreateProps,
response: {
200: ajv.compile<{ customUserStatus: ICustomUserStatus }>({
type: 'object',
properties: {
customUserStatus: {
$ref: '#/components/schemas/ICustomUserStatus',
},
success: {
type: 'boolean',
enum: [true],
},
},
required: ['success', 'customUserStatus'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
);
async function () {
const userStatusData = {
name: this.bodyParams.name,
statusType: this.bodyParams.statusType || '',
};

await insertOrUpdateUserStatus(this.userId, userStatusData);

API.v1.addRoute(
const customUserStatus = await CustomUserStatus.findOneByName(userStatusData.name);
if (!customUserStatus) {
throw new Meteor.Error('error-creating-custom-user-status', 'Error creating custom user status');
}

return API.v1.success({
customUserStatus,
});
},
);
/**
* @openapi
* /api/v1/custom-user-status.update:
* post:
* description: Update an existing custom user status
* security:
* - cookieAuth: []
* - x-user-id: []
* - x-auth-token: []
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* _id:
* type: string
* name:
* type: string
* statusType:
* type: string
* required:
* - _id
* - name
* responses:
* 200:
* description: The updated custom user status
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiSuccessV1'
* default:
* $ref: '#/components/schemas/ApiErrorsV1'
*/
API.v1.post(
'custom-user-status.delete',
{ authRequired: true },
{
async post() {
const { customUserStatusId } = this.bodyParams;
if (!customUserStatusId) {
return API.v1.failure('The "customUserStatusId" params is required!');
}
authRequired: true,
body: isCustomUserStatusDeleteProps,
response: {
200: ajv.compile<void>({
type: 'object',
properties: {
success: {
type: 'boolean',
enum: [true],
},
},
required: ['success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function () {
const { customUserStatusId } = this.bodyParams;

await deleteCustomUserStatus(this.userId, customUserStatusId);
await deleteCustomUserStatus(this.userId, customUserStatusId);

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

API.v1.addRoute(
/**
* @openapi
* /api/v1/custom-user-status.update:
* post:
* description: Update an existing custom user status
* security:
* - cookieAuth: []
* - x-user-id: []
* - x-auth-token: []
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* _id:
* type: string
* name:
* type: string
* statusType:
* type: string
* required:
* - _id
* - name
* responses:
* 200:
* description: The updated custom user status
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiSuccessV1'
* default:
* $ref: '#/components/schemas/ApiErrorsV1'
*/
API.v1.post(
'custom-user-status.update',
{ authRequired: true },
{
async post() {
check(this.bodyParams, {
_id: String,
name: String,
statusType: Match.Maybe(String),
});

const userStatusData = {
_id: this.bodyParams._id,
name: this.bodyParams.name,
statusType: this.bodyParams.statusType,
};
{
authRequired: true,
body: isCustomUserStatusUpdateProps,
response: {
200: ajv.compile<{ customUserStatus: ICustomUserStatus }>({
type: 'object',
properties: {
customUserStatus: {
$ref: '#/components/schemas/ICustomUserStatus',
},
success: {
type: 'boolean',
enum: [true],
},
},
required: ['success', 'customUserStatus'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
},
},
async function () {
const { _id, name, statusType } = this.bodyParams;

const customUserStatusToUpdate = await CustomUserStatus.findOneById(userStatusData._id);
const customUserStatusToUpdate = await CustomUserStatus.findOneById(_id);

// Ensure the message exists
if (!customUserStatusToUpdate) {
return API.v1.failure(`No custom user status found with the id of "${userStatusData._id}".`);
}
// Ensure the message exists
if (!customUserStatusToUpdate) {
return API.v1.failure(`No custom user status found with the id of "${_id}".`);
}

await insertOrUpdateUserStatus(this.userId, userStatusData);
await insertOrUpdateUserStatus(this.userId, {
_id,
name: name || customUserStatusToUpdate.name,
statusType: statusType || customUserStatusToUpdate.statusType,
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 22, 2026

Choose a reason for hiding this comment

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

P2: Using || for update field fallback silently ignores explicit empty-string inputs, causing request payloads to be accepted but not applied.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/app/api/server/v1/custom-user-status.ts, line 335:

<comment>Using `||` for update field fallback silently ignores explicit empty-string inputs, causing request payloads to be accepted but not applied.</comment>

<file context>
@@ -120,95 +122,237 @@ const customUserStatusEndpoints = API.v1.get(
+		await insertOrUpdateUserStatus(this.userId, {
+			_id,
+			name: name || customUserStatusToUpdate.name,
+			statusType: statusType || customUserStatusToUpdate.statusType,
+			previousName: customUserStatusToUpdate.name,
+			previousStatusType: customUserStatusToUpdate.statusType,
</file context>
Suggested change
statusType: statusType || customUserStatusToUpdate.statusType,
statusType: statusType ?? customUserStatusToUpdate.statusType,
Fix with Cubic

previousName: customUserStatusToUpdate.name,
previousStatusType: customUserStatusToUpdate.statusType,
Comment on lines +332 to +337
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.

⚠️ Potential issue | 🟠 Major

Use ?? here to preserve empty-string updates.

|| treats '' as “not provided”, so a client can no longer clear statusType back to the empty-string value that create already uses. ?? keeps the old optional-field fallback without changing explicit empty-string inputs.

🔧 Proposed fix
 		await insertOrUpdateUserStatus(this.userId, {
 			_id,
-			name: name || customUserStatusToUpdate.name,
-			statusType: statusType || customUserStatusToUpdate.statusType,
+			name: name ?? customUserStatusToUpdate.name,
+			statusType: statusType ?? customUserStatusToUpdate.statusType,
 			previousName: customUserStatusToUpdate.name,
 			previousStatusType: customUserStatusToUpdate.statusType,
 		});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await insertOrUpdateUserStatus(this.userId, {
_id,
name: name || customUserStatusToUpdate.name,
statusType: statusType || customUserStatusToUpdate.statusType,
previousName: customUserStatusToUpdate.name,
previousStatusType: customUserStatusToUpdate.statusType,
await insertOrUpdateUserStatus(this.userId, {
_id,
name: name ?? customUserStatusToUpdate.name,
statusType: statusType ?? customUserStatusToUpdate.statusType,
previousName: customUserStatusToUpdate.name,
previousStatusType: customUserStatusToUpdate.statusType,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/api/server/v1/custom-user-status.ts` around lines 332 - 337,
Replace the || fallback for optional fields so explicit empty-string values are
preserved: in the call to insertOrUpdateUserStatus (inside this user-status
update block), use the nullish coalescing operator (??) for name and statusType
(e.g., name ?? customUserStatusToUpdate.name and statusType ??
customUserStatusToUpdate.statusType) while leaving previousName and
previousStatusType as-is; this ensures clients can set empty strings without
being overridden by the old values.

});

const customUserStatus = await CustomUserStatus.findOneById(userStatusData._id);
const customUserStatus = await CustomUserStatus.findOneById(_id);

if (!customUserStatus) {
throw new Meteor.Error('error-updating-custom-user-status', 'Error updating custom user status');
}
if (!customUserStatus) {
throw new Meteor.Error('error-updating-custom-user-status', 'Error updating custom user status');
}

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

export type CustomUserStatusEndpoints = ExtractRoutesFromAPI<typeof customUserStatusEndpoints>;
// Note for Mentors:
// The circular reference error (Type alias 'CustomUserStatusEndpoints' circularly references itself)
// was resolved by removing the redundant server-side re-definition of CustomUserStatusEndpoints
// and the manual module augmentation. The endpoint types are now correctly resolved through the
// global `Endpoints` interface provided by the `@rocket.chat/rest-typings` package, following
// the project's modern API migration patterns.

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends CustomUserStatusEndpoints {}
}
2 changes: 1 addition & 1 deletion packages/rest-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ export * from './v1/videoConference';
export * from './v1/assets';
export * from './v1/channels';
export * from './v1/customSounds';
export type * from './v1/customUserStatus';
export * from './v1/customUserStatus';
export * from './v1/subscriptionsEndpoints';
export type * from './v1/mailer';
export * from './v1/mailer/MailerParamsPOST';
Expand Down
Loading