mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
[CB] New and repeat passwords can be different (#4234)
* dbeaver/pro#8572 fixes password validity on blur
* Revert "dbeaver/pro#8572 fixes password validity on blur"
This reverts commit cefd174254.
* adds context to the form for correct validation + handles validation on change also to keep the actual validity state
* adds context check to prevent validation inconsistent behavior
* adds revalidate to handle depended validations cases
* adds useFormCustomInputValidation
* cleanup
* uses useFormCustomInputValidation to avoid regression after refactor
This commit is contained in:
@@ -1,22 +1,22 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { useContext, useEffect, useRef } from 'react';
|
||||
|
||||
import { ExecutorInterrupter } from '@cloudbeaver/core-executor';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslate } from '../localization/useTranslate.js';
|
||||
import { useExecutor } from '../useExecutor.js';
|
||||
import { FormContext } from './FormContext.js';
|
||||
|
||||
export interface ICustomInputValidation<TType extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement> {
|
||||
ref: React.RefObject<TType | null>;
|
||||
revalidate: () => void;
|
||||
revalidateAndReport: (element: TType) => boolean;
|
||||
}
|
||||
|
||||
export function useCustomInputValidation<T = void, TType extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement>(
|
||||
validation: (value: T) => string | null,
|
||||
): React.RefObject<TType | null> {
|
||||
const context = useContext(FormContext);
|
||||
): ICustomInputValidation<TType> {
|
||||
const inputRef = useRef<TType | null>(null);
|
||||
const translate = useTranslate();
|
||||
|
||||
@@ -27,37 +27,32 @@ export function useCustomInputValidation<T = void, TType extends HTMLInputElemen
|
||||
value = element.value as unknown as T;
|
||||
}
|
||||
|
||||
const valid = element.validity.valid;
|
||||
const result = validation(value);
|
||||
|
||||
try {
|
||||
if (typeof result === 'string') {
|
||||
element.setCustomValidity(result || translate('core_blocks_custom_input_validation_error'));
|
||||
return false;
|
||||
}
|
||||
element.setCustomValidity('');
|
||||
return true;
|
||||
} finally {
|
||||
if (valid !== element.validity.valid) {
|
||||
element.reportValidity();
|
||||
}
|
||||
if (typeof result === 'string') {
|
||||
element.setCustomValidity(result || translate('core_blocks_custom_input_validation_error'));
|
||||
return false;
|
||||
}
|
||||
element.setCustomValidity('');
|
||||
return true;
|
||||
}
|
||||
|
||||
useExecutor({
|
||||
executor: context?.onValidate,
|
||||
handlers: [
|
||||
function validationHandler(_, context) {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
}
|
||||
function revalidateAndReport(element: TType): boolean {
|
||||
const valid = element.validity.valid;
|
||||
const result = validate(element);
|
||||
|
||||
if (!validate(inputRef.current)) {
|
||||
ExecutorInterrupter.interrupt(context);
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
if (valid !== element.validity.valid) {
|
||||
element.reportValidity();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function revalidate() {
|
||||
if (inputRef.current) {
|
||||
validate(inputRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const element = inputRef.current;
|
||||
@@ -68,14 +63,14 @@ export function useCustomInputValidation<T = void, TType extends HTMLInputElemen
|
||||
function handleInput(event: Event) {
|
||||
const target = event.target as TType;
|
||||
if (target.validity.valid === false) {
|
||||
validate(target);
|
||||
revalidateAndReport(target);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur(event: Event) {
|
||||
const target = event.target as TType;
|
||||
if (target.validity.valid === true) {
|
||||
validate(target);
|
||||
revalidateAndReport(target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,5 +83,5 @@ export function useCustomInputValidation<T = void, TType extends HTMLInputElemen
|
||||
};
|
||||
});
|
||||
|
||||
return inputRef;
|
||||
return { ref: inputRef, revalidate, revalidateAndReport };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { FormContext, useExecutor, type IFormContext } from '../index.js';
|
||||
import { useCustomInputValidation, type ICustomInputValidation } from './useCustomInputValidation.js';
|
||||
import { ExecutorInterrupter } from '@cloudbeaver/core-executor';
|
||||
|
||||
export function useFormCustomInputValidation<T = void, TType extends HTMLInputElement | HTMLTextAreaElement = HTMLInputElement>(
|
||||
validation: (value: T) => string | null,
|
||||
formContext?: IFormContext,
|
||||
): ICustomInputValidation<TType> {
|
||||
const customValidation = useCustomInputValidation<T, TType>(validation);
|
||||
const reactContext = useContext(FormContext);
|
||||
const context = formContext ?? reactContext;
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useCustomInputValidation must be used within a FormContext provider');
|
||||
}
|
||||
|
||||
useExecutor({
|
||||
executor: context?.onValidate,
|
||||
handlers: [
|
||||
function validationHandler(_, context) {
|
||||
if (!customValidation.ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!customValidation.revalidateAndReport(customValidation.ref.current)) {
|
||||
ExecutorInterrupter.interrupt(context);
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return customValidation;
|
||||
}
|
||||
@@ -179,6 +179,7 @@ export * from './FormControls/Form.js';
|
||||
export * from './FormControls/Textarea.js';
|
||||
export * from './FormControls/useCapsLockTracker.js';
|
||||
export * from './FormControls/useCustomInputValidation.js';
|
||||
export * from './FormControls/useFormCustomInputValidation.js';
|
||||
export * from './FormControls/useForm.js';
|
||||
export * from './FormControls/Textarea.js';
|
||||
export * from './Link.js';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -9,21 +9,22 @@ import { PasswordPolicyService } from '@cloudbeaver/core-authentication';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import { PasswordPolicyResource } from '@cloudbeaver/core-root';
|
||||
|
||||
import { useCustomInputValidation } from './FormControls/useCustomInputValidation.js';
|
||||
import { useFormCustomInputValidation } from './FormControls/useFormCustomInputValidation.js';
|
||||
import { useResource } from './ResourcesHooks/useResource.js';
|
||||
import type { IFormContext } from './FormControls/FormContext.js';
|
||||
|
||||
export function usePasswordValidation() {
|
||||
export function usePasswordValidation(formContext?: IFormContext) {
|
||||
useResource(usePasswordValidation, PasswordPolicyResource, undefined);
|
||||
const passwordPolicyService = useService(PasswordPolicyService);
|
||||
|
||||
const ref = useCustomInputValidation<string>(value => {
|
||||
const { ref } = useFormCustomInputValidation<string>(value => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validation = passwordPolicyService.validatePassword(value);
|
||||
return validation.isValid ? null : validation.errorMessage;
|
||||
});
|
||||
}, formContext);
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Textarea,
|
||||
useResource,
|
||||
useTranslate,
|
||||
useCustomInputValidation,
|
||||
useFormCustomInputValidation,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
|
||||
@@ -33,7 +33,7 @@ interface Props {
|
||||
export const ServerConfigurationInfoForm = observer<Props>(function ServerConfigurationInfoForm({ state }) {
|
||||
const serverConfigLoader = useResource(ServerConfigurationInfoForm, ServerConfigResource, undefined);
|
||||
const translate = useTranslate();
|
||||
const validation = useCustomInputValidation<string, HTMLTextAreaElement>(value => {
|
||||
const { ref: validation } = useFormCustomInputValidation<string, HTMLTextAreaElement>(value => {
|
||||
const currentHost = window.location.host;
|
||||
|
||||
if (!isIp(window.location.hostname) && value.trim() && !value.includes(currentHost)) {
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -8,7 +8,7 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { ADMIN_USERNAME_MIN_LENGTH } from '@cloudbeaver/core-authentication';
|
||||
import { Group, GroupTitle, InputField, useCustomInputValidation, usePasswordValidation, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { Group, GroupTitle, InputField, useFormCustomInputValidation, usePasswordValidation, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { isValuesEqual } from '@cloudbeaver/core-utils';
|
||||
import type { IServerConfig } from '@cloudbeaver/plugin-administration';
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ServerConfigurationAdminForm = observer<Props>(function ServerConfi
|
||||
const translate = useTranslate();
|
||||
const passwordValidationRef = usePasswordValidation();
|
||||
|
||||
const passwordRepeatRef = useCustomInputValidation<string>(value => {
|
||||
const { ref: passwordRepeatRef } = useFormCustomInputValidation<string>(value => {
|
||||
if (!isValuesEqual(value, serverConfig.adminPassword, null)) {
|
||||
return translate('authentication_user_passwords_not_match');
|
||||
}
|
||||
|
||||
+12
-3
@@ -1,13 +1,22 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { Container, Group, InputField, Textarea, useAutoLoad, useCustomInputValidation, useResource, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import {
|
||||
Container,
|
||||
Group,
|
||||
InputField,
|
||||
Textarea,
|
||||
useAutoLoad,
|
||||
useFormCustomInputValidation,
|
||||
useResource,
|
||||
useTranslate,
|
||||
} from '@cloudbeaver/core-blocks';
|
||||
import { ServerConfigResource } from '@cloudbeaver/core-root';
|
||||
import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui';
|
||||
|
||||
@@ -27,7 +36,7 @@ export const TeamOptions: TabContainerPanelComponent<TeamFormProps> = observer(f
|
||||
|
||||
useAutoLoad(TeamOptions, tabState, tab.selected && !loaded);
|
||||
|
||||
const idValidationRef = useCustomInputValidation<string>(value => {
|
||||
const { ref: idValidationRef } = useFormCustomInputValidation<string>(value => {
|
||||
const v = value.trim();
|
||||
|
||||
if (!v) {
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Container,
|
||||
GroupTitle,
|
||||
InputField,
|
||||
useCustomInputValidation,
|
||||
useFormCustomInputValidation,
|
||||
usePasswordValidation,
|
||||
useResource,
|
||||
useTranslate,
|
||||
@@ -45,7 +45,7 @@ export const UserFormInfoCredentials = observer<Props>(function UserFormInfoCred
|
||||
local = !editing || (!!userInfo.data && isLocalUser(userInfo.data));
|
||||
}
|
||||
|
||||
const usernameValidationRef = useCustomInputValidation<string>(value => {
|
||||
const { ref: usernameValidationRef } = useFormCustomInputValidation<string>(value => {
|
||||
const v = value.trim();
|
||||
|
||||
if (!v) {
|
||||
@@ -59,7 +59,7 @@ export const UserFormInfoCredentials = observer<Props>(function UserFormInfoCred
|
||||
return null;
|
||||
});
|
||||
|
||||
const passwordRepeatRef = useCustomInputValidation<string>(value => {
|
||||
const { ref: passwordRepeatRef } = useFormCustomInputValidation<string>(value => {
|
||||
if (!isValuesEqual(value, tabState.state.password, null)) {
|
||||
return translate('authentication_user_passwords_not_match');
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { Select, FieldCheckbox, InputField, Textarea, useCustomInputValidation, useTranslate } from '@cloudbeaver/core-blocks';
|
||||
import { Select, FieldCheckbox, InputField, Textarea, useTranslate, useFormCustomInputValidation } from '@cloudbeaver/core-blocks';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
import {
|
||||
ESettingsValueType,
|
||||
@@ -55,7 +55,7 @@ export const SettingField = observer<Props>(function SettingField({ resolver, se
|
||||
|
||||
value = value ?? '';
|
||||
|
||||
const customValidation = useCustomInputValidation(value => {
|
||||
const { ref: customValidation } = useFormCustomInputValidation(value => {
|
||||
if (!(setting.key in settingsProviderService.schema.shape)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+11
-10
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
* Copyright (C) 2020-2026 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
InputField,
|
||||
ToolsAction,
|
||||
ToolsPanel,
|
||||
useCustomInputValidation,
|
||||
useExecutor,
|
||||
useForm,
|
||||
useFormCustomInputValidation,
|
||||
useObservableRef,
|
||||
usePasswordValidation,
|
||||
useTranslate,
|
||||
@@ -54,13 +54,6 @@ export const ChangePassword = observer(function ChangePassword() {
|
||||
const userInfoResource = useService(UserInfoResource);
|
||||
const commonDialogService = useService(CommonDialogService);
|
||||
const disabled = userInfoResource.isLoading();
|
||||
const passwordValidationRef = usePasswordValidation();
|
||||
const passwordRepeatRef = useCustomInputValidation<string>(value => {
|
||||
if (!isValuesEqual(value, state.password, null)) {
|
||||
return translate('authentication_user_passwords_not_match');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
async onSubmit() {
|
||||
@@ -75,6 +68,13 @@ export const ChangePassword = observer(function ChangePassword() {
|
||||
}
|
||||
},
|
||||
});
|
||||
const passwordValidationRef = usePasswordValidation(form);
|
||||
const passwordRepeatValidation = useFormCustomInputValidation<string>(value => {
|
||||
if (!isValuesEqual(value, state.password, null)) {
|
||||
return translate('authentication_user_passwords_not_match');
|
||||
}
|
||||
return null;
|
||||
}, form);
|
||||
|
||||
function resetForm() {
|
||||
state.oldPassword = '';
|
||||
@@ -143,11 +143,12 @@ export const ChangePassword = observer(function ChangePassword() {
|
||||
mapValue={(value?: string) => value?.trim() ?? ''}
|
||||
small
|
||||
required
|
||||
onChange={passwordRepeatValidation.revalidate}
|
||||
>
|
||||
{translate('plugin_user_profile_authentication_change_password_new_password')}
|
||||
</InputField>
|
||||
<InputField
|
||||
ref={passwordRepeatRef}
|
||||
ref={passwordRepeatValidation.ref}
|
||||
type="password"
|
||||
name="repeatedPassword"
|
||||
state={state}
|
||||
|
||||
Reference in New Issue
Block a user