From d74297b93301cea18f785ba3127771ba56a64ac4 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Tue, 14 Oct 2025 13:10:44 +0530 Subject: [PATCH 01/15] refactor: voip user states rename --- apps/meteor/client/lib/voip/EEVoipClient.ts | 4 ++-- apps/meteor/client/lib/voip/VoIPUser.ts | 12 ++++++------ .../client/providers/CallProvider/CallProvider.tsx | 4 ++-- .../client/views/admin/users/AdminUsersPage.tsx | 2 +- packages/core-typings/src/IUser.ts | 4 ++++ packages/core-typings/src/UserState.ts | 0 packages/core-typings/src/voip/ICallDetails.ts | 4 ++-- .../src/voip/{UserState.ts => VoIPUserState.ts} | 2 +- packages/core-typings/src/voip/VoIpCallerInfo.ts | 6 +++--- packages/core-typings/src/voip/index.ts | 2 +- 10 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 packages/core-typings/src/UserState.ts rename packages/core-typings/src/voip/{UserState.ts => VoIPUserState.ts} (81%) diff --git a/apps/meteor/client/lib/voip/EEVoipClient.ts b/apps/meteor/client/lib/voip/EEVoipClient.ts index 5f3496ed317c5..750c47328fe54 100644 --- a/apps/meteor/client/lib/voip/EEVoipClient.ts +++ b/apps/meteor/client/lib/voip/EEVoipClient.ts @@ -1,5 +1,5 @@ import type { ICallerInfo, IMediaStreamRenderer, VoIPUserConfiguration } from '@rocket.chat/core-typings'; -import { Operation, UserState } from '@rocket.chat/core-typings'; +import { Operation, VoIPUserState } from '@rocket.chat/core-typings'; import { Inviter, UserAgent } from 'sip.js'; import type { IncomingResponse } from 'sip.js/lib/core'; @@ -58,7 +58,7 @@ export class EEVoipClient extends VoIPUser { host: inviter.remoteIdentity.uri.host, }; this._callerInfo = callerInfo; - this._userState = UserState.UAC; + this._userState = VoIPUserState.UAC; this.emit('stateChanged'); } diff --git a/apps/meteor/client/lib/voip/VoIPUser.ts b/apps/meteor/client/lib/voip/VoIPUser.ts index 5e74ae2cbd050..262fb769b4ae8 100644 --- a/apps/meteor/client/lib/voip/VoIPUser.ts +++ b/apps/meteor/client/lib/voip/VoIPUser.ts @@ -19,7 +19,7 @@ import type { IState, VoipEvents, } from '@rocket.chat/core-typings'; -import { Operation, UserState, WorkflowTypes } from '@rocket.chat/core-typings'; +import { Operation, VoIPUserState, WorkflowTypes } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { UserAgentOptions, InvitationAcceptOptions, Session, SessionInviteOptions } from 'sip.js'; import { UserAgent, Invitation, SessionState, Registerer, RequestPendingError, Inviter } from 'sip.js'; @@ -79,7 +79,7 @@ export class VoIPUser extends Emitter { protected _callerInfo: ICallerInfo | undefined; - protected _userState: UserState = UserState.IDLE; + protected _userState: VoIPUserState = VoIPUserState.IDLE; protected _opInProgress: Operation = Operation.OP_NONE; @@ -87,7 +87,7 @@ export class VoIPUser extends Emitter { return this._opInProgress; } - get userState(): UserState | undefined { + get userState(): VoIPUserState | undefined { return this._userState; } @@ -298,7 +298,7 @@ export class VoIPUser extends Emitter { if (this.callState === 'REGISTERED') { this._opInProgress = Operation.OP_PROCESS_INVITE; this._callState = 'OFFER_RECEIVED'; - this._userState = UserState.UAS; + this._userState = VoIPUserState.UAS; this.session = invitation; this.setupSessionEventHandlers(invitation); const callerInfo: ICallerInfo = { @@ -338,7 +338,7 @@ export class VoIPUser extends Emitter { this.emit('ringing', { userState: this._userState, callInfo: this._callerInfo }); break; case SessionState.Established: - if (this._userState === UserState.UAC) { + if (this._userState === VoIPUserState.UAC) { /** * We need to decide about user-state ANSWER-RECEIVED for outbound. * This state is there for the symmetry of ANSWER-SENT. @@ -369,7 +369,7 @@ export class VoIPUser extends Emitter { this.session = undefined; this._callState = 'REGISTERED'; this._opInProgress = Operation.OP_NONE; - this._userState = UserState.IDLE; + this._userState = VoIPUserState.IDLE; this.emit('callterminated'); this.remoteStream?.clear(); this.emit('stateChanged'); diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index 7448b4160aa8c..416b150ca42e4 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -7,7 +7,7 @@ import { isVoipEventQueueMemberAdded, isVoipEventQueueMemberRemoved, isVoipEventCallAbandoned, - UserState, + VoIPUserState, } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { Random } from '@rocket.chat/random'; @@ -339,7 +339,7 @@ export const CallProvider = ({ children }: CallProviderProps) => { voipSounds.stopAll(); - if (callDetails.userState !== UserState.UAC) { + if (callDetails.userState !== VoIPUserState.UAC) { return; } // Agent has sent Invite. So it must create a room. diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 46ed046fc5e1e..40a86e9d5550c 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -77,7 +77,7 @@ const AdminUsersPage = (): ReactElement => { }); const pendingUsersCount = usePendingUsersCount(filteredUsersQueryResult.data?.users); - + console.log('pendingUsersCount', pendingUsersCount.data); const handleReload = (): void => { seatsCap?.reload(); filteredUsersQueryResult?.refetch(); diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index f725b2738cf92..aa1e74b6a5a72 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -183,6 +183,9 @@ export interface IGetRoomRoles { roles: string[]; } +// + +export type IUserState = 'pending_approval' | 'active' | 'deactivated' | 'inactive'; export interface IUser extends IRocketChatRecord { _id: string; createdAt: Date; @@ -252,6 +255,7 @@ export interface IUser extends IRocketChatRecord { roomRolePriorities?: Record; isOAuthUser?: boolean; // client only field __rooms?: string[]; + // state: IUserState; } export interface IRegisterUser extends IUser { diff --git a/packages/core-typings/src/UserState.ts b/packages/core-typings/src/UserState.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/core-typings/src/voip/ICallDetails.ts b/packages/core-typings/src/voip/ICallDetails.ts index 0f17fea308cce..9bf650b05e146 100644 --- a/packages/core-typings/src/voip/ICallDetails.ts +++ b/packages/core-typings/src/voip/ICallDetails.ts @@ -1,7 +1,7 @@ import type { ICallerInfo } from './ICallerInfo'; -import type { UserState } from './UserState'; +import type { VoIPUserState } from './VoIPUserState'; export interface ICallDetails { callInfo?: ICallerInfo; - userState: UserState; + userState: VoIPUserState; } diff --git a/packages/core-typings/src/voip/UserState.ts b/packages/core-typings/src/voip/VoIPUserState.ts similarity index 81% rename from packages/core-typings/src/voip/UserState.ts rename to packages/core-typings/src/voip/VoIPUserState.ts index ff704108defda..61655ac5a103f 100644 --- a/packages/core-typings/src/voip/UserState.ts +++ b/packages/core-typings/src/voip/VoIPUserState.ts @@ -1,6 +1,6 @@ // User state is based on whether the User has sent an invite(UAC) or it // has received an invite (UAS) -export enum UserState { +export enum VoIPUserState { IDLE, UAC, UAS, diff --git a/packages/core-typings/src/voip/VoIpCallerInfo.ts b/packages/core-typings/src/voip/VoIpCallerInfo.ts index ea0af8a1d955a..e3b3c8f824cc7 100644 --- a/packages/core-typings/src/voip/VoIpCallerInfo.ts +++ b/packages/core-typings/src/voip/VoIpCallerInfo.ts @@ -1,6 +1,6 @@ import type { CallStates } from './CallStates'; import type { ICallerInfo } from './ICallerInfo'; -import type { UserState } from './UserState'; +import type { VoIPUserState } from './VoIPUserState'; export interface IState { isReady: boolean; @@ -10,10 +10,10 @@ export interface IState { export type VoIpCallerInfo = | { state: Exclude; - userState: UserState; + userState: VoIPUserState; } | { state: 'IN_CALL' | 'ON_HOLD' | 'OFFER_RECEIVED' | 'OFFER_SENT'; - userState: UserState; + userState: VoIPUserState; caller: ICallerInfo; }; // TODO: Check for additional properties and States (E.g. call on hold, muted, etc) diff --git a/packages/core-typings/src/voip/index.ts b/packages/core-typings/src/voip/index.ts index d7f2f37c79df7..3154d6870c1d8 100644 --- a/packages/core-typings/src/voip/index.ts +++ b/packages/core-typings/src/voip/index.ts @@ -9,7 +9,7 @@ export * from './IRegisterHandlerDelegate'; export * from './IRegistrationInfo'; export * from './Operations'; export * from './SignalingSocketEvents'; -export * from './UserState'; +export * from './VoIPUserState'; export * from './VoipClientEvents'; export * from './VoipEvents'; export * from './WorkflowTypes'; From 23ce5683515f08b6d6237ee3eeb65366733372b2 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Tue, 14 Oct 2025 23:44:16 +0530 Subject: [PATCH 02/15] feat: user state --- .../authentication/server/startup/index.js | 18 +++++++---- .../server/functions/setUserActiveStatus.ts | 8 ++--- .../server/functions/getBaseUserFields.ts | 1 + packages/core-typings/src/IUser.ts | 32 ++++++++++++++++--- packages/core-typings/src/UserState.ts | 6 ++++ packages/core-typings/src/index.ts | 1 + 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index a56f237c6e527..f3c39ac021e6e 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -1,5 +1,6 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { User } from '@rocket.chat/core-services'; +import { isUserActive, UserState } from '@rocket.chat/core-typings'; import { Roles, Settings, Users } from '@rocket.chat/models'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { getLoginExpirationInDays } from '@rocket.chat/tools'; @@ -74,18 +75,18 @@ Accounts.emailTemplates.userToActivate = { }; Accounts.emailTemplates.userActivated = { - subject({ active, username }) { + subject({ active, username, state }) { const activated = username ? 'Activated' : 'Approved'; - const action = active ? activated : 'Deactivated'; + const action = isUserActive({ state }) ? activated : 'Deactivated'; const subject = `Accounts_Email_${action}_Subject`; const siteName = settings.get('Site_Name'); return `[${siteName}] ${i18n.t(subject)}`; }, - html({ active, name, username }) { + html({ active, name, username, state }) { const activated = username ? 'Activated' : 'Approved'; - const action = active ? activated : 'Deactivated'; + const action = isUserActive({ state }) ? activated : 'Deactivated'; return Mailer.replace(i18n.t(`Accounts_Email_${action}`), { name: escapeHTML(name), @@ -204,7 +205,10 @@ const onCreateUserAsync = async function (options, user = {}) { } user.status = 'offline'; - user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); + // user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); + + const userStateForManualApproval = settings.get('Accounts_ManuallyApproveNewUsers') ? UserState.PENDING_APPROVAL : UserState.ACTIVE; + user.state = user.state !== undefined ? user.state : userStateForManualApproval; if (!user.name) { if (options.profile) { @@ -236,7 +240,7 @@ const onCreateUserAsync = async function (options, user = {}) { } } - if (!options.skipAdminEmail && !user.active) { + if (!options.skipAdminEmail && !isUserActive(user)) { const destinations = []; const usersInRole = await Roles.findUsersInRole('admin'); await usersInRole.forEach((adminUser) => { @@ -415,7 +419,7 @@ const validateLoginAttemptAsync = async function (login) { }); } - if (!!login.user.active !== true) { + if (!isUserActive(login.user)) { throw new Meteor.Error('error-user-is-not-activated', 'User is not activated', { function: 'Accounts.validateLoginAttempt', }); diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index fa59df01ba589..51a29121aa511 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -1,4 +1,4 @@ -import type { IUser, IUserEmail } from '@rocket.chat/core-typings'; +import type { IUser, IUserEmail, UserState } from '@rocket.chat/core-typings'; import { isUserFederated, isDirectMessageRoom } from '@rocket.chat/core-typings'; import { Rooms, Users, Subscriptions } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; @@ -145,14 +145,14 @@ export async function setUserActiveStatus( const destinations = user.emails.map((email: IUserEmail) => `${user.name || user.username}<${email.address}>`); type UserActivated = { - subject: (params: { active: boolean }) => string; - html: (params: { active: boolean; name: string; username: string }) => string; + subject: (params: { active: boolean; username: string; state: UserState }) => string; + html: (params: { active: boolean; name: string; username: string; state: UserState }) => string; }; const { subject, html } = (Accounts.emailTemplates as unknown as { userActivated: UserActivated }).userActivated; const email = { to: String(destinations), from: String(settings.get('From_Email')), - subject: subject({ active } as any), + subject: subject({ active, username: user.username, state: user.state } as any), html: html({ active, name: user.name, diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts index a366f95a2fe54..358595ca875f6 100644 --- a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts @@ -31,5 +31,6 @@ export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({ 'avatarETag': 1, 'extension': 1, 'openBusinessHours': 1, + 'state': 1, ...(allowServiceKeys && { 'services.totp.enabled': 1, 'services.email2fa.enabled': 1 }), }); diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index aa1e74b6a5a72..a439553c9b17d 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -1,6 +1,7 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IRole } from './IRole'; import type { Serialized } from './Serialized'; +import { UserState } from './UserState'; import type { UserStatus } from './UserStatus'; export interface ILoginToken { @@ -183,9 +184,6 @@ export interface IGetRoomRoles { roles: string[]; } -// - -export type IUserState = 'pending_approval' | 'active' | 'deactivated' | 'inactive'; export interface IUser extends IRocketChatRecord { _id: string; createdAt: Date; @@ -255,9 +253,35 @@ export interface IUser extends IRocketChatRecord { roomRolePriorities?: Record; isOAuthUser?: boolean; // client only field __rooms?: string[]; - // state: IUserState; + state?: UserState; +} + +export interface IActiveUser extends IUser { + state: UserState.ACTIVE; +} +export interface IDeactivatedUser extends IUser { + state: UserState.DEACTIVATED; +} + +export interface IInactiveUser extends IUser { + state: UserState.INACTIVE; } +export interface IPendingApprovalUser extends IUser { + state: UserState.PENDING_APPROVAL; +} + +export const isUserActive = (user: Partial | Partial>): user is IActiveUser => user.state === UserState.ACTIVE; + +export const isUserDeactivated = (user: Partial | Partial>): user is IDeactivatedUser => + user.state === UserState.DEACTIVATED; + +export const isUserInactive = (user: Partial | Partial>): user is IInactiveUser => + user.state === UserState.INACTIVE; + +export const isUserPendingApproval = (user: Partial | Partial>): user is IPendingApprovalUser => + user.state === UserState.PENDING_APPROVAL; + export interface IRegisterUser extends IUser { username: string; name: string; diff --git a/packages/core-typings/src/UserState.ts b/packages/core-typings/src/UserState.ts index e69de29bb2d1d..affa4f3b1cc74 100644 --- a/packages/core-typings/src/UserState.ts +++ b/packages/core-typings/src/UserState.ts @@ -0,0 +1,6 @@ +export enum UserState { + PENDING_APPROVAL = 'pending_approval', + ACTIVE = 'active', + DEACTIVATED = 'deactivated', + INACTIVE = 'inactive', +} diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 7e8bba71d6b02..19c64ba83ee81 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -19,6 +19,7 @@ export * from './RoomType'; export * from './IInvite'; export * from './IRocketChatRecord'; export * from './UserStatus'; +export * from './UserState'; export * from './IUserAction'; export * from './IBanner'; export * from './IStats'; From b660ecf5f048fb3550b09c0ce1ccdaa1cc346f00 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 16 Oct 2025 22:27:24 +0530 Subject: [PATCH 03/15] Revert "feat: user state" This reverts commit 410a0a3b5121e101f8b144aaafd050aae28cc494. --- .../authentication/server/startup/index.js | 18 ++++------- .../server/functions/setUserActiveStatus.ts | 8 ++--- .../server/functions/getBaseUserFields.ts | 1 - packages/core-typings/src/IUser.ts | 32 +++---------------- packages/core-typings/src/UserState.ts | 6 ---- packages/core-typings/src/index.ts | 1 - 6 files changed, 15 insertions(+), 51 deletions(-) diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index f3c39ac021e6e..a56f237c6e527 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -1,6 +1,5 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { User } from '@rocket.chat/core-services'; -import { isUserActive, UserState } from '@rocket.chat/core-typings'; import { Roles, Settings, Users } from '@rocket.chat/models'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { getLoginExpirationInDays } from '@rocket.chat/tools'; @@ -75,18 +74,18 @@ Accounts.emailTemplates.userToActivate = { }; Accounts.emailTemplates.userActivated = { - subject({ active, username, state }) { + subject({ active, username }) { const activated = username ? 'Activated' : 'Approved'; - const action = isUserActive({ state }) ? activated : 'Deactivated'; + const action = active ? activated : 'Deactivated'; const subject = `Accounts_Email_${action}_Subject`; const siteName = settings.get('Site_Name'); return `[${siteName}] ${i18n.t(subject)}`; }, - html({ active, name, username, state }) { + html({ active, name, username }) { const activated = username ? 'Activated' : 'Approved'; - const action = isUserActive({ state }) ? activated : 'Deactivated'; + const action = active ? activated : 'Deactivated'; return Mailer.replace(i18n.t(`Accounts_Email_${action}`), { name: escapeHTML(name), @@ -205,10 +204,7 @@ const onCreateUserAsync = async function (options, user = {}) { } user.status = 'offline'; - // user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); - - const userStateForManualApproval = settings.get('Accounts_ManuallyApproveNewUsers') ? UserState.PENDING_APPROVAL : UserState.ACTIVE; - user.state = user.state !== undefined ? user.state : userStateForManualApproval; + user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); if (!user.name) { if (options.profile) { @@ -240,7 +236,7 @@ const onCreateUserAsync = async function (options, user = {}) { } } - if (!options.skipAdminEmail && !isUserActive(user)) { + if (!options.skipAdminEmail && !user.active) { const destinations = []; const usersInRole = await Roles.findUsersInRole('admin'); await usersInRole.forEach((adminUser) => { @@ -419,7 +415,7 @@ const validateLoginAttemptAsync = async function (login) { }); } - if (!isUserActive(login.user)) { + if (!!login.user.active !== true) { throw new Meteor.Error('error-user-is-not-activated', 'User is not activated', { function: 'Accounts.validateLoginAttempt', }); diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index 51a29121aa511..fa59df01ba589 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -1,4 +1,4 @@ -import type { IUser, IUserEmail, UserState } from '@rocket.chat/core-typings'; +import type { IUser, IUserEmail } from '@rocket.chat/core-typings'; import { isUserFederated, isDirectMessageRoom } from '@rocket.chat/core-typings'; import { Rooms, Users, Subscriptions } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; @@ -145,14 +145,14 @@ export async function setUserActiveStatus( const destinations = user.emails.map((email: IUserEmail) => `${user.name || user.username}<${email.address}>`); type UserActivated = { - subject: (params: { active: boolean; username: string; state: UserState }) => string; - html: (params: { active: boolean; name: string; username: string; state: UserState }) => string; + subject: (params: { active: boolean }) => string; + html: (params: { active: boolean; name: string; username: string }) => string; }; const { subject, html } = (Accounts.emailTemplates as unknown as { userActivated: UserActivated }).userActivated; const email = { to: String(destinations), from: String(settings.get('From_Email')), - subject: subject({ active, username: user.username, state: user.state } as any), + subject: subject({ active } as any), html: html({ active, name: user.name, diff --git a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts index 358595ca875f6..a366f95a2fe54 100644 --- a/apps/meteor/app/utils/server/functions/getBaseUserFields.ts +++ b/apps/meteor/app/utils/server/functions/getBaseUserFields.ts @@ -31,6 +31,5 @@ export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({ 'avatarETag': 1, 'extension': 1, 'openBusinessHours': 1, - 'state': 1, ...(allowServiceKeys && { 'services.totp.enabled': 1, 'services.email2fa.enabled': 1 }), }); diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index a439553c9b17d..aa1e74b6a5a72 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -1,7 +1,6 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; import type { IRole } from './IRole'; import type { Serialized } from './Serialized'; -import { UserState } from './UserState'; import type { UserStatus } from './UserStatus'; export interface ILoginToken { @@ -184,6 +183,9 @@ export interface IGetRoomRoles { roles: string[]; } +// + +export type IUserState = 'pending_approval' | 'active' | 'deactivated' | 'inactive'; export interface IUser extends IRocketChatRecord { _id: string; createdAt: Date; @@ -253,35 +255,9 @@ export interface IUser extends IRocketChatRecord { roomRolePriorities?: Record; isOAuthUser?: boolean; // client only field __rooms?: string[]; - state?: UserState; -} - -export interface IActiveUser extends IUser { - state: UserState.ACTIVE; -} -export interface IDeactivatedUser extends IUser { - state: UserState.DEACTIVATED; -} - -export interface IInactiveUser extends IUser { - state: UserState.INACTIVE; + // state: IUserState; } -export interface IPendingApprovalUser extends IUser { - state: UserState.PENDING_APPROVAL; -} - -export const isUserActive = (user: Partial | Partial>): user is IActiveUser => user.state === UserState.ACTIVE; - -export const isUserDeactivated = (user: Partial | Partial>): user is IDeactivatedUser => - user.state === UserState.DEACTIVATED; - -export const isUserInactive = (user: Partial | Partial>): user is IInactiveUser => - user.state === UserState.INACTIVE; - -export const isUserPendingApproval = (user: Partial | Partial>): user is IPendingApprovalUser => - user.state === UserState.PENDING_APPROVAL; - export interface IRegisterUser extends IUser { username: string; name: string; diff --git a/packages/core-typings/src/UserState.ts b/packages/core-typings/src/UserState.ts index affa4f3b1cc74..e69de29bb2d1d 100644 --- a/packages/core-typings/src/UserState.ts +++ b/packages/core-typings/src/UserState.ts @@ -1,6 +0,0 @@ -export enum UserState { - PENDING_APPROVAL = 'pending_approval', - ACTIVE = 'active', - DEACTIVATED = 'deactivated', - INACTIVE = 'inactive', -} diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 19c64ba83ee81..7e8bba71d6b02 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -19,7 +19,6 @@ export * from './RoomType'; export * from './IInvite'; export * from './IRocketChatRecord'; export * from './UserStatus'; -export * from './UserState'; export * from './IUserAction'; export * from './IBanner'; export * from './IStats'; From a1054af2c335c5bbf48ad083b5d12c97d2f8c487 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 16 Oct 2025 22:27:44 +0530 Subject: [PATCH 04/15] Revert "refactor: voip user states rename" This reverts commit c4b2f5bffa8b5a272d08654232a66b147b481869. --- apps/meteor/client/lib/voip/EEVoipClient.ts | 4 ++-- apps/meteor/client/lib/voip/VoIPUser.ts | 12 ++++++------ .../client/providers/CallProvider/CallProvider.tsx | 4 ++-- .../client/views/admin/users/AdminUsersPage.tsx | 2 +- packages/core-typings/src/IUser.ts | 4 ---- packages/core-typings/src/UserState.ts | 0 packages/core-typings/src/voip/ICallDetails.ts | 4 ++-- .../src/voip/{VoIPUserState.ts => UserState.ts} | 2 +- packages/core-typings/src/voip/VoIpCallerInfo.ts | 6 +++--- packages/core-typings/src/voip/index.ts | 2 +- 10 files changed, 18 insertions(+), 22 deletions(-) delete mode 100644 packages/core-typings/src/UserState.ts rename packages/core-typings/src/voip/{VoIPUserState.ts => UserState.ts} (81%) diff --git a/apps/meteor/client/lib/voip/EEVoipClient.ts b/apps/meteor/client/lib/voip/EEVoipClient.ts index 750c47328fe54..5f3496ed317c5 100644 --- a/apps/meteor/client/lib/voip/EEVoipClient.ts +++ b/apps/meteor/client/lib/voip/EEVoipClient.ts @@ -1,5 +1,5 @@ import type { ICallerInfo, IMediaStreamRenderer, VoIPUserConfiguration } from '@rocket.chat/core-typings'; -import { Operation, VoIPUserState } from '@rocket.chat/core-typings'; +import { Operation, UserState } from '@rocket.chat/core-typings'; import { Inviter, UserAgent } from 'sip.js'; import type { IncomingResponse } from 'sip.js/lib/core'; @@ -58,7 +58,7 @@ export class EEVoipClient extends VoIPUser { host: inviter.remoteIdentity.uri.host, }; this._callerInfo = callerInfo; - this._userState = VoIPUserState.UAC; + this._userState = UserState.UAC; this.emit('stateChanged'); } diff --git a/apps/meteor/client/lib/voip/VoIPUser.ts b/apps/meteor/client/lib/voip/VoIPUser.ts index 262fb769b4ae8..5e74ae2cbd050 100644 --- a/apps/meteor/client/lib/voip/VoIPUser.ts +++ b/apps/meteor/client/lib/voip/VoIPUser.ts @@ -19,7 +19,7 @@ import type { IState, VoipEvents, } from '@rocket.chat/core-typings'; -import { Operation, VoIPUserState, WorkflowTypes } from '@rocket.chat/core-typings'; +import { Operation, UserState, WorkflowTypes } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { UserAgentOptions, InvitationAcceptOptions, Session, SessionInviteOptions } from 'sip.js'; import { UserAgent, Invitation, SessionState, Registerer, RequestPendingError, Inviter } from 'sip.js'; @@ -79,7 +79,7 @@ export class VoIPUser extends Emitter { protected _callerInfo: ICallerInfo | undefined; - protected _userState: VoIPUserState = VoIPUserState.IDLE; + protected _userState: UserState = UserState.IDLE; protected _opInProgress: Operation = Operation.OP_NONE; @@ -87,7 +87,7 @@ export class VoIPUser extends Emitter { return this._opInProgress; } - get userState(): VoIPUserState | undefined { + get userState(): UserState | undefined { return this._userState; } @@ -298,7 +298,7 @@ export class VoIPUser extends Emitter { if (this.callState === 'REGISTERED') { this._opInProgress = Operation.OP_PROCESS_INVITE; this._callState = 'OFFER_RECEIVED'; - this._userState = VoIPUserState.UAS; + this._userState = UserState.UAS; this.session = invitation; this.setupSessionEventHandlers(invitation); const callerInfo: ICallerInfo = { @@ -338,7 +338,7 @@ export class VoIPUser extends Emitter { this.emit('ringing', { userState: this._userState, callInfo: this._callerInfo }); break; case SessionState.Established: - if (this._userState === VoIPUserState.UAC) { + if (this._userState === UserState.UAC) { /** * We need to decide about user-state ANSWER-RECEIVED for outbound. * This state is there for the symmetry of ANSWER-SENT. @@ -369,7 +369,7 @@ export class VoIPUser extends Emitter { this.session = undefined; this._callState = 'REGISTERED'; this._opInProgress = Operation.OP_NONE; - this._userState = VoIPUserState.IDLE; + this._userState = UserState.IDLE; this.emit('callterminated'); this.remoteStream?.clear(); this.emit('stateChanged'); diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx index 416b150ca42e4..7448b4160aa8c 100644 --- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx +++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx @@ -7,7 +7,7 @@ import { isVoipEventQueueMemberAdded, isVoipEventQueueMemberRemoved, isVoipEventCallAbandoned, - VoIPUserState, + UserState, } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { Random } from '@rocket.chat/random'; @@ -339,7 +339,7 @@ export const CallProvider = ({ children }: CallProviderProps) => { voipSounds.stopAll(); - if (callDetails.userState !== VoIPUserState.UAC) { + if (callDetails.userState !== UserState.UAC) { return; } // Agent has sent Invite. So it must create a room. diff --git a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx index 40a86e9d5550c..46ed046fc5e1e 100644 --- a/apps/meteor/client/views/admin/users/AdminUsersPage.tsx +++ b/apps/meteor/client/views/admin/users/AdminUsersPage.tsx @@ -77,7 +77,7 @@ const AdminUsersPage = (): ReactElement => { }); const pendingUsersCount = usePendingUsersCount(filteredUsersQueryResult.data?.users); - console.log('pendingUsersCount', pendingUsersCount.data); + const handleReload = (): void => { seatsCap?.reload(); filteredUsersQueryResult?.refetch(); diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index aa1e74b6a5a72..f725b2738cf92 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -183,9 +183,6 @@ export interface IGetRoomRoles { roles: string[]; } -// - -export type IUserState = 'pending_approval' | 'active' | 'deactivated' | 'inactive'; export interface IUser extends IRocketChatRecord { _id: string; createdAt: Date; @@ -255,7 +252,6 @@ export interface IUser extends IRocketChatRecord { roomRolePriorities?: Record; isOAuthUser?: boolean; // client only field __rooms?: string[]; - // state: IUserState; } export interface IRegisterUser extends IUser { diff --git a/packages/core-typings/src/UserState.ts b/packages/core-typings/src/UserState.ts deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/packages/core-typings/src/voip/ICallDetails.ts b/packages/core-typings/src/voip/ICallDetails.ts index 9bf650b05e146..0f17fea308cce 100644 --- a/packages/core-typings/src/voip/ICallDetails.ts +++ b/packages/core-typings/src/voip/ICallDetails.ts @@ -1,7 +1,7 @@ import type { ICallerInfo } from './ICallerInfo'; -import type { VoIPUserState } from './VoIPUserState'; +import type { UserState } from './UserState'; export interface ICallDetails { callInfo?: ICallerInfo; - userState: VoIPUserState; + userState: UserState; } diff --git a/packages/core-typings/src/voip/VoIPUserState.ts b/packages/core-typings/src/voip/UserState.ts similarity index 81% rename from packages/core-typings/src/voip/VoIPUserState.ts rename to packages/core-typings/src/voip/UserState.ts index 61655ac5a103f..ff704108defda 100644 --- a/packages/core-typings/src/voip/VoIPUserState.ts +++ b/packages/core-typings/src/voip/UserState.ts @@ -1,6 +1,6 @@ // User state is based on whether the User has sent an invite(UAC) or it // has received an invite (UAS) -export enum VoIPUserState { +export enum UserState { IDLE, UAC, UAS, diff --git a/packages/core-typings/src/voip/VoIpCallerInfo.ts b/packages/core-typings/src/voip/VoIpCallerInfo.ts index e3b3c8f824cc7..ea0af8a1d955a 100644 --- a/packages/core-typings/src/voip/VoIpCallerInfo.ts +++ b/packages/core-typings/src/voip/VoIpCallerInfo.ts @@ -1,6 +1,6 @@ import type { CallStates } from './CallStates'; import type { ICallerInfo } from './ICallerInfo'; -import type { VoIPUserState } from './VoIPUserState'; +import type { UserState } from './UserState'; export interface IState { isReady: boolean; @@ -10,10 +10,10 @@ export interface IState { export type VoIpCallerInfo = | { state: Exclude; - userState: VoIPUserState; + userState: UserState; } | { state: 'IN_CALL' | 'ON_HOLD' | 'OFFER_RECEIVED' | 'OFFER_SENT'; - userState: VoIPUserState; + userState: UserState; caller: ICallerInfo; }; // TODO: Check for additional properties and States (E.g. call on hold, muted, etc) diff --git a/packages/core-typings/src/voip/index.ts b/packages/core-typings/src/voip/index.ts index 3154d6870c1d8..d7f2f37c79df7 100644 --- a/packages/core-typings/src/voip/index.ts +++ b/packages/core-typings/src/voip/index.ts @@ -9,7 +9,7 @@ export * from './IRegisterHandlerDelegate'; export * from './IRegistrationInfo'; export * from './Operations'; export * from './SignalingSocketEvents'; -export * from './VoIPUserState'; +export * from './UserState'; export * from './VoipClientEvents'; export * from './VoipEvents'; export * from './WorkflowTypes'; From 05bdac84230065a56d991e61595bc321408213b6 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Fri, 17 Oct 2025 16:09:04 +0530 Subject: [PATCH 05/15] implement user inactive reason --- apps/meteor/app/api/server/lib/users.ts | 24 +++++++++++++++++++ apps/meteor/app/api/server/v1/users.ts | 3 ++- .../authentication/server/startup/index.js | 2 ++ .../admin/users/hooks/useFilteredUsers.ts | 7 +++--- .../admin/users/hooks/usePendingUsersCount.ts | 2 +- packages/core-typings/src/IUser.ts | 1 + .../model-typings/src/models/IUsersModel.ts | 1 - packages/models/src/models/Users.ts | 12 ++-------- .../src/v1/users/UsersListStatusParamsGET.ts | 9 +++++++ 9 files changed, 45 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index e1451f4dd6698..79caccfe79e57 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -131,6 +131,7 @@ type FindPaginatedUsersByStatusProps = { searchTerm: string; hasLoggedIn: boolean; type: string; + inactiveReason: ('deactivated' | 'pending_approval' | 'idle_too_long')[]; }; export async function findPaginatedUsersByStatus({ @@ -143,6 +144,7 @@ export async function findPaginatedUsersByStatus({ searchTerm, hasLoggedIn, type, + inactiveReason, }: FindPaginatedUsersByStatusProps) { const actualSort: Record = sort || { username: 1 }; if (sort?.status) { @@ -199,6 +201,28 @@ export async function findPaginatedUsersByStatus({ match.roles = { $in: roles }; } + if (inactiveReason) { + // match.inactiveReason = { $in: inactiveReason }; + // } + + // if (inactiveReason && (inactiveReason.includes('deactivated') || inactiveReason.includes('idle_too_long'))) { + const inactiveReasonCondition = { + $or: [ + { inactiveReason: { $in: inactiveReason } }, + ...(inactiveReason.includes('deactivated') || inactiveReason.includes('idle_too_long') + ? [{ inactiveReason: { $exists: false } }] + : []), + ], + }; + + if (match.$or) { + match.$and = [{ $or: match.$or }, inactiveReasonCondition]; + delete match.$or; + } else { + Object.assign(match, inactiveReasonCondition); + } + } + const { cursor, totalCount } = Users.findPaginated( { ...match, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 048b289b3fcec..b637e26b83bac 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -611,7 +611,7 @@ API.v1.addRoute( const { offset, count } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); - const { status, hasLoggedIn, type, roles, searchTerm } = this.queryParams; + const { status, hasLoggedIn, type, roles, searchTerm, inactiveReason } = this.queryParams; return API.v1.success( await findPaginatedUsersByStatus({ @@ -624,6 +624,7 @@ API.v1.addRoute( searchTerm, hasLoggedIn, type, + inactiveReason, }), ); }, diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index a56f237c6e527..b7704cbe57357 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -204,7 +204,9 @@ const onCreateUserAsync = async function (options, user = {}) { } user.status = 'offline'; + user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); + user.inactiveReason = settings.get('Accounts_ManuallyApproveNewUsers') && !user.active ? 'pending_approval' : undefined; if (!user.name) { if (options.profile) { diff --git a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts index 435c033ed529f..dff91a2bd829b 100644 --- a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts +++ b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts @@ -29,16 +29,17 @@ const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData const listUsersPayload: Partial> = { all: {}, pending: { - hasLoggedIn: false, + // hasLoggedIn: false, type: 'user', - status: 'active', + status: 'deactivated', + inactiveReason: ['pending_approval'], }, active: { - hasLoggedIn: true, status: 'active', }, deactivated: { status: 'deactivated', + inactiveReason: ['deactivated', 'idle_too_long'], }, }; diff --git a/apps/meteor/client/views/admin/users/hooks/usePendingUsersCount.ts b/apps/meteor/client/views/admin/users/hooks/usePendingUsersCount.ts index 7010a3a50ff18..2aa1cb8f1f0d4 100644 --- a/apps/meteor/client/views/admin/users/hooks/usePendingUsersCount.ts +++ b/apps/meteor/client/views/admin/users/hooks/usePendingUsersCount.ts @@ -11,8 +11,8 @@ const usePendingUsersCount = (users: Serialized | undefined) queryFn: async () => { const payload: UsersListStatusParamsGET = { - hasLoggedIn: false, status: 'deactivated', + inactiveReason: ['pending_approval'], type: 'user', count: 1, }; diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index f725b2738cf92..6961781a88dbb 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -252,6 +252,7 @@ export interface IUser extends IRocketChatRecord { roomRolePriorities?: Record; isOAuthUser?: boolean; // client only field __rooms?: string[]; + inactiveReason?: 'deactivated' | 'pending_approval' | 'idle_too_long'; } export interface IRegisterUser extends IUser { diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 2d50bfd377cb9..66cc22607f929 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -387,7 +387,6 @@ export interface IUsersModel extends IBaseModel { setAvatarData(userId: string, origin: string, etag?: Date | null | string, options?: UpdateOptions): Promise; unsetAvatarData(userId: string): Promise; setUserActive(userId: string, active: boolean): Promise; - setAllUsersActive(active: boolean): Promise; setActiveNotLoggedInAfterWithRole(latestLastLoginDate: Date, role?: string, active?: boolean): Promise; unsetRequirePasswordChange(userId: string): Promise; resetPasswordAndSetRequirePasswordChange( diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index f84411ff50953..1e4bfc823d813 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -2954,22 +2954,13 @@ export class UsersRaw extends BaseRaw> implements IU const update = { $set: { active, + ...(!active && { inactiveReason: 'deactivated' as const }), }, }; return this.updateOne({ _id }, update); } - setAllUsersActive(active: boolean) { - const update = { - $set: { - active, - }, - }; - - return this.updateMany({}, update); - } - /** * @param latestLastLoginDate * @param {IRole['_id']} role the role id @@ -2988,6 +2979,7 @@ export class UsersRaw extends BaseRaw> implements IU const update = { $set: { active, + ...(!active && { inactiveReason: 'idle_too_long' as const }), }, }; diff --git a/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts index 25024aee12cb1..2249161ba91eb 100644 --- a/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts +++ b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts @@ -12,6 +12,7 @@ export type UsersListStatusParamsGET = PaginatedRequest<{ type?: string; roles?: string[]; searchTerm?: string; + inactiveReason?: ('deactivated' | 'pending_approval' | 'idle_too_long')[]; }>; const UsersListStatusParamsGetSchema = { type: 'object', @@ -51,6 +52,14 @@ const UsersListStatusParamsGetSchema = { type: 'number', nullable: true, }, + inactiveReason: { + type: 'array', + items: { + type: 'string', + enum: ['deactivated', 'pending_approval', 'idle_too_long'], + }, + nullable: true, + }, }, additionalProperties: false, }; From a34971d57d931909bc09ddeb69a7068ec3ed35dd Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Fri, 17 Oct 2025 16:31:15 +0530 Subject: [PATCH 06/15] unset inactiveReason when activating a user --- packages/models/src/models/Users.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 1e4bfc823d813..49aa37037b4c6 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -2956,6 +2956,7 @@ export class UsersRaw extends BaseRaw> implements IU active, ...(!active && { inactiveReason: 'deactivated' as const }), }, + ...(active && { $unset: { inactiveReason: 1 as const } }), }; return this.updateOne({ _id }, update); @@ -2981,6 +2982,7 @@ export class UsersRaw extends BaseRaw> implements IU active, ...(!active && { inactiveReason: 'idle_too_long' as const }), }, + ...(active && { $unset: { inactiveReason: 1 as const } }), }; return this.updateMany(query, update); From 7c03e95980db4560b4cf1ef7abf67de3782fbc9e Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Fri, 17 Oct 2025 16:40:07 +0530 Subject: [PATCH 07/15] refactor: remove unused code --- apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts index dff91a2bd829b..7fd57f34dbdad 100644 --- a/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts +++ b/apps/meteor/client/views/admin/users/hooks/useFilteredUsers.ts @@ -29,7 +29,6 @@ const useFilteredUsers = ({ searchTerm, prevSearchTerm, sortData, paginationData const listUsersPayload: Partial> = { all: {}, pending: { - // hasLoggedIn: false, type: 'user', status: 'deactivated', inactiveReason: ['pending_approval'], From a8079bc4e40efb42ab0001cb7797fbfcafa88abb Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Fri, 17 Oct 2025 16:43:42 +0530 Subject: [PATCH 08/15] refactor: code comments --- apps/meteor/app/api/server/lib/users.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 79caccfe79e57..061248b2e1751 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -202,13 +202,11 @@ export async function findPaginatedUsersByStatus({ } if (inactiveReason) { - // match.inactiveReason = { $in: inactiveReason }; - // } - - // if (inactiveReason && (inactiveReason.includes('deactivated') || inactiveReason.includes('idle_too_long'))) { const inactiveReasonCondition = { $or: [ { inactiveReason: { $in: inactiveReason } }, + // This condition is to make it backward compatible with the old behavior + // The deactivated users not having the inactiveReason field should be returned as well ...(inactiveReason.includes('deactivated') || inactiveReason.includes('idle_too_long') ? [{ inactiveReason: { $exists: false } }] : []), From 7dff525d8523c3f95939361e1902111db4e56804 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Wed, 19 Nov 2025 15:35:05 +0530 Subject: [PATCH 09/15] fix e2e test --- apps/meteor/tests/e2e/admin-users.spec.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/meteor/tests/e2e/admin-users.spec.ts b/apps/meteor/tests/e2e/admin-users.spec.ts index c7f6be2a598a3..5d3e1212d47e5 100644 --- a/apps/meteor/tests/e2e/admin-users.spec.ts +++ b/apps/meteor/tests/e2e/admin-users.spec.ts @@ -1,5 +1,6 @@ import { Users } from './fixtures/userStates'; import { AdminUsers } from './page-objects'; +import { setSettingValueById } from './utils/setSettingValueById'; import { test, expect } from './utils/test'; import type { ITestUser } from './utils/user-helpers'; import { createTestUser } from './utils/user-helpers'; @@ -12,10 +13,12 @@ test.use({ storageState: Users.admin.state }); test.describe('Admin > Users', () => { test.beforeAll('Create a new user', async ({ api }) => { user = await createTestUser(api); + await setSettingValueById(api, 'Accounts_ManuallyApproveNewUsers', true); }); - test.afterAll('Delete the new user', async () => { + test.afterAll('Delete the new user', async ({ api }) => { await user.delete(); + await setSettingValueById(api, 'Accounts_ManuallyApproveNewUsers', false); }); test.beforeEach('Go to /admin/users', async ({ page }) => { @@ -46,19 +49,19 @@ test.describe('Admin > Users', () => { await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); }); - await test.step('should move from Pending to Deactivated tab', async () => { + await test.step('should move from Pending to Active tab', async () => { await admin.getTabByName('Pending').click(); - await admin.dispatchUserAction(user.data.username, 'Deactivate'); + await admin.dispatchUserAction(user.data.username, 'Activate'); await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); - await admin.getTabByName('Deactivated').click(); + await admin.getTabByName('Active').click(); await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); }); - await test.step('should move from Deactivated to Pending tab', async () => { - await admin.getTabByName('Deactivated').click(); - await admin.dispatchUserAction(user.data.username, 'Activate'); + await test.step('should move from Active to Deactivated tab', async () => { + await admin.getTabByName('Active').click(); + await admin.dispatchUserAction(user.data.username, 'Deactivate'); await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); - await admin.getTabByName('Pending').click(); + await admin.getTabByName('Deactivated').click(); await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); }); }); From f739f1759dc2c5b3e0fe7b525ff021c9fe1a991b Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Wed, 19 Nov 2025 21:24:18 +0530 Subject: [PATCH 10/15] test order --- apps/meteor/tests/e2e/admin-users.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/admin-users.spec.ts b/apps/meteor/tests/e2e/admin-users.spec.ts index 564de08e8a768..6b9a697eaa9c3 100644 --- a/apps/meteor/tests/e2e/admin-users.spec.ts +++ b/apps/meteor/tests/e2e/admin-users.spec.ts @@ -12,8 +12,8 @@ test.use({ storageState: Users.admin.state }); test.describe('Admin > Users', () => { test.beforeAll('Create a new user', async ({ api }) => { - user = await createTestUser(api); await setSettingValueById(api, 'Accounts_ManuallyApproveNewUsers', true); + user = await createTestUser(api); }); test.afterAll('Delete the new user', async ({ api }) => { From e414352f898b3255c702b5b1fcae2589ee51c4df Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Wed, 19 Nov 2025 22:36:42 +0530 Subject: [PATCH 11/15] add changeset --- .changeset/lazy-pianos-care.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/lazy-pianos-care.md diff --git a/.changeset/lazy-pianos-care.md b/.changeset/lazy-pianos-care.md new file mode 100644 index 0000000000000..c1f4407efbcbb --- /dev/null +++ b/.changeset/lazy-pianos-care.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/models': minor +'@rocket.chat/meteor': minor +--- + +Fixes an issue where new users pending approval were being shown under deactivated tab. From f29370cf1275ae0e20ccb377fd8c880a34496e2b Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Wed, 19 Nov 2025 23:57:39 +0530 Subject: [PATCH 12/15] optional inactiveReason field --- apps/meteor/app/api/server/lib/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/lib/users.ts b/apps/meteor/app/api/server/lib/users.ts index 061248b2e1751..9bb27e09f02ec 100644 --- a/apps/meteor/app/api/server/lib/users.ts +++ b/apps/meteor/app/api/server/lib/users.ts @@ -131,7 +131,7 @@ type FindPaginatedUsersByStatusProps = { searchTerm: string; hasLoggedIn: boolean; type: string; - inactiveReason: ('deactivated' | 'pending_approval' | 'idle_too_long')[]; + inactiveReason?: ('deactivated' | 'pending_approval' | 'idle_too_long')[]; }; export async function findPaginatedUsersByStatus({ From b16ed1ca5174bb34d9a3f0f97d352804acb0dd7a Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 20 Nov 2025 01:37:40 +0530 Subject: [PATCH 13/15] fix api validation --- packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts index 2249161ba91eb..1221e00985c06 100644 --- a/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts +++ b/packages/rest-typings/src/v1/users/UsersListStatusParamsGET.ts @@ -58,7 +58,6 @@ const UsersListStatusParamsGetSchema = { type: 'string', enum: ['deactivated', 'pending_approval', 'idle_too_long'], }, - nullable: true, }, }, additionalProperties: false, From 91ec45dde38a9e7b2ad8e7ed6798acd40dbdb305 Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 27 Nov 2025 20:40:15 +0530 Subject: [PATCH 14/15] changeset --- .changeset/lazy-pianos-care.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/lazy-pianos-care.md b/.changeset/lazy-pianos-care.md index c1f4407efbcbb..660e47a8ee7c3 100644 --- a/.changeset/lazy-pianos-care.md +++ b/.changeset/lazy-pianos-care.md @@ -6,4 +6,4 @@ '@rocket.chat/meteor': minor --- -Fixes an issue where new users pending approval were being shown under deactivated tab. +feat: Enhance user's deactivated state handling to correctly distinguish between pending and deactivated users. From 6ca14b4928c4cf9feefb23c6e0ab2be4aaab069e Mon Sep 17 00:00:00 2001 From: yash-rajpal Date: Thu, 27 Nov 2025 21:58:59 +0530 Subject: [PATCH 15/15] changeset --- .changeset/lazy-pianos-care.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/lazy-pianos-care.md b/.changeset/lazy-pianos-care.md index 660e47a8ee7c3..d5afd4cb714af 100644 --- a/.changeset/lazy-pianos-care.md +++ b/.changeset/lazy-pianos-care.md @@ -6,4 +6,4 @@ '@rocket.chat/meteor': minor --- -feat: Enhance user's deactivated state handling to correctly distinguish between pending and deactivated users. +Enhance user's deactivated state handling to correctly distinguish between pending and deactivated users.