Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
7a75a9d
ci: fix dedupe workflow claude action inputs and ignore auth errors
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
6253b4f
fix: address review feedback from CodeRabbit and ggazzo
Harshit2405-2004 Mar 2, 2026
0929d43
refactor(rest-typings): enforce strict mutual exclusivity for user av…
Harshit2405-2004 Mar 2, 2026
36b9b33
refactor(rest-typings): align users.getAvatar signature with strict u…
Harshit2405-2004 Mar 2, 2026
f65a9b3
fix(rest-typings): remove unused IUser import in UsersGetAvatarParamsGET
Harshit2405-2004 Mar 2, 2026
272dcd1
chore: cleanup changeset and remove unrelated groups mention
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
49e031f
Merge branch 'feat-groups-list-validation' into feat-rest-typings-vid…
Harshit2405-2004 Mar 2, 2026
5ed98d1
fix(rest-typings): fix Prettier formatting in users.ts
Harshit2405-2004 Mar 2, 2026
a0ca08c
Merge branch 'develop' into feat-rest-typings-video-conference
Harshit2405-2004 Mar 5, 2026
897c750
Merge branch 'develop' into feat-rest-typings-video-conference
Harshit2405-2004 Mar 13, 2026
79b7a86
Merge branch 'develop' into feat-rest-typings-video-conference
Harshit2405-2004 Mar 18, 2026
a4a44ae
Merge branch 'develop' into feat-rest-typings-video-conference
Harshit2405-2004 Mar 18, 2026
9ba13b2
Merge branch 'develop' into feat-rest-typings-video-conference
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': patch
'@rocket.chat/meteor': patch
---

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;
Comment thread
Harshit2405-2004 marked this conversation as resolved.

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,
Comment thread
Harshit2405-2004 marked this conversation as resolved.
},
{
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 };