Upgraded ConfirmEmail

This commit is contained in:
Colin Dawson 2026-01-30 11:30:40 +00:00
parent 8308515c9b
commit 04c1875d5d
2 changed files with 162 additions and 122 deletions

View File

@ -1,5 +1,7 @@
{ {
"Activate": "Activate",
"Admin": "Admin", "Admin": "Admin",
"AnErrorOccurred": "An error occurred",
"AuditLog": "Audit Logs", "AuditLog": "Audit Logs",
"AuditLogs": "Audit Logs", "AuditLogs": "Audit Logs",
"BlockedIPAddresses": "Blocked IP addresses", "BlockedIPAddresses": "Blocked IP addresses",
@ -8,6 +10,7 @@
"ClientDomainManager": "Client Domain Manager", "ClientDomainManager": "Client Domain Manager",
"ClientDomains": "Client Domains", "ClientDomains": "Client Domains",
"Comment": "Comment", "Comment": "Comment",
"ConfirmPassword": "Confirm Password",
"CustomFieldManager": "Custom Field Manager", "CustomFieldManager": "Custom Field Manager",
"CustomFields": "Custom Fields", "CustomFields": "Custom Fields",
"DisplayName": "Display Name", "DisplayName": "Display Name",
@ -35,6 +38,10 @@
"NumberOfAttempts": "Number Of Attempts", "NumberOfAttempts": "Number Of Attempts",
"NewValue": "New Value", "NewValue": "New Value",
"OldValue": "Old Value", "OldValue": "Old Value",
"Password": "Password",
"PasswordIsRequired": "Password is required",
"PasswordMinLength": "Password must be at least {{minPasswordLength}} characters",
"PasswordsMustMatch": "You need to confirm by typing exactly the same as the new password",
"PressAgainToUnblock": "Press again to unblock", "PressAgainToUnblock": "Press again to unblock",
"Sequence": "Sequence", "Sequence": "Sequence",
"SequenceManager": "Sequence Manager", "SequenceManager": "Sequence Manager",

View File

@ -1,8 +1,13 @@
import Form, { FormState, FormData, businessValidationResult } from "../../../components/common/Form"; import React, { useState } from "react";
import Joi from "joi";
import authentication from "../services/authenticationService"; import authentication from "../services/authenticationService";
import { IEmailUserAction } from "../models/IEmailUserAction"; import { IEmailUserAction } from "../models/IEmailUserAction";
import { InputType } from "../../../components/common/Input"; import { InputType } from "../../../components/common/Input";
import Input from "../../../components/common/Input";
import Button, { ButtonType } from "../../../components/common/Button";
import ErrorBlock from "../../../components/common/ErrorBlock";
import { FormData } from "../../../components/common/Form";
import { useTranslation } from "react-i18next";
import { Namespaces } from "../../../i18n/i18n";
export interface EmailUserActionConfirmEmailData extends FormData { export interface EmailUserActionConfirmEmailData extends FormData {
password: string; password: string;
@ -10,81 +15,74 @@ export interface EmailUserActionConfirmEmailData extends FormData {
emailConfirmed: boolean; emailConfirmed: boolean;
} }
export interface EmailUserActionConfirmEmailState extends FormState { interface Props {
data: EmailUserActionConfirmEmailData; emailUserAction: IEmailUserAction;
hasTwelveCharacters: boolean,
hasSpecialCharacter: boolean,
hasUppercaseLetter: boolean,
hasLowercaseLetter: boolean,
hasNumber: boolean
} }
class EmailUserActionConfirmEmail extends Form<any, any, EmailUserActionConfirmEmailState> { const EmailUserActionConfirmEmail: React.FC<Props> = ({ emailUserAction }) => {
state = { const { t } = useTranslation<typeof Namespaces.Common>();
loaded: true, const [password, setPassword] = useState("");
passwordMaxLenght: 255, const [confirmPassword, setConfirmPassword] = useState("");
data: { password: "", confirmPassword: "", emailConfirmed: false }, const [emailConfirmed, setEmailConfirmed] = useState(false);
errors: {}, const [generalError, setGeneralError] = useState("");
hasTwelveCharacters: false, const [errors, setErrors] = useState<{ [key: string]: string }>({});
hasSpecialCharacter: false, const [hasTwelveCharacters, setHasTwelveCharacters] = useState(false);
hasUppercaseLetter: false, const [hasSpecialCharacter, setHasSpecialCharacter] = useState(false);
hasLowercaseLetter: false, const [hasUppercaseLetter, setHasUppercaseLetter] = useState(false);
hasNumber: false const [hasLowercaseLetter, setHasLowercaseLetter] = useState(false);
const [hasNumber, setHasNumber] = useState(false);
const LABEL_PASSWORD = t("Password");
const LABEL_CONFIRM_PASSWORD = t("Confirmassword");
const LABEL_CONFIRM_EMAIL = t("Activate");
const PASSWORD_MAX_LENGTH = 255;
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPassword = e.currentTarget.value;
setPassword(newPassword);
setConfirmPassword("");
setHasNumber(/\d+/g.test(newPassword));
setHasLowercaseLetter(/[a-z]/g.test(newPassword));
setHasUppercaseLetter(/[A-Z]/g.test(newPassword));
setHasSpecialCharacter(
/[ ~`! @#$%^&*()_+\-=[\]{};:\\|,.'"<>/?]/.test(newPassword),
);
setHasTwelveCharacters(newPassword.length >= 12);
}; };
labelPassword = "Password"; const handleConfirmPasswordChange = (
labelConfirmPassword = "Confirm Password"; e: React.ChangeEvent<HTMLInputElement>,
labelConfirmEmail = "Activate"; ) => {
setConfirmPassword(e.currentTarget.value);
schema = {
password: Joi.string().required().min(12).label(this.labelPassword),
confirmPassword: Joi.string()
.when("password", {
is: "",
then: Joi.optional(),
otherwise: Joi.valid(Joi.ref("password")).error(() => {
const e = new Error("Passwords must match");
e.name = "confirmPassword";
return e;
}),
})
.label(this.labelConfirmPassword),
emailConfirmed: Joi.boolean(),
}; };
BusinessValidation(): businessValidationResult | null { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
const { password, confirmPassword } = this.state.data; e.preventDefault();
const newErrors: { [key: string]: string } = {};
if (password !== confirmPassword) { const minPasswordLength = 12;
return {
details: [ // Validation
{ if (!password) {
path: "confirmPassword", newErrors.password = t("PasswordIsRequired");
message: "You need to confirm by typing exactly the same as the new password", } else if (password.length < minPasswordLength) {
}, newErrors.password = t("PasswordMinLength", {
], minPasswordLength: minPasswordLength,
}; });
}
return null;
} }
handlePasswordChange = async (e: React.ChangeEvent<HTMLInputElement>) => { if (password && password !== confirmPassword) {
const stateData = this.state; newErrors.confirmPassword = t("PasswordsMustMatch");
stateData.data.password = e.currentTarget.value; }
stateData.data.confirmPassword = "";
stateData.hasNumber = /\d+/g.test(stateData.data.password);
stateData.hasLowercaseLetter = /[a-z]/g.test(stateData.data.password);
stateData.hasUppercaseLetter = /[A-Z]/g.test(stateData.data.password);;
stateData.hasSpecialCharacter = /[ ~`! @#$%^&*()_+\-=[\]{};:\\|,.'"<>/?]/.test(stateData.data.password);
stateData.hasTwelveCharacters = stateData.data.password.length >= 12;
this.setState(stateData)
};
doSubmit = async (buttonName : string) => { if (Object.keys(newErrors).length > 0) {
const { emailUserAction } = this.props; setErrors(newErrors);
const { password } = this.state.data; return;
}
setErrors({});
try {
const action: IEmailUserAction = { const action: IEmailUserAction = {
email: emailUserAction.email, email: emailUserAction.email,
token: emailUserAction.token, token: emailUserAction.token,
@ -92,50 +90,85 @@ class EmailUserActionConfirmEmail extends Form<any, any, EmailUserActionConfirmE
emailActionType: emailUserAction.emailActionType, emailActionType: emailUserAction.emailActionType,
}; };
try {
const callResult = await authentication.completeEmailAction(action); const callResult = await authentication.completeEmailAction(action);
if (callResult === 1) { if (callResult === 1) {
let data = { ...this.state.data }; setEmailConfirmed(true);
data.emailConfirmed = true; setTimeout(() => {
this.setState({ data }); window.location.replace("/login");
setTimeout(function () {
window.location.replace('/login');
}, 1000); }, 1000);
} }
} } catch (ex: any) {
catch(ex: any) { setGeneralError(ex?.message || t("AnErrorOccurred"));
this.handleGeneralError(ex);
} }
}; };
render() { const isFormValid =
const { emailConfirmed, password, confirmPassword } = this.state.data; password !== "" &&
const { hasNumber, hasLowercaseLetter, hasSpecialCharacter, hasUppercaseLetter, hasTwelveCharacters, passwordMaxLenght } = this.state; password === confirmPassword &&
const isFormValid = password !== "" && password === confirmPassword && hasNumber && hasLowercaseLetter && hasSpecialCharacter && hasUppercaseLetter && hasTwelveCharacters; hasNumber &&
hasLowercaseLetter &&
hasSpecialCharacter &&
hasUppercaseLetter &&
hasTwelveCharacters;
if (emailConfirmed) { if (emailConfirmed) {
return <div className="alert alert-info">Success, your e-mail is confirmed. You can now log in.</div>; return (
<div className="alert alert-info">
Success, your e-mail is confirmed. You can now log in.
</div>
);
} }
return ( return (
<> <>
<div>To activate your account, please enter a password</div> <div>To activate your account, please enter a password</div>
<form onSubmit={this.handleSubmit}> <form onSubmit={handleSubmit}>
{this.renderError("_general")} {generalError && <ErrorBlock error={generalError} />}
{this.renderInputWithChangeEvent("password", "", InputType.password, undefined, this.handlePasswordChange, undefined, this.labelPassword, passwordMaxLenght)} <Input
<div className={hasTwelveCharacters ? "checked" : "unchecked"}>Password requires a minimum of 12 characters containing a combination of:</div> name="password"
label={LABEL_PASSWORD}
type={InputType.password}
value={password}
onChange={handlePasswordChange}
maxLength={PASSWORD_MAX_LENGTH}
error={errors.password}
/>
<div className={hasTwelveCharacters ? "checked" : "unchecked"}>
Password requires a minimum of 12 characters containing a combination
of:
</div>
<ul> <ul>
<li className={hasSpecialCharacter ? "checked" : ""}>At least 1 symbol</li> <li className={hasSpecialCharacter ? "checked" : ""}>
At least 1 symbol
</li>
<li className={hasNumber ? "checked" : ""}>At least 1 number</li> <li className={hasNumber ? "checked" : ""}>At least 1 number</li>
<li className={hasLowercaseLetter ? "checked" : ""}>At least 1 lowercase letter</li> <li className={hasLowercaseLetter ? "checked" : ""}>
<li className={hasUppercaseLetter ? "checked" : ""}>At least 1 uppercase letter</li> At least 1 lowercase letter
</li>
<li className={hasUppercaseLetter ? "checked" : ""}>
At least 1 uppercase letter
</li>
</ul> </ul>
{this.renderInput("confirmPassword", "", InputType.password, undefined, undefined, this.labelConfirmPassword, passwordMaxLenght)} <Input
{this.renderButton(this.labelConfirmEmail, "confirmEmail", undefined, undefined, isFormValid)} name="confirmPassword"
label={LABEL_CONFIRM_PASSWORD}
type={InputType.password}
value={confirmPassword}
onChange={handleConfirmPasswordChange}
maxLength={PASSWORD_MAX_LENGTH}
error={errors.confirmPassword}
/>
<Button
buttonType={ButtonType.primary}
disabled={!isFormValid}
onClick={() => {}}
>
{LABEL_CONFIRM_EMAIL}
</Button>
</form> </form>
</> </>
); );
} };
}
export default EmailUserActionConfirmEmail; export default EmailUserActionConfirmEmail;