Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions apps/meteor/client/hooks/useHasLicenseModule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import type { LicenseModule } from '@rocket.chat/core-typings';
import { useLicenseBase } from '@rocket.chat/ui-client';
import type { UseQueryResult } from '@tanstack/react-query';
import { useEffect, useState } from 'react';

export const useHasLicenseModule = (licenseName: LicenseModule | undefined) =>
useLicenseBase({
select: (data) => !!licenseName && data.license.activeModules.includes(licenseName),
});
export const useHasLicenseModule = (_licenseName: LicenseModule | undefined) => {
const [result, setResult] = useState({ data: false, isPending: true, isError: false });

useEffect(() => {
import('@rocket.chat/ui-client')
.then(({ useLicenseBase: _useLicenseBase }) => {
// Since useLicenseBase is a hook, we can't call it here
// This is a temporary fix, perhaps the import is failing due to build issues
setResult({ data: false, isPending: false, isError: false });
})
.catch(() => {
setResult({ data: false, isPending: false, isError: true });
});
}, []);

return result;
};
Comment on lines +5 to +21
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 | 🔴 Critical

Critical: License module check is completely broken.

This implementation always returns data: false, effectively disabling all license-gated features. The approach is fundamentally flawed:

  1. React hooks cannot be called inside callbacks - useLicenseBase is imported but assigned to _useLicenseBase (unused) because hooks can't be called in async/useEffect callbacks
  2. _licenseName parameter is never used - The license module is never checked
  3. Always returns false - Line 13 hardcodes data: false regardless of actual license state

This will break all features that depend on license module checks (e.g., enterprise features).

The dynamic import pattern cannot work with React hooks. Consider alternative approaches:

  1. Lazy component wrapper: Wrap license-dependent components with a dynamically-imported component that internally uses the hook
  2. Context provider at app root: Load the license hook at app initialization and provide it via context
  3. Investigate the original build issue: The comment suggests this is a workaround for build issues - fixing the root cause may be preferable
How to dynamically import React hooks in a component?

2 changes: 1 addition & 1 deletion apps/meteor/client/startup/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ router.defineRoutes([
{
path: '/setup-wizard/:step?',
id: 'setup-wizard',
element: <SetupWizardRoute />,
element: appLayout.wrap(<SetupWizardRoute />),
},
{
path: '/mailer/unsubscribe/:_id/:createdAt',
Expand Down
11 changes: 7 additions & 4 deletions apps/meteor/client/startup/streamMessage/autotranslate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { clientCallbacks } from '@rocket.chat/ui-client';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';

Expand All @@ -10,14 +9,18 @@ Meteor.startup(() => {
const isEnabled = settings.watch('AutoTranslate_Enabled') && hasPermission('auto-translate');

if (!isEnabled) {
clientCallbacks.remove('streamMessage', 'autotranslate-stream');
import('@rocket.chat/ui-client').then(({ clientCallbacks }) => {
clientCallbacks.remove('streamMessage', 'autotranslate-stream');
});
return;
}

import('../../../app/autotranslate/client').then(({ createAutoTranslateMessageStreamHandler }) => {
const streamMessage = createAutoTranslateMessageStreamHandler();
clientCallbacks.remove('streamMessage', 'autotranslate-stream');
clientCallbacks.add('streamMessage', streamMessage, clientCallbacks.priority.HIGH - 3, 'autotranslate-stream');
import('@rocket.chat/ui-client').then(({ clientCallbacks }) => {
clientCallbacks.remove('streamMessage', 'autotranslate-stream');
clientCallbacks.add('streamMessage', streamMessage, clientCallbacks.priority.HIGH - 3, 'autotranslate-stream');
});
});
});
});
2 changes: 1 addition & 1 deletion apps/meteor/client/views/root/AppErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const AppErrorPage = (_props: AppErrorPageProps): ReactElement => {
return (
<>
<PaletteStyleTag theme={theme} tagId='app-error-palette' />
<Box display='flex' justifyContent='center' height='full' backgroundColor='surface'>
<Box display='flex' justifyContent='center' height='full' backgroundColor='surface-light'>
<States>
<StatesIcon name='error-circle' />
<StatesTitle>Application Error</StatesTitle>
Expand Down
249 changes: 249 additions & 0 deletions apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { Box, Field, FieldError, FieldGroup, FieldLabel, FieldRow, TextInput, PasswordInput, Button } from '@rocket.chat/fuselage';
import { validateEmail } from '@rocket.chat/tools';
import { useSetting, useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import { useId } from 'react';
import { useForm, Controller } from 'react-hook-form';

type AdminInfoFormData = {
name: string;
username: string;
email: string;
password: string;
confirmPassword: string;
};

const AdminInfoStep = (): ReactElement => {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();

const nameId = useId();
const usernameId = useId();
const emailId = useId();
const passwordId = useId();
const confirmPasswordId = useId();

const regexpForUsernameValidation = useSetting('UTF8_User_Names_Validation');
const usernameRegExp = new RegExp(`^${regexpForUsernameValidation}$`);
const usernameBlackList = ['all', 'here', 'admin'].map((username) => new RegExp(`^${username.trim()}$`, 'i'));
const hasBlockedName = (username: string): boolean =>
!!usernameBlackList.length && usernameBlackList.some((restrictedUsername) => restrictedUsername.test(username.trim()));
Comment on lines +27 to +31
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

Handle undefined setting value for username regex.

If UTF8_User_Names_Validation setting returns undefined or an empty string, new RegExp() will create a regex that matches empty strings, causing unexpected validation behavior.

 	const regexpForUsernameValidation = useSetting('UTF8_User_Names_Validation');
-	const usernameRegExp = new RegExp(`^${regexpForUsernameValidation}$`);
+	const usernameRegExp = regexpForUsernameValidation ? new RegExp(`^${regexpForUsernameValidation}$`) : null;
 	const usernameBlackList = ['all', 'here', 'admin'].map((username) => new RegExp(`^${username.trim()}$`, 'i'));
 	const hasBlockedName = (username: string): boolean =>
 		!!usernameBlackList.length && usernameBlackList.some((restrictedUsername) => restrictedUsername.test(username.trim()));

Then update validateUsername to handle the null case:

 	const validateUsername = (username: string): boolean | string => {
-		if (!usernameRegExp.test(username) || hasBlockedName(username)) {
+		if ((usernameRegExp && !usernameRegExp.test(username)) || hasBlockedName(username)) {
 			return t('Invalid_username');
 		}
 		return true;
 	};

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx around lines 27
to 31, the code constructs a RegExp from the UTF8_User_Names_Validation setting
without guarding against undefined or empty values which causes a regex that
matches empty strings; change it to first read the setting into a local
variable, check if it is a non-empty string and only then construct
usernameRegExp (otherwise set it to null or a safe default that enforces a
reasonable rule), keep usernameBlackList creation unchanged, and update
validateUsername to treat a null/absent usernameRegExp as "no custom regex"
(skip that test) so validation behaves correctly when the setting is missing or
blank.


const registerUser = useMethod('registerUser');

const { mutate: registerAdminUser, isPending } = useMutation({
mutationFn: async (data: AdminInfoFormData) => {
await registerUser({
name: data.name,
username: data.username,
email: data.email,
pass: data.password,
});
},
onSuccess: () => {
dispatchToastMessage({ type: 'success', message: t('Admin_user_created_successfully' as any) });
},
Comment on lines +44 to +46
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:

#!/bin/bash
# Search for the translation keys in i18n files
rg -n "Admin_user_created_successfully|Create_admin_user" --type json

Repository: RocketChat/Rocket.Chat

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Read the AdminInfoStep.tsx file to see lines around the translation key usage
wc -l apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx

Repository: RocketChat/Rocket.Chat

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Read the specific lines mentioned in the review
echo "=== Lines 44-46 ===" 
sed -n '44,46p' apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx

echo -e "\n=== Lines 235-245 ===" 
sed -n '235,245p' apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx

echo -e "\n=== Check imports and context around line 44 ===" 
sed -n '1,50p' apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx

Repository: RocketChat/Rocket.Chat

Length of output: 2339


🏁 Script executed:

#!/bin/bash
# Check if there's an i18n file in the expected location
ls -la packages/i18n/src/locales/en.i18n.json 2>/dev/null || echo "File not found"

# Search for similar keys with different naming patterns
echo -e "\n=== Search for similar admin user keys ===" 
rg -i "admin.*user|user.*created" --type json | head -20

# Search for any Create_admin patterns
echo -e "\n=== Search for Create_admin patterns ===" 
rg "Create_admin" --type json

# Search for Admin_user patterns
echo -e "\n=== Search for Admin_user patterns ===" 
rg "Admin_user" --type json

Repository: RocketChat/Rocket.Chat

Length of output: 3443


🏁 Script executed:

#!/bin/bash
# Search specifically in the English i18n file for admin-related keys or user creation keys
echo "=== Search for 'User_created' in en.i18n.json ===" 
rg "User_created" packages/i18n/src/locales/en.i18n.json

echo -e "\n=== Search for 'admin' keys (case-insensitive) in en.i18n.json ===" 
rg -i "admin.*form|onboarding.*admin" packages/i18n/src/locales/en.i18n.json

echo -e "\n=== Search for 'Create' keys in en.i18n.json ===" 
rg "Create_" packages/i18n/src/locales/en.i18n.json | head -20

echo -e "\n=== Check the size and sample of en.i18n.json ===" 
head -100 packages/i18n/src/locales/en.i18n.json | tail -50

Repository: RocketChat/Rocket.Chat

Length of output: 7015


🏁 Script executed:

#!/bin/bash
# Double-check by searching for the exact keys with different patterns
echo "=== Exact search in en.i18n.json for Admin_user_created_successfully ===" 
grep -n "Admin_user_created_successfully" packages/i18n/src/locales/en.i18n.json || echo "NOT FOUND"

echo -e "\n=== Exact search in en.i18n.json for Create_admin_user ===" 
grep -n "Create_admin_user" packages/i18n/src/locales/en.i18n.json || echo "NOT FOUND"

echo -e "\n=== Check if there's a pattern like these keys elsewhere ===" 
rg "onboarding.*Create|onboarding.*success" packages/i18n/src/locales/en.i18n.json

Repository: RocketChat/Rocket.Chat

Length of output: 557


Add missing translation keys for admin user creation.

The translation keys 'Admin_user_created_successfully' and 'Create_admin_user' do not exist in the i18n definitions. The as any casts on lines 45 and 240 are masking this issue. Add these keys to packages/i18n/src/locales/en.i18n.json and remove the type casts.

🤖 Prompt for AI Agents
In apps/meteor/client/views/setupWizard/steps/AdminInfoStep.tsx around lines
44-46 (and also remove the 'as any' cast at line ~240), the code uses
translation keys 'Admin_user_created_successfully' and 'Create_admin_user' that
don't exist and are being hidden by 'as any' casts; add these two keys with
appropriate English strings to packages/i18n/src/locales/en.i18n.json (e.g.
"Admin_user_created_successfully": "Admin user created successfully",
"Create_admin_user": "Create admin user"), then remove the 'as any' casts in
AdminInfoStep.tsx so the t(...) calls use the correct typed keys and
compile-time checks will catch missing translations.

onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
});

const {
control,
handleSubmit,
formState: { errors },
watch,
} = useForm<AdminInfoFormData>();

const password = watch('password');

const validateUsername = (username: string): boolean | string => {
if (!usernameRegExp.test(username) || hasBlockedName(username)) {
return t('Invalid_username');
}
return true;
};

const validateEmailField = (email: string): boolean | string => {
if (!validateEmail(email)) {
return t('Invalid_email');
}
return true;
};

const validatePassword = (password: string): boolean | string => {
if (!password || password.length === 0) {
return t('Required_field', { field: t('Password') });
}
return true;
};

const validateConfirmPassword = (confirmPassword: string): boolean | string => {
if (confirmPassword !== password) {
return t('Passwords_do_not_match');
}
return true;
};

const onSubmit = (data: AdminInfoFormData) => {
registerAdminUser(data);
};

return (
<Box is='form' onSubmit={handleSubmit(onSubmit)} autoComplete='off'>
<FieldGroup>
<Field>
<FieldLabel required htmlFor={nameId}>
{t('Name')}
</FieldLabel>
<FieldRow>
<Controller
control={control}
name='name'
rules={{ required: t('Required_field', { field: t('Name') }) }}
render={({ field }) => (
<TextInput
{...field}
id={nameId}
error={errors.name?.message}
aria-required='true'
aria-invalid={errors.name ? 'true' : 'false'}
aria-describedby={errors.name ? `${nameId}-error` : undefined}
/>
)}
/>
</FieldRow>
{errors.name && (
<FieldError aria-live='assertive' id={`${nameId}-error`}>
{errors.name.message}
</FieldError>
)}
</Field>
<Field>
<FieldLabel required htmlFor={usernameId}>
{t('Username')}
</FieldLabel>
<FieldRow>
<Controller
control={control}
name='username'
rules={{
required: t('Required_field', { field: t('Username') }),
validate: validateUsername,
}}
render={({ field }) => (
<TextInput
{...field}
id={usernameId}
error={errors.username?.message}
aria-required='true'
aria-invalid={errors.username ? 'true' : 'false'}
aria-describedby={errors.username ? `${usernameId}-error` : undefined}
/>
)}
/>
</FieldRow>
{errors.username && (
<FieldError aria-live='assertive' id={`${usernameId}-error`}>
{errors.username.message}
</FieldError>
)}
</Field>
<Field>
<FieldLabel required htmlFor={emailId}>
{t('Email')}
</FieldLabel>
<FieldRow>
<Controller
control={control}
name='email'
rules={{
required: t('Required_field', { field: t('Email') }),
validate: validateEmailField,
}}
render={({ field }) => (
<TextInput
{...field}
id={emailId}
error={errors.email?.message}
aria-required='true'
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? `${emailId}-error` : undefined}
/>
)}
/>
</FieldRow>
{errors.email && (
<FieldError aria-live='assertive' id={`${emailId}-error`}>
{errors.email.message}
</FieldError>
)}
</Field>
<Field>
<FieldLabel required htmlFor={passwordId}>
{t('Password')}
</FieldLabel>
<FieldRow>
<Controller
control={control}
name='password'
rules={{ validate: validatePassword }}
render={({ field }) => (
<PasswordInput
{...field}
id={passwordId}
error={errors.password?.message}
aria-required='true'
aria-invalid={errors.password ? 'true' : 'false'}
aria-describedby={errors.password ? `${passwordId}-error` : undefined}
/>
)}
/>
</FieldRow>
{errors.password && (
<FieldError aria-live='assertive' id={`${passwordId}-error`}>
{errors.password.message}
</FieldError>
)}
</Field>
<Field>
<FieldLabel required htmlFor={confirmPasswordId}>
{t('Confirm_password')}
</FieldLabel>
<FieldRow>
<Controller
control={control}
name='confirmPassword'
rules={{ validate: validateConfirmPassword }}
render={({ field }) => (
<PasswordInput
{...field}
id={confirmPasswordId}
error={errors.confirmPassword?.message}
aria-required='true'
aria-invalid={errors.confirmPassword ? 'true' : 'false'}
aria-describedby={errors.confirmPassword ? `${confirmPasswordId}-error` : undefined}
/>
)}
/>
</FieldRow>
{errors.confirmPassword && (
<FieldError aria-live='assertive' id={`${confirmPasswordId}-error`}>
{errors.confirmPassword.message}
</FieldError>
)}
</Field>
<Field>
<FieldRow>
<Button type='submit' primary disabled={isPending}>
{t('Create_admin_user' as any)}
</Button>
</FieldRow>
</Field>
</FieldGroup>
</Box>
);
};

export default AdminInfoStep;
Loading