Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f227c81
feat(rest-typings): migrate four user endpoints to Ajv
Harshit2405-2004 Mar 1, 2026
a64fa34
chore: add changeset for users REST param validations migrations
Harshit2405-2004 Mar 1, 2026
da0ff36
refactor(rest-typings): remove duplicate ajv validator exports
Harshit2405-2004 Mar 1, 2026
57f2327
fix(rest-typings): address validation schema gaps in users REST API
Harshit2405-2004 Mar 1, 2026
075af9c
feat(api): Enforce Schema Validation on groups.list and groups.listAll
Harshit2405-2004 Mar 1, 2026
e77c2b1
fix(groups): allow
Harshit2405-2004 Mar 1, 2026
163b463
refactor(rest-typings): enforce strict mutual exclusivity for user av…
Harshit2405-2004 Mar 2, 2026
f90fe50
refactor(rest-typings): align users.getAvatar signature with strict u…
Harshit2405-2004 Mar 2, 2026
d9fc7e1
fix(rest-typings): remove unused IUser import in UsersGetAvatarParamsGET
Harshit2405-2004 Mar 2, 2026
82cf45a
fix: resolve CI failures and fix indentation in groups REST API
Harshit2405-2004 Mar 2, 2026
d853e09
chore: ignore .agent and .optimization folders local to development
Harshit2405-2004 Mar 2, 2026
ac9cfed
fix(api): resolve lint failures in REST typings and ignore local meta…
Harshit2405-2004 Mar 2, 2026
f9548ca
Merge branch 'develop' into feat-groups-list-validation
Harshit2405-2004 Mar 2, 2026
30e898c
fix(rest-typings): fix Prettier formatting in users.ts
Harshit2405-2004 Mar 2, 2026
d2ffb42
Merge branch 'develop' into feat-groups-list-validation
Harshit2405-2004 Mar 5, 2026
70b41c5
Merge branch 'develop' into feat-groups-list-validation
Harshit2405-2004 Mar 13, 2026
5f61088
Merge branch 'develop' into feat-groups-list-validation
Harshit2405-2004 Mar 18, 2026
5141e35
Merge branch 'develop' into feat-groups-list-validation
ggazzo Mar 18, 2026
a08e036
Merge branch 'develop' into feat-groups-list-validation
Harshit2405-2004 Mar 22, 2026
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/add-users-rest-param-validations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Migrated `users.getAvatar`, `users.deleteOwnAccount`, `users.resetAvatar`, and `users.forgotPassword` to explicit `Ajv` schema validation. Similarly, `groups.list` and `groups.listAll` were migrated to ensure strict parameter validation.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ storybook-static
development/tempo-data/

.env

# Antigravity local folders
.agent/
.optimization/
18 changes: 9 additions & 9 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Team, isMeteorError } from '@rocket.chat/core-services';
import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings';
import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models';
import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps } from '@rocket.chat/rest-typings';
import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps, isGroupsListProps } from '@rocket.chat/rest-typings';
import { isTruthy } from '@rocket.chat/tools';
import { check, Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -81,12 +81,12 @@ async function findPrivateGroupByIdOrName({
userId,
}: {
params:
| {
roomId?: string;
}
| {
roomName?: string;
};
| {
roomId?: string;
}
| {
roomName?: string;
};
userId: string;
checkedArchived?: boolean;
}): Promise<{
Expand Down Expand Up @@ -653,7 +653,7 @@ API.v1.addRoute(
// List Private Groups a user has access to
API.v1.addRoute(
'groups.list',
{ authRequired: true },
{ authRequired: true, validateParams: isGroupsListProps },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
Expand Down Expand Up @@ -692,7 +692,7 @@ API.v1.addRoute(

API.v1.addRoute(
'groups.listAll',
{ authRequired: true, permissionsRequired: ['view-room-administration'] },
{ authRequired: true, permissionsRequired: ['view-room-administration'], validateParams: isGroupsListProps },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
Expand Down
63 changes: 36 additions & 27 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import {
ajv,
validateBadRequestErrorResponse,
validateUnauthorizedErrorResponse,
isUsersGetAvatarProps,
isUsersDeleteOwnAccountProps,
isUsersResetAvatarProps,
isUsersForgotPasswordProps,
} from '@rocket.chat/rest-typings';
import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools';
import { Accounts } from 'meteor/accounts-base';
Expand Down Expand Up @@ -83,7 +87,10 @@ import { findPaginatedUsersByStatus, findUsersToAutocomplete, getInclusiveFields

API.v1.addRoute(
'users.getAvatar',
{ authRequired: true },
{
authRequired: true,
validateParams: isUsersGetAvatarProps,
},
{
async get() {
const user = await getUserFromParams(this.queryParams);
Expand Down Expand Up @@ -172,9 +179,9 @@ API.v1.addRoute(
const twoFactorOptions = !userData.typedPassword
? null
: {
twoFactorCode: userData.typedPassword,
twoFactorMethod: 'password',
};
twoFactorCode: userData.typedPassword,
twoFactorMethod: 'password',
};

await executeSaveUserProfile.call(this, this.user, userData, this.bodyParams.customFields, twoFactorOptions);

Expand Down Expand Up @@ -362,19 +369,18 @@ API.v1.addRoute(

API.v1.addRoute(
'users.deleteOwnAccount',
{ authRequired: true },
{
authRequired: true,
validateParams: isUsersDeleteOwnAccountProps,
},
{
async post() {
const { password } = this.bodyParams;
if (!password) {
return API.v1.failure('Body parameter "password" is required.');
}
const { password, confirmRelinquish = false } = this.bodyParams;

if (!settings.get('Accounts_AllowDeleteOwnAccount')) {
throw new Meteor.Error('error-not-allowed', 'Not allowed');
}

const { confirmRelinquish = false } = this.bodyParams;

await deleteUserOwnAccount(this.userId, password, confirmRelinquish);

return API.v1.success();
Expand Down Expand Up @@ -537,13 +543,18 @@ API.v1.addRoute(
const limit =
count !== 0
? [
{
$limit: count,
},
]
{
$limit: count,
},
]
: [];

const result = await Users.col
const [
{
sortedResults: users,
totalCount: [{ total } = { total: 0 }],
} = { sortedResults: [], totalCount: [] },
] = await Users.col
.aggregate<{ sortedResults: IUser[]; totalCount: { total: number }[] }>([
{
$match: nonEmptyQuery,
Expand Down Expand Up @@ -575,11 +586,6 @@ API.v1.addRoute(
])
.toArray();

const {
sortedResults: users,
totalCount: [{ total } = { total: 0 }],
} = result[0];

return API.v1.success({
users,
count: users.length,
Expand Down Expand Up @@ -730,7 +736,10 @@ API.v1.addRoute(

API.v1.addRoute(
'users.resetAvatar',
{ authRequired: true },
{
authRequired: true,
validateParams: isUsersResetAvatarProps,
},
{
async post() {
const user = await getUserFromParams(this.bodyParams);
Expand Down Expand Up @@ -901,7 +910,10 @@ API.v1.addRoute(

API.v1.addRoute(
'users.forgotPassword',
{ authRequired: false },
{
authRequired: false,
validateParams: isUsersForgotPasswordProps,
},
{
async post() {
const isPasswordResetEnabled = settings.get('Accounts_PasswordReset');
Expand All @@ -911,9 +923,6 @@ API.v1.addRoute(
}

const { email } = this.bodyParams;
if (!email) {
return API.v1.failure("The 'email' param is required");
}

await sendForgotPasswordEmail(email.toLowerCase());
return API.v1.success();
Expand Down Expand Up @@ -1559,5 +1568,5 @@ type UsersEndpoints = ExtractRoutesFromAPI<typeof usersEndpoints>;

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 { }
}
2 changes: 1 addition & 1 deletion packages/rest-typings/src/v1/Ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ addFormats(ajvQuery);
ajv.addFormat('basic_email', /^[^@]+@[^@]+$/);
ajv.addFormat(
'rfc_email',
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
);
ajvQuery.addFormat('basic_email', /^[^@]+@[^@]+$/);
ajvQuery.addFormat(
Expand Down
36 changes: 34 additions & 2 deletions packages/rest-typings/src/v1/groups/GroupsListProps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import type { PaginatedRequest } from '../../helpers/PaginatedRequest';
import { ajv } from '../Ajv';

export type GroupsListProps = PaginatedRequest<null>;
const groupsListPropsSchema = {};
export type GroupsListProps = PaginatedRequest<{ name?: string }>;

const groupsListPropsSchema = {
type: 'object',
properties: {
count: {
type: 'number',
nullable: true,
},
offset: {
type: 'number',
nullable: true,
},
sort: {
type: 'string',
nullable: true,
},
fields: {
type: 'string',
nullable: true,
},
query: {
type: 'string',
nullable: true,
},
name: {
type: 'string',
nullable: true,
},
},
required: [],
additionalProperties: false,
};

export const isGroupsListProps = ajv.compile<GroupsListProps>(groupsListPropsSchema);
50 changes: 49 additions & 1 deletion packages/rest-typings/src/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import type { UserLogoutParamsPOST } from './users/UserLogoutParamsPOST';
import type { UserRegisterParamsPOST } from './users/UserRegisterParamsPOST';
import type { UserSetActiveStatusParamsPOST } from './users/UserSetActiveStatusParamsPOST';
import type { UsersAutocompleteParamsGET } from './users/UsersAutocompleteParamsGET';
import type { UsersDeleteOwnAccountParamsPOST } from './users/UsersDeleteOwnAccountParamsPOST';
import type { UsersForgotPasswordParamsPOST } from './users/UsersForgotPasswordParamsPOST';
import type { UsersGetAvatarParamsGET } from './users/UsersGetAvatarParamsGET';
import type { UsersInfoParamsGet } from './users/UsersInfoParamsGet';
import type { UsersListStatusParamsGET } from './users/UsersListStatusParamsGET';
import type { UsersListTeamsParamsGET } from './users/UsersListTeamsParamsGET';
Expand All @@ -18,6 +21,48 @@ import type { UsersSetPreferencesParamsPOST } from './users/UsersSetPreferencePa
import type { UsersUpdateOwnBasicInfoParamsPOST } from './users/UsersUpdateOwnBasicInfoParamsPOST';
import type { UsersUpdateParamsPOST } from './users/UsersUpdateParamsPOST';

export const isUsersGetAvatarProps = ajv.compile<UsersGetAvatarParamsGET>({
oneOf: [
{
type: 'object',
properties: { userId: { type: 'string' } },
required: ['userId'],
additionalProperties: false,
},
{
type: 'object',
properties: { username: { type: 'string' } },
required: ['username'],
additionalProperties: false,
},
{
type: 'object',
properties: { user: { type: 'string' } },
required: ['user'],
additionalProperties: false,
},
],
});

export const isUsersDeleteOwnAccountProps = ajv.compile<UsersDeleteOwnAccountParamsPOST>({
type: 'object',
properties: {
password: { type: 'string', minLength: 1 },
confirmRelinquish: { type: 'boolean' },
},
required: ['password'],
additionalProperties: false,
});

export const isUsersForgotPasswordProps = ajv.compile<UsersForgotPasswordParamsPOST>({
type: 'object',
properties: {
email: { type: 'string', minLength: 1 },
},
required: ['email'],
additionalProperties: false,
});

type UsersInfo = { userId?: IUser['_id']; username?: IUser['username'] };

const UsersInfoSchema = {
Expand Down Expand Up @@ -356,7 +401,7 @@ export type UsersEndpoints = {
};

'/v1/users.getAvatar': {
GET: (params: { userId?: string; username?: string; user?: string }) => void;
GET: (params: UsersGetAvatarParamsGET) => void;
};

'/v1/users.updateOwnBasicInfo': {
Expand All @@ -380,3 +425,6 @@ export * from './users/UserRegisterParamsPOST';
export * from './users/UserLogoutParamsPOST';
export * from './users/UsersListTeamsParamsGET';
export * from './users/UsersAutocompleteParamsGET';
export * from './users/UsersGetAvatarParamsGET';
export * from './users/UsersDeleteOwnAccountParamsPOST';
export * from './users/UsersForgotPasswordParamsPOST';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UsersDeleteOwnAccountParamsPOST = {
password: string;
confirmRelinquish?: boolean;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type UsersForgotPasswordParamsPOST = {
email: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type UsersGetAvatarParamsGET =
| { userId: string; username?: never; user?: never }
| { username: string; userId?: never; user?: never }
| { user: string; userId?: never; username?: never };