Skip to content

Conversation

@sharanyamahajan
Copy link

@sharanyamahajan sharanyamahajan commented Jan 27, 2026

Proposed changes (including videos or screenshots)

This PR refactors RegisterForm.tsx to reduce cyclomatic complexity and improve code maintainability, without introducing any functional or UI changes.

Previously, the registration form combined form validation, API error handling, conditional logic, and UI rendering in a single component, requiring the eslint-disable complexity directive.
This refactor improves separation of concerns by extracting logic into dedicated hooks while preserving all existing behavior.

Summary of changes:

  • Extracted form validation logic into useRegisterValidation
  • Extracted registration error handling logic into useRegisterErrorHandling
  • Removed eslint-disable complexity from RegisterForm.tsx
  • Reduced component complexity while keeping layout, accessibility attributes, and behavior unchanged

This is a refactor-only change aimed at improving readability and reducing future regression risk.


Issue(s)

Refactor to address excessive complexity and mixed concerns in RegisterForm.tsx.


Steps to test or reproduce

  1. Open the registration page
  2. Verify all form fields behave as before:
    • Required field validation
    • Email and password validation
    • Password confirmation logic
  3. Submit the form with:
    • Valid data → registration proceeds as expected
    • Invalid data → same error messages and behavior as before
  4. Confirm there are no UI or accessibility changes

Further comments

  • No breaking changes
  • No new dependencies added
  • Validation rules, error messages, redirects, and accessibility attributes remain unchanged
  • This refactor aligns the registration form with existing modular patterns used in similar flows

Summary by CodeRabbit

  • New Features

    • Loading skeleton for contextual lists; centralized registration validation and error-handling hooks; new Livechat store types and context.
  • Bug Fixes

    • Sidebar item unregistration now safely ignores missing items; cache invalidation for stale entries.
  • Improvements

    • Profile form trims inputs and improves save flow; stronger type safety across store and cache; lifecycle/init robustness.
  • Breaking Changes

    • Simplified/removed legacy model APIs (public surface reduced).

✏️ Tip: You can customize this high-level summary in your review settings.

@sharanyamahajan sharanyamahajan requested review from a team as code owners January 27, 2026 15:55
@dionisio-bot
Copy link
Contributor

dionisio-bot bot commented Jan 27, 2026

Looks like this PR is not ready to merge, because of the following issues:

  • This PR is missing the 'stat: QA assured' label
  • This PR is missing the required milestone or project

Please fix the issues and try again

If you have any trouble, please check the PR guidelines

@changeset-bot
Copy link

changeset-bot bot commented Jan 27, 2026

⚠️ No Changeset found

Latest commit: 55c6715

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

Walkthrough

Multiple changes across Meteor client and livechat: cache lifecycle and token validation added to CachedStore; form validation centralized and trimmed; livechat store/types expanded; BaseRaw API surface reduced; new UI skeleton and registration hooks added; MongoDB TLS and options handling adjusted.

Changes

Cohort / File(s) Summary
Cache Store Lifecycle Management
apps/meteor/client/lib/cachedStores/CachedStore.ts
Added explicit init flow (init, performInitialization, initializationPromise, reconnectionComputation), MAX_CACHE_TIME-based invalidation, token validation, stricter (de)serialization of _updatedAt, timestamped changes queue, and removal validation.
Account Profile Form
apps/meteor/client/views/account/profile/AccountProfileForm.tsx, apps/meteor/client/views/account/profile/getProfileInitialValues.ts
handleSave now accepts a single values object; all text fields trimmed before send/reset; avatar update and success/error toasts added; initial values normalized via .trim().
Sidebar Item Management
apps/meteor/client/lib/createSidebarItems.ts
unregisterSidebarItem now guards against missing items and uses splice to remove entries safely.
Contextual List Skeleton
apps/meteor/client/views/room/contextualBar/lib/ContextualListSkeleton.tsx
New ContextualListSkeleton component rendering configurable skeleton rows using Box and Skeleton.
MongoDB Configuration
apps/meteor/packages/rocketchat-mongo-config/server/index.js
Enabled tls.DEFAULT_ECDH_CURVE = 'auto', safer MONGO_URL checks with optional chaining, and improved MONGO_OPTIONS JSON parse error logging and rethrow with cause.
Livechat Store & Types
packages/livechat/src/store/index.tsx, packages/livechat/src/store/types.ts
Added LivechatAlert, LivechatQueueInfo, LivechatModalState, LivechatConfig; tightened StoreState types (replacing many anys), added messageListPosition, created typed StoreContext/StoreContextValue, removed legacy session/window handlers.
Livechat Menu Refactor
packages/livechat/src/components/Menu/index.tsx
Refactored internal PopoverMenuWrapper from class to functional component with hooks (useRef, useState, useLayoutEffect); public exports unchanged.
Models API Reduction
packages/models/src/models/BaseRaw.ts
Removed many legacy public methods (multiple find*, insert*, update*, delete*, trash-related APIs); consolidated find/upsert/delete logic and improved rollback/error handling.
Registration Form & Hooks
packages/web-ui-registration/src/RegisterForm.tsx, packages/web-ui-registration/src/hooks/useRegisterValidation.ts, packages/web-ui-registration/src/hooks/useRegisterErrorHandling.ts
Centralized validation via useRegisterValidation, centralized error handling via useRegisterErrorHandling; replaced inline per-field validation; simplified focus handling and error flows.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant Cache as Local Cache
    participant Store as CachedStore
    participant Server as Server Sync

    Client->>Store: init()
    activate Store
    Store->>Cache: loadFromCache()
    activate Cache
    Cache->>Cache: validateToken()<br/>check MAX_CACHE_TIME
    alt Cache Valid
        Cache->>Cache: deserializeFromCache()<br/>convert _updatedAt to Date
        Cache-->>Store: return cached data
    else Cache Invalid/Stale
        Cache-->>Store: return null
    end
    deactivate Cache

    alt Data Loaded from Cache
        Store->>Store: setReady(true)
    else No Cache / Invalid
        Store->>Server: performInitialization()/sync()
        activate Server
        Server->>Server: fetch records<br/>collect timestamped changes
        Server-->>Store: return synced data
        deactivate Server
        Store->>Store: apply changes in order<br/>setReady(true)
    end

    deactivate Store
    Client->>Client: Initialization complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested labels

stat: ready to merge, stat: QA assured

Suggested reviewers

  • cardoso
  • ggazzo

Poem

🐰 A little rabbit hops through lines of code,
Tucking timestamps, tokens, and trims in a row,
Forms get cleaner, types stand tall and proud,
Old methods snooze while new hooks sing aloud,
Hooray — the repo hums, polished and ready to go! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main objective: extracting validation and error handling logic from RegisterForm into separate custom hooks to improve maintainability and reduce complexity.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/meteor/packages/rocketchat-mongo-config/server/index.js (1)

20-23: Guard against undefined MONGO_URL before .includes.

process.env.MONGO_URL can be undefined in some environments/tests, which would throw on startup. Add a safe fallback to avoid a hard crash.

🔧 Proposed fix
-	...(!process.env.MONGO_URL.includes('retryWrites') && { retryWrites: false }),
+	...(!((process.env.MONGO_URL ?? '').includes('retryWrites')) && { retryWrites: false }),
apps/meteor/client/lib/cachedStores/CachedStore.ts (1)

368-375: Missing cleanup for reconnectionComputation in release.

The reconnectionComputation Tracker autorun is set up during initialization (line 349) but is not stopped during release(). This could cause the computation to continue running after the store is released, potentially leading to memory leaks or unexpected behavior when the connection status changes.

Suggested fix
 async release() {
 	if (this.initializationPromise) {
 		await this.initializationPromise;
 	}

+	this.reconnectionComputation?.stop();
+	this.reconnectionComputation = undefined;
 	this.listenerUnsubscriber?.();
 	this.setReady(false);
 }
packages/models/src/models/BaseRaw.ts (1)

172-216: Thread options.session through trash-enabled deletion path.

When options.session is provided (multi-document transactions), the trash path ignores it for findOne, trash.updateOne, and col.deleteOne operations, causing them to execute outside the transaction. This breaks atomicity. Pass session to all three calls to maintain transactional consistency.

Suggested fix
  async findOneAndDelete(filter: Filter<T>, options?: FindOneAndDeleteOptions): Promise<WithId<T> | null> {
    if (!this.trash) {
      return this.col.findOneAndDelete(filter, options || {});
    }

+   const { session } = options ?? {};
-   const doc = await this.col.findOne(filter);
+   const doc = await this.col.findOne(filter, { session });
    if (!doc) {
      return null;
    }

    const { _id, ...record } = doc;
    const trash: TDeleted = {
      ...record,
      _deletedAt: new Date(),
      __collection__: this.name,
    } as unknown as TDeleted;

-   await this.trash.updateOne({ _id } as Filter<TDeleted>, { $set: trash } as UpdateFilter<TDeleted>, {
-     upsert: true,
-   });
+   await this.trash.updateOne({ _id } as Filter<TDeleted>, { $set: trash } as UpdateFilter<TDeleted>, {
+     upsert: true,
+     session,
+   });

    try {
-     await this.col.deleteOne({ _id } as Filter<T>);
+     await this.col.deleteOne({ _id } as Filter<T>, { session });
    } catch (originalError) {
🤖 Fix all issues with AI agents
In `@apps/meteor/client/lib/cachedStores/CachedStore.ts`:
- Around line 141-146: The computed updatedAt from deserializedRecords (variable
updatedAt, and the conditional that sets this.updatedAt = new Date(updatedAt))
is being overwritten by the later assignment this.updatedAt = data.updatedAt ||
this.updatedAt; either remove the earlier computation or stop overwriting
it—preferably keep the computed value as authoritative: remove the final
assignment (this.updatedAt = data.updatedAt || this.updatedAt) or change it to
only assign if data.updatedAt is strictly newer than the computed updatedAt;
refer to symbols updatedAt, deserializedRecords, hasId,
this.store.getState().replaceAll, and this.updatedAt to locate and adjust the
logic.

In `@apps/meteor/client/views/account/profile/AccountProfileForm.tsx`:
- Around line 240-243: The FieldLabel components (e.g., FieldLabel with
htmlFor={nicknameId}) are not associated with their inputs because the inputs
rendered by Controller (TextInput, TextArea, etc.) do not receive matching id
props; update the Controller render calls for 'nickname', 'bio', and 'email' to
spread the field props and explicitly pass id={nicknameId}, id={bioId}, and
id={emailId} respectively (or set field.id = ... before spreading) so the label
htmlFor matches the input id and restores proper accessibility linkage; locate
the Controller instances in AccountProfileForm.tsx and add the id prop to the
underlying input components (TextInput/TextArea) to fix the label-input binding.
- Around line 89-92: The validateUsername function currently returns undefined
for empty usernames which lets blank values pass client validation; update
validateUsername (referenced in AccountProfileForm.tsx) to explicitly check for
an empty string and return the same required-field validation message used
elsewhere (e.g., t('Required_field', { field: t('Username') })) instead of
returning early; keep the existing short-circuit for previousUsername, and
ensure behavior remains consistent with AdminUserForm and RegisterUsername
client-side validation before calling setUsername.

In `@apps/meteor/client/views/room/contextualBar/lib/ContextualListSkeleton.tsx`:
- Around line 8-11: The ContextualListSkeleton component uses Array.from({
length: items }) which can throw for negative or fractional items; clamp and
coerce the prop before creating the array by computing a safeCount (e.g., const
safeCount = Math.max(0, Math.floor(items ?? 6))) inside the
ContextualListSkeleton function and use Array.from({ length: safeCount })
instead; this ensures negative or non-integer items are converted to a
non-negative integer while preserving the default value behavior.

In `@packages/models/src/models/BaseRaw.ts`:
- Around line 149-153: In findOneAndUpdate ensure you detect whether the
provided update is an operator-style UpdateFilter or a full replacement document
before injecting _id: currently setUpdatedAt(update) and unconditional use of
update.$setOnInsert can add $-operators into a replacement and cause MongoDB
errors; fix by checking for operator keys (e.g., presence of any $-prefixed
properties or that update is UpdateFilter) and only assign _id into
update.$setOnInsert for operator updates, otherwise assign _id directly on the
replacement document (mutating the replacement object) when options.upsert is
true and query lacks _id.
🧹 Nitpick comments (12)
packages/web-ui-registration/src/RegisterForm.tsx (1)

40-40: Remove newly added inline comments to align with guidelines.

Line 40, Line 49, and the JSX section labels at Line 124, Line 147, Line 170, Line 193, Line 217, and Line 242 introduce implementation comments that the project guidelines discourage. As per coding guidelines, please remove them.

♻️ Proposed diff
-	// Settings
 	const requireNameForRegister = useSetting('Accounts_RequireNameForSignUp', true);
 	const requiresPasswordConfirmation = useSetting('Accounts_RequirePasswordConfirmation', true);
 	const manuallyApproveNewUsersRequired = useSetting('Accounts_ManuallyApproveNewUsers', false);
@@
-	// IDs
 	const formLabelId = useId();
 	const passwordVerifierId = useId();
@@
-					{/* Name */}
 					<Field>
@@
-					{/* Email */}
 					<Field>
@@
-					{/* Username */}
 					<Field>
@@
-					{/* Password */}
 					<Field>
@@
-					{/* Confirm Password */}
 					{requiresPasswordConfirmation && (
@@
-					{/* Reason */}
 					{manuallyApproveNewUsersRequired && (

Based on coding guidelines, ...

Also applies to: 49-49, 124-124, 147-147, 170-170, 193-193, 217-217, 242-242

apps/meteor/packages/rocketchat-mongo-config/server/index.js (2)

38-46: Consolidate the throws and preserve the error cause.

There are two consecutive throws; the second is unreachable and the first drops the cause. Prefer a single throw that retains the original error.

♻️ Proposed refactor
-		throw new Error(
-			`Invalid MONGO_OPTIONS configuration: ${error.message}`,
-		);
-		throw new Error('Invalid MONGO_OPTIONS environment variable: must be valid JSON.', { cause: error });
+		const message = error instanceof Error ? error.message : String(error);
+		throw new Error(`Invalid MONGO_OPTIONS configuration: ${message}`, { cause: error });

15-17: Remove inline implementation comments.

The updated inline comments conflict with the “avoid code comments in implementation” guideline for JS/TS files. Consider removing them or moving rationale to commit/ADR notes. As per coding guidelines, avoid code comments in the implementation.

Also applies to: 57-63

packages/livechat/src/store/types.ts (2)

1-5: Remove unused imports.

IMessage, ILivechatTrigger, and ILivechatAgent are imported but not used in this file.

Proposed fix
-import type {
-  IMessage,
-  ILivechatTrigger,
-  ILivechatAgent,
-} from '@rocket.chat/core-typings';
-

31-31: Remove inline comment.

As per coding guidelines, avoid code comments in implementation files.

Proposed fix
-  payload?: unknown; // unknown > any (safer)
+  payload?: unknown;
packages/livechat/src/store/index.tsx (2)

231-256: Provider class looks correct but consider adding explicit typing.

The Provider class component works correctly with Preact, but the state property and render method lack explicit type annotations. This is minor but could improve maintainability.

Optional: Add explicit type annotations
 export class Provider extends Component {
 	static override displayName = 'StoreProvider';

-	override state = {
+	override state: StoreContextValue = {
 		...store.state,
 		dispatch: store.setState.bind(store),
 		on: store.on.bind(store),
 		off: store.off.bind(store),
 	};

26-31: Typo in type name: "Sytem" should be "System".

LivechatHiddenSytemMessageType has a typo. Rename to LivechatHiddenSystemMessageType for clarity and consistency with the comment and property name hiddenSystemMessages.

Proposed fix
-export type LivechatHiddenSytemMessageType =
+export type LivechatHiddenSystemMessageType =
 	| 'uj'
 	| 'ul'
 	| 'livechat-close'
 	| 'livechat-started'
 	| 'livechat_transfer_history';

Update usages at lines 68 and 112.

apps/meteor/client/lib/cachedStores/CachedStore.ts (3)

21-22: Type guard doesn't validate _id is a string.

The type assertion claims _id: string, but the runtime check only verifies the property exists, not its type. This could lead to runtime issues if _id is not a string.

Suggested fix
 const hasId = <T>(record: T): record is T & { _id: string } =>
-	typeof record === 'object' && record !== null && '_id' in record;
+	typeof record === 'object' && record !== null && '_id' in record && typeof (record as { _id: unknown })._id === 'string';

229-238: Async callback without error handling could cause unhandled rejections.

The async callback passed to sdk.stream may not be awaited by the stream handler. If handleRecordEvent throws, the promise rejection could go unhandled.

Consider adding error handling
 protected setupListener() {
 	return sdk.stream(
 		this.eventType,
 		[this.eventName],
 		(async (action: 'removed' | 'changed', record: U) => {
 			this.log('record received', action, record);
-			await this.handleRecordEvent(action, record);
+			try {
+				await this.handleRecordEvent(action, record);
+			} catch (error) {
+				console.error('Error handling record event:', error);
+			}
 		}) as (...args: unknown[]) => void,
 	);
 }

410-412: Non-awaited async release() call.

release() is an async method that awaits initializationPromise, but it's called without await in the logout handler. Errors from release() will result in unhandled promise rejections.

Consider adding error handling
 Accounts.onLogout(() => {
-	this.release();
+	void this.release().catch(console.error);
 });
packages/models/src/models/BaseRaw.ts (1)

170-170: Remove banner comments from implementation.

The added banner comments aren’t needed and conflict with the “avoid code comments in implementation” guideline.

🧹 Suggested cleanup
-	/* ===================== FIXED METHOD ===================== */
...
-	/* ======================================================== */
As per coding guidelines, ...

Also applies to: 222-222

apps/meteor/client/views/account/profile/AccountProfileForm.tsx (1)

155-156: Remove JSX section comments.

Inline section comments add noise in implementation files and conflict with the repo guideline to avoid code comments.

Proposed cleanup
-				{/* Avatar */}
-				{/* Name + Username */}
-				{/* Status */}
-				{/* Nickname */}
-				{/* Bio */}
-				{/* Email */}
As per coding guidelines, avoid code comments in implementation files.

Also applies to: 173-175, 210-211, 238-239, 246-247, 259-260

Comment on lines 141 to 146
if (updatedAt > this.updatedAt.getTime()) {
this.updatedAt = new Date(updatedAt);
}

this.store.getState().replaceAll(deserializedRecords.filter(hasId));

this.updatedAt = data.updatedAt || this.updatedAt;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential issue with updatedAt assignment order.

Lines 141-143 compute updatedAt from deserialized records, but line 146 immediately overwrites it with data.updatedAt || this.updatedAt. This makes the computation on lines 141-143 effectively unused since it will always be overwritten.

If the intent is to use data.updatedAt as the source of truth, the computation on lines 141-143 can be removed. If the computed value should take precedence, line 146 should be removed or reordered.

🤖 Prompt for AI Agents
In `@apps/meteor/client/lib/cachedStores/CachedStore.ts` around lines 141 - 146,
The computed updatedAt from deserializedRecords (variable updatedAt, and the
conditional that sets this.updatedAt = new Date(updatedAt)) is being overwritten
by the later assignment this.updatedAt = data.updatedAt || this.updatedAt;
either remove the earlier computation or stop overwriting it—preferably keep the
computed value as authoritative: remove the final assignment (this.updatedAt =
data.updatedAt || this.updatedAt) or change it to only assign if data.updatedAt
is strictly newer than the computed updatedAt; refer to symbols updatedAt,
deserializedRecords, hasId, this.store.getState().replaceAll, and this.updatedAt
to locate and adjust the logic.

Comment on lines 89 to 92
const validateUsername = async (username: string): Promise<string | undefined> => {
if (!username) {
return;
}

if (username === previousUsername) {
if (!username || username === previousUsername) {
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/meteor/client/views/account/profile/AccountProfileForm.tsx | sed -n '80,110p'

Repository: RocketChat/Rocket.Chat

Length of output: 1168


🏁 Script executed:

# Search for backend username validation rules
rg "username" apps/meteor/server --type ts --type js -A 3 -B 1 | head -100

Repository: RocketChat/Rocket.Chat

Length of output: 7055


🏁 Script executed:

# Look for similar validation patterns with 'Required_field' error
rg "Required_field" apps/meteor/client --type ts --type js -B 2 -A 2

Repository: RocketChat/Rocket.Chat

Length of output: 50378


🏁 Script executed:

# Check the form field definition for username input
rg -A 5 -B 5 "name.*username|username.*name" apps/meteor/client/views/account/profile/AccountProfileForm.tsx

Repository: RocketChat/Rocket.Chat

Length of output: 2665


🏁 Script executed:

# Search for backend endpoint that handles user update
rg "updateOwnBasicInfo|users.updateOwnBasicInfo" apps/meteor/server --type ts --type js -B 2 -A 10 | head -80

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

# Look for username validation rules in backend
rg "username.*required|empty.*username" apps/meteor/server --type ts -B 2 -A 2 | head -60

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

# Check for test cases related to username validation
fd ".*\.spec\.|.*\.test\." --type f | xargs rg "validateUsername|empty.*username" 2>/dev/null | head -40

Repository: RocketChat/Rocket.Chat

Length of output: 4019


🏁 Script executed:

cat -n apps/meteor/tests/unit/app/lib/server/functions/validateUsername.spec.ts | head -50

Repository: RocketChat/Rocket.Chat

Length of output: 1703


🏁 Script executed:

# Find the actual backend validateUsername implementation
fd "validateUsername.ts" --type f | grep -v spec | grep -v test

Repository: RocketChat/Rocket.Chat

Length of output: 122


🏁 Script executed:

# Look for the users.updateOwnBasicInfo endpoint implementation
rg "updateOwnBasicInfo" apps/meteor/server --type ts -l | head -5

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

cat -n apps/meteor/app/lib/server/functions/validateUsername.ts

Repository: RocketChat/Rocket.Chat

Length of output: 623


🏁 Script executed:

# Search for the endpoint that handles user profile updates
rg "users.updateOwnBasicInfo" apps/meteor/app --type ts -B 5 -A 15 | head -100

Repository: RocketChat/Rocket.Chat

Length of output: 1322


🏁 Script executed:

# Look for where username is updated in the backend
rg "updateOwnBasicInfo" apps/meteor/app/lib/server --type ts -B 3 -A 15

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

# Get the full users.updateOwnBasicInfo endpoint implementation
rg -A 30 "users.updateOwnBasicInfo" apps/meteor/app/api/server/v1/users.ts | head -80

Repository: RocketChat/Rocket.Chat

Length of output: 1038


🏁 Script executed:

# Look for the validation params function
rg "isUsersUpdateOwnBasicInfoParamsPOST" apps/meteor/app --type ts -B 2 -A 10

Repository: RocketChat/Rocket.Chat

Length of output: 1870


🏁 Script executed:

# Find the REST typings for updateOwnBasicInfo
fd "rest-typings" --type d

Repository: RocketChat/Rocket.Chat

Length of output: 88


🏁 Script executed:

# Search for isUsersUpdateOwnBasicInfoParamsPOST definition in rest-typings
rg "isUsersUpdateOwnBasicInfoParamsPOST|UpdateOwnBasicInfoParamsPOST" packages/ --type ts -B 2 -A 10

Repository: RocketChat/Rocket.Chat

Length of output: 4599


🏁 Script executed:

# Check if there's username validation in the endpoint handler itself
rg -A 50 "async post\(\)" apps/meteor/app/api/server/v1/users.ts | grep -A 50 "users.updateOwnBasicInfo" | head -70

Repository: RocketChat/Rocket.Chat

Length of output: 1462


🏁 Script executed:

# Find executeSaveUserProfile implementation
rg "executeSaveUserProfile|saveUserProfile" apps/meteor/app --type ts -l | head -5

Repository: RocketChat/Rocket.Chat

Length of output: 227


🏁 Script executed:

# Get the schema for UsersUpdateOwnBasicInfoParamsPostSchema
cat -n packages/rest-typings/src/v1/users/UsersUpdateOwnBasicInfoParamsPOST.ts

Repository: RocketChat/Rocket.Chat

Length of output: 2009


🏁 Script executed:

# Find and examine executeSaveUserProfile implementation
rg "executeSaveUserProfile" apps/meteor/app/lib/server --type ts -B 3 -A 30 | head -80

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

# Also check for saveUserProfile
rg "saveUserProfile" apps/meteor/app/lib/server/functions --type ts -l

Repository: RocketChat/Rocket.Chat

Length of output: 188


🏁 Script executed:

# Look for username validation in the save user profile flow
rg "validateUsername" apps/meteor/app/lib/server/functions --type ts -B 2 -A 5

Repository: RocketChat/Rocket.Chat

Length of output: 3010


🏁 Script executed:

# Find where saveUserProfile is defined
fd "saveUser" apps/meteor/app/lib/server/functions --type d

Repository: RocketChat/Rocket.Chat

Length of output: 112


🏁 Script executed:

# Check if saveUserProfile calls setUsername
rg "saveUserProfile|setUsername" apps/meteor/app/lib/server/functions/saveUser --type ts -B 2 -A 5 | head -50

Repository: RocketChat/Rocket.Chat

Length of output: 1594


🏁 Script executed:

# Look for the main saveUserProfile function
cat -n apps/meteor/app/lib/server/functions/saveUser.ts 2>/dev/null | head -100

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

# List all files in saveUser directory
fd . apps/meteor/app/lib/server/functions/saveUser --type f

Repository: RocketChat/Rocket.Chat

Length of output: 628


🏁 Script executed:

# Find the main export or index
rg "export.*saveUserProfile|export.*executeSaveUserProfile" apps/meteor/app/lib/server/functions --type ts

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

# Look for where username is handled in saveUser
rg "username" apps/meteor/app/lib/server/functions/saveUser --type ts -B 3 -A 8

Repository: RocketChat/Rocket.Chat

Length of output: 10565


🏁 Script executed:

# Find isUpdateUserData definition
rg "isUpdateUserData" apps/meteor/app/lib/server/functions/saveUser --type ts -B 2 -A 5

Repository: RocketChat/Rocket.Chat

Length of output: 6855


🏁 Script executed:

# Confirm that validateUserData only requires username for NEW users, not updates
cat -n apps/meteor/app/lib/server/functions/saveUser/validateUserData.ts | sed -n '1,70p'

Repository: RocketChat/Rocket.Chat

Length of output: 3293


🏁 Script executed:

# Double-check the saveUserIdentity function to see if it validates empty username
rg "saveUserIdentity" apps/meteor/app/lib/server/functions --type ts -l

Repository: RocketChat/Rocket.Chat

Length of output: 232


Add client-side validation to reject empty usernames in profile updates.

The validateUsername function currently returns early (undefined) for empty usernames, allowing them to pass client validation. While the backend would eventually reject an empty username through setUsername, the form should catch this on the client first. This is inconsistent with other username fields throughout the codebase (e.g., AdminUserForm, RegisterUsername) which all include explicit { required: t('Required_field', { field: t('Username') }) } validation.

Proposed fix
-	if (!username || username === previousUsername) {
-		return;
-	}
+	if (username === previousUsername) {
+		return;
+	}
+	if (!username) {
+		return t('Required_field', { field: t('Username') });
+	}
🤖 Prompt for AI Agents
In `@apps/meteor/client/views/account/profile/AccountProfileForm.tsx` around lines
89 - 92, The validateUsername function currently returns undefined for empty
usernames which lets blank values pass client validation; update
validateUsername (referenced in AccountProfileForm.tsx) to explicitly check for
an empty string and return the same required-field validation message used
elsewhere (e.g., t('Required_field', { field: t('Username') })) instead of
returning early; keep the existing short-circuit for previousUsername, and
ensure behavior remains consistent with AdminUserForm and RegisterUsername
client-side validation before calling setUsername.

Comment on lines 240 to 243
<FieldLabel htmlFor={nicknameId}>{t('Nickname')}</FieldLabel>
<FieldRow>
<Controller
control={control}
name='nickname'
render={({ field }) => (
<TextInput {...field} id={nicknameId} flexGrow={1} addon={<Icon name='edit' size='x20' alignSelf='center' />} />
)}
/>
<Controller control={control} name='nickname' render={({ field }) => <TextInput {...field} />} />
</FieldRow>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bind labels to inputs for accessibility.

FieldLabel uses htmlFor, but the nickname, bio, and email inputs don’t set the matching id, which breaks label association for screen readers.

Proposed fix
-						<Controller control={control} name='nickname' render={({ field }) => <TextInput {...field} />} />
+						<Controller control={control} name='nickname' render={({ field }) => <TextInput {...field} id={nicknameId} />} />
-							render={({ field }) => <TextAreaInput {...field} rows={3} />}
+							render={({ field }) => <TextAreaInput {...field} id={bioId} rows={3} />}
-							render={({ field }) => <TextInput {...field} disabled={!allowEmailChange} />}
+							render={({ field }) => <TextInput {...field} id={emailId} disabled={!allowEmailChange} />}

Also applies to: 248-255, 264-270

🤖 Prompt for AI Agents
In `@apps/meteor/client/views/account/profile/AccountProfileForm.tsx` around lines
240 - 243, The FieldLabel components (e.g., FieldLabel with
htmlFor={nicknameId}) are not associated with their inputs because the inputs
rendered by Controller (TextInput, TextArea, etc.) do not receive matching id
props; update the Controller render calls for 'nickname', 'bio', and 'email' to
spread the field props and explicitly pass id={nicknameId}, id={bioId}, and
id={emailId} respectively (or set field.id = ... before spreading) so the label
htmlFor matches the input id and restores proper accessibility linkage; locate
the Controller instances in AccountProfileForm.tsx and add the id prop to the
underlying input components (TextInput/TextArea) to fix the label-input binding.

Comment on lines +8 to +11
const ContextualListSkeleton: FC<ContextualListSkeletonProps> = ({ items = 6 }) => (
<Box p='x24'>
{Array.from({ length: items }).map((_, i) => (
<Box key={i} display='flex' alignItems='center' mb='x12'>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against negative/fractional items to avoid RangeError.

If a caller passes a negative or non-integer value, Array.from({ length: items }) can throw or behave unexpectedly. Clamp to a non-negative integer before use.

🔧 Proposed fix
-const ContextualListSkeleton: FC<ContextualListSkeletonProps> = ({ items = 6 }) => (
-	<Box p='x24'>
-		{Array.from({ length: items }).map((_, i) => (
+const ContextualListSkeleton: FC<ContextualListSkeletonProps> = ({ items = 6 }) => {
+	const safeItems = Math.max(0, Math.floor(items));
+	return (
+		<Box p='x24'>
+			{Array.from({ length: safeItems }).map((_, i) => (
 				<Box key={i} display='flex' alignItems='center' mb='x12'>
 					<Skeleton variant='rect' width='x40' height='x40' borderRadius='x4' />
 					<Box mi='x8' flexGrow={1}>
 						<Skeleton variant='text' width='100%' />
 						<Skeleton variant='text' width='60%' />
 					</Box>
 				</Box>
-		))}
-	</Box>
-);
+			))}
+		</Box>
+	);
+};
🤖 Prompt for AI Agents
In `@apps/meteor/client/views/room/contextualBar/lib/ContextualListSkeleton.tsx`
around lines 8 - 11, The ContextualListSkeleton component uses Array.from({
length: items }) which can throw for negative or fractional items; clamp and
coerce the prop before creating the array by computing a safeCount (e.g., const
safeCount = Math.max(0, Math.floor(items ?? 6))) inside the
ContextualListSkeleton function and use Array.from({ length: safeCount })
instead; this ensures negative or non-integer items are converted to a
non-negative integer while preserving the default value behavior.

Comment on lines 149 to 153
public findOneAndUpdate(query: Filter<T>, update: UpdateFilter<T> | T, options?: FindOneAndUpdateOptions): Promise<WithId<T> | null> {
this.setUpdatedAt(update);

if (options?.upsert && !('_id' in update || (update.$set && '_id' in update.$set)) && !('_id' in query)) {
update.$setOnInsert = {
...(update.$setOnInsert || {}),
_id: new ObjectId().toHexString(),
} as Partial<T> & { _id: string };
update.$setOnInsert = { ...(update.$setOnInsert || {}), _id: new ObjectId().toHexString() } as Partial<T> & { _id: string };
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd packages/models/src/models && head -200 BaseRaw.ts | tail -60

Repository: RocketChat/Rocket.Chat

Length of output: 2452


🏁 Script executed:

cat -n packages/models/src/models/BaseRaw.ts | sed -n '140,165p'

Repository: RocketChat/Rocket.Chat

Length of output: 1593


🌐 Web query:

MongoDB $setOnInsert replacement document operator mixing invalid

💡 Result:

That error means your update document is being treated as a replacement document (a plain { field: value, ... } document), but you’re also trying to use an update operator like $setOnInsert. MongoDB does not allow mixing replacement-style updates with operator-style updates—you must choose one form. In replacement updates, the document must contain only field/value pairs (no $... operators). [2]

Fix (most common): use an operator-style update with upsert: true$setOnInsert only applies when an upsert results in an insert. [1]

db.collection.updateOne(
  { _id: key },
  {
    $set: { updatedAt: new Date() },
    $setOnInsert: { createdAt: new Date(), status: "new" }
  },
  { upsert: true }
)

If you actually want to replace the whole document, use replaceOne() instead (but then you can’t use $setOnInsert in that same operation). [2]

Sources:
[1] MongoDB Docs: $setOnInsert behavior with upsert https://www.mongodb.com/docs/manual/reference/operator/update/setoninsert/
[2] MongoDB Docs: replacement documents cannot include update operators https://www.mongodb.com/docs/v7.1/tutorial/update-documents/


Branch update handling to avoid invalid replacement-operator mixing.

The update parameter accepts both operator-style updates (UpdateFilter<T>) and replacement documents (T). When update is a replacement document, adding $setOnInsert violates MongoDB's constraint that replacement documents cannot contain $ operators—MongoDB will reject this at runtime. Detect the update style and apply _id appropriately: for operator updates use $setOnInsert, for replacements set _id directly on the document.

Suggested fix
  public findOneAndUpdate(query: Filter<T>, update: UpdateFilter<T> | T, options?: FindOneAndUpdateOptions): Promise<WithId<T> | null> {
    this.setUpdatedAt(update);
    if (options?.upsert && !('_id' in update || (update.$set && '_id' in update.$set)) && !('_id' in query)) {
-     update.$setOnInsert = { ...(update.$setOnInsert || {}), _id: new ObjectId().toHexString() } as Partial<T> & { _id: string };
+     const updateObj = update as Record<string, any>;
+     const hasOperators = Object.keys(updateObj).some((key) => key.startsWith('$'));
+     if (hasOperators) {
+       updateObj.$setOnInsert = { ...(updateObj.$setOnInsert || {}), _id: new ObjectId().toHexString() };
+     } else {
+       (update as T)._id = new ObjectId().toHexString();
+     }
    }
    return this.col.findOneAndUpdate(query, update, options || {});
  }
📝 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
public findOneAndUpdate(query: Filter<T>, update: UpdateFilter<T> | T, options?: FindOneAndUpdateOptions): Promise<WithId<T> | null> {
this.setUpdatedAt(update);
if (options?.upsert && !('_id' in update || (update.$set && '_id' in update.$set)) && !('_id' in query)) {
update.$setOnInsert = {
...(update.$setOnInsert || {}),
_id: new ObjectId().toHexString(),
} as Partial<T> & { _id: string };
update.$setOnInsert = { ...(update.$setOnInsert || {}), _id: new ObjectId().toHexString() } as Partial<T> & { _id: string };
}
public findOneAndUpdate(query: Filter<T>, update: UpdateFilter<T> | T, options?: FindOneAndUpdateOptions): Promise<WithId<T> | null> {
this.setUpdatedAt(update);
if (options?.upsert && !('_id' in update || (update.$set && '_id' in update.$set)) && !('_id' in query)) {
const updateObj = update as Record<string, any>;
const hasOperators = Object.keys(updateObj).some((key) => key.startsWith('$'));
if (hasOperators) {
updateObj.$setOnInsert = { ...(updateObj.$setOnInsert || {}), _id: new ObjectId().toHexString() };
} else {
(update as T)._id = new ObjectId().toHexString();
}
}
return this.col.findOneAndUpdate(query, update, options || {});
}
🤖 Prompt for AI Agents
In `@packages/models/src/models/BaseRaw.ts` around lines 149 - 153, In
findOneAndUpdate ensure you detect whether the provided update is an
operator-style UpdateFilter or a full replacement document before injecting _id:
currently setUpdatedAt(update) and unconditional use of update.$setOnInsert can
add $-operators into a replacement and cause MongoDB errors; fix by checking for
operator keys (e.g., presence of any $-prefixed properties or that update is
UpdateFilter) and only assign _id into update.$setOnInsert for operator updates,
otherwise assign _id directly on the replacement document (mutating the
replacement object) when options.upsert is true and query lacks _id.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 11 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="apps/meteor/client/views/account/profile/AccountProfileForm.tsx">

<violation number="1" location="apps/meteor/client/views/account/profile/AccountProfileForm.tsx:200">
P2: Username field no longer required; empty username now passes validation and is submitted</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

required: t('Required_field', { field: t('Username') }),
validate: (username) => validateUsername(username),
}}
rules={{ validate: validateUsername }}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 27, 2026

Choose a reason for hiding this comment

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

P2: Username field no longer required; empty username now passes validation and is submitted

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/meteor/client/views/account/profile/AccountProfileForm.tsx, line 200:

<comment>Username field no longer required; empty username now passes validation and is submitted</comment>

<file context>
@@ -154,218 +161,121 @@ const AccountProfileForm = (props: AllHTMLAttributes<HTMLFormElement>): ReactEle
-									required: t('Required_field', { field: t('Username') }),
-									validate: (username) => validateUsername(username),
-								}}
+								rules={{ validate: validateUsername }}
 								render={({ field }) => (
-									<TextInput
</file context>
Fix with Cubic

@MartinSchoeler
Copy link
Member

Please check your PRs, some of them have lots of unrelated code, also there are other 2 similar PRs to this one that were opened before
#38365
and #38369

Please check if those already implement the changes done in this one. Closing this for now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants