aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx')
-rw-r--r--server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx185
1 files changed, 185 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx b/server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx
new file mode 100644
index 00000000000..8f43ca1c716
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx
@@ -0,0 +1,185 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import styled from '@emotion/styled';
+import { IconCheck, IconX } from '@sonarsource/echoes-react';
+import {
+ FlagErrorIcon,
+ FlagSuccessIcon,
+ FormField,
+ InputField,
+ LightLabel,
+ TextError,
+ themeColor,
+} from 'design-system';
+import * as React from 'react';
+import { translate } from '../../helpers/l10n';
+import FocusOutHandler from '../controls/FocusOutHandler';
+
+const MIN_PASSWORD_LENGTH = 12;
+
+export interface Props {
+ onChange: (password: { isValid: boolean; value: string }) => void;
+ value: string;
+}
+
+export default function UserPasswordInput(props: Readonly<Props>) {
+ const { value, onChange } = props;
+
+ const [isFocused, setIsFocused] = React.useState(false);
+ const [confirmValue, setConfirmValue] = React.useState('');
+
+ const isInvalid = !isFocused && value !== '' && !isPasswordValid(value);
+ const isValid = !isFocused && isPasswordValid(value);
+ const passwordMatch = isPasswordConfirmed(value, confirmValue);
+ const passwordDontMatch = value !== confirmValue && confirmValue !== '';
+
+ return (
+ <>
+ <FocusOutHandler className="sw-flex sw-items-center" onFocusOut={() => setIsFocused(false)}>
+ <FormField required label={translate('password')} htmlFor="create-password">
+ <div className="sw-flex sw-items-center">
+ <InputField
+ isInvalid={isInvalid}
+ isValid={isValid}
+ onFocus={() => setIsFocused(true)}
+ id="create-password"
+ size="full"
+ onChange={({ currentTarget }) => {
+ onChange({
+ value: currentTarget.value,
+ isValid:
+ isPasswordValid(currentTarget.value) &&
+ isPasswordConfirmed(currentTarget.value, confirmValue),
+ });
+ }}
+ type="password"
+ value={value}
+ />
+ {isInvalid && <FlagErrorIcon className="sw-ml-2" />}
+ {isValid && <FlagSuccessIcon className="sw-ml-2" />}
+ </div>
+ </FormField>
+ </FocusOutHandler>
+ {isInvalid && <TextError className="sw-mt-2" text={translate('user.password.invalid')} />}
+ {isFocused && <PasswordConstraint value={value} />}
+
+ <FormField
+ className="sw-mt-4"
+ required
+ label={translate('confirm_password')}
+ htmlFor="confirm-password"
+ >
+ <div className="sw-flex sw-items-center">
+ <InputField
+ isInvalid={passwordDontMatch}
+ isValid={passwordMatch}
+ onFocus={() => setIsFocused(true)}
+ id="confirm-password"
+ size="full"
+ onChange={({ currentTarget }) => {
+ setConfirmValue(currentTarget.value);
+ onChange({
+ value,
+ isValid:
+ isPasswordValid(currentTarget.value) &&
+ isPasswordConfirmed(value, currentTarget.value),
+ });
+ }}
+ type="password"
+ value={confirmValue}
+ />
+ {passwordDontMatch && <FlagErrorIcon className="sw-ml-2" />}
+ {passwordMatch && <FlagSuccessIcon className="sw-ml-2" />}
+ </div>
+ {passwordDontMatch && (
+ <TextError className="sw-mt-2" text={translate('user.password.do_not_match')} />
+ )}
+ </FormField>
+ </>
+ );
+}
+
+function PasswordConstraint({ value }: Readonly<{ value: string }>) {
+ return (
+ <div className="sw-mt-2">
+ <LightLabel>{translate('user.password.conditions')}</LightLabel>
+ <ul className="sw-list-none sw-p-0 sw-mt-1">
+ <Condition
+ condition={contains12Characters(value)}
+ label={translate('user.password.condition.12_characters')}
+ />
+ <Condition
+ condition={containsUppercase(value)}
+ label={translate('user.password.condition.1_upper_case')}
+ />
+ <Condition
+ condition={containsLowercase(value)}
+ label={translate('user.password.condition.1_lower_case')}
+ />
+ <Condition
+ condition={containsDigit(value)}
+ label={translate('user.password.condition.1_number')}
+ />
+ <Condition
+ condition={containsSpecialCharacter(value)}
+ label={translate('user.password.condition.1_special_character')}
+ />
+ </ul>
+ </div>
+ );
+}
+
+function Condition({ condition, label }: Readonly<{ condition: boolean; label: string }>) {
+ return (
+ <li className="sw-mb-1">
+ {condition ? (
+ <SuccessLabel>
+ <IconCheck />
+ {label}
+ </SuccessLabel>
+ ) : (
+ <LightLabel>
+ <IconX />
+ {label}
+ </LightLabel>
+ )}
+ </li>
+ );
+}
+
+const contains12Characters = (password: string) => password.length >= MIN_PASSWORD_LENGTH;
+const containsUppercase = (password: string) => /[A-Z]/.test(password);
+const containsLowercase = (password: string) => /[a-z]/.test(password);
+const containsDigit = (password: string) => /\d/.test(password);
+const containsSpecialCharacter = (password: string) => /[^a-zA-Z0-9]/.test(password);
+
+const isPasswordValid = (password: string) =>
+ contains12Characters(password) &&
+ containsUppercase(password) &&
+ containsLowercase(password) &&
+ containsDigit(password) &&
+ containsSpecialCharacter(password);
+
+const isPasswordConfirmed = (password: string, confirm: string) =>
+ password === confirm && password !== '';
+
+const SuccessLabel = styled(LightLabel)`
+ color: ${themeColor('textSuccess')};
+`;