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",
"AnErrorOccurred": "An error occurred",
"AuditLog": "Audit Logs",
"AuditLogs": "Audit Logs",
"BlockedIPAddresses": "Blocked IP addresses",
@ -8,6 +10,7 @@
"ClientDomainManager": "Client Domain Manager",
"ClientDomains": "Client Domains",
"Comment": "Comment",
"ConfirmPassword": "Confirm Password",
"CustomFieldManager": "Custom Field Manager",
"CustomFields": "Custom Fields",
"DisplayName": "Display Name",
@ -35,6 +38,10 @@
"NumberOfAttempts": "Number Of Attempts",
"NewValue": "New 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",
"Sequence": "Sequence",
"SequenceManager": "Sequence Manager",

View File

@ -1,141 +1,174 @@
import Form, { FormState, FormData, businessValidationResult } from "../../../components/common/Form";
import Joi from "joi";
import React, { useState } from "react";
import authentication from "../services/authenticationService";
import { IEmailUserAction } from "../models/IEmailUserAction";
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 {
password: string;
confirmPassword: string;
emailConfirmed: boolean;
password: string;
confirmPassword: string;
emailConfirmed: boolean;
}
export interface EmailUserActionConfirmEmailState extends FormState {
data: EmailUserActionConfirmEmailData;
hasTwelveCharacters: boolean,
hasSpecialCharacter: boolean,
hasUppercaseLetter: boolean,
hasLowercaseLetter: boolean,
hasNumber: boolean
interface Props {
emailUserAction: IEmailUserAction;
}
class EmailUserActionConfirmEmail extends Form<any, any, EmailUserActionConfirmEmailState> {
state = {
loaded: true,
passwordMaxLenght: 255,
data: { password: "", confirmPassword: "", emailConfirmed: false },
errors: {},
hasTwelveCharacters: false,
hasSpecialCharacter: false,
hasUppercaseLetter: false,
hasLowercaseLetter: false,
hasNumber: false
};
const EmailUserActionConfirmEmail: React.FC<Props> = ({ emailUserAction }) => {
const { t } = useTranslation<typeof Namespaces.Common>();
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [emailConfirmed, setEmailConfirmed] = useState(false);
const [generalError, setGeneralError] = useState("");
const [errors, setErrors] = useState<{ [key: string]: string }>({});
const [hasTwelveCharacters, setHasTwelveCharacters] = useState(false);
const [hasSpecialCharacter, setHasSpecialCharacter] = useState(false);
const [hasUppercaseLetter, setHasUppercaseLetter] = useState(false);
const [hasLowercaseLetter, setHasLowercaseLetter] = useState(false);
const [hasNumber, setHasNumber] = useState(false);
labelPassword = "Password";
labelConfirmPassword = "Confirm Password";
labelConfirmEmail = "Activate";
const LABEL_PASSWORD = t("Password");
const LABEL_CONFIRM_PASSWORD = t("Confirmassword");
const LABEL_CONFIRM_EMAIL = t("Activate");
const PASSWORD_MAX_LENGTH = 255;
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),
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);
};
emailConfirmed: Joi.boolean(),
};
const handleConfirmPasswordChange = (
e: React.ChangeEvent<HTMLInputElement>,
) => {
setConfirmPassword(e.currentTarget.value);
};
BusinessValidation(): businessValidationResult | null {
const { password, confirmPassword } = this.state.data;
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const newErrors: { [key: string]: string } = {};
if (password !== confirmPassword) {
return {
details: [
{
path: "confirmPassword",
message: "You need to confirm by typing exactly the same as the new password",
},
],
};
}
return null;
const minPasswordLength = 12;
// Validation
if (!password) {
newErrors.password = t("PasswordIsRequired");
} else if (password.length < minPasswordLength) {
newErrors.password = t("PasswordMinLength", {
minPasswordLength: minPasswordLength,
});
}
handlePasswordChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const stateData = this.state;
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) => {
const { emailUserAction } = this.props;
const { password } = this.state.data;
const action: IEmailUserAction = {
email: emailUserAction.email,
token: emailUserAction.token,
password: password,
emailActionType: emailUserAction.emailActionType,
};
try {
const callResult = await authentication.completeEmailAction(action);
if (callResult === 1) {
let data = { ...this.state.data };
data.emailConfirmed = true;
this.setState({ data });
setTimeout(function () {
window.location.replace('/login');
}, 1000);
}
}
catch(ex: any) {
this.handleGeneralError(ex);
}
};
render() {
const { emailConfirmed, password, confirmPassword } = this.state.data;
const { hasNumber, hasLowercaseLetter, hasSpecialCharacter, hasUppercaseLetter, hasTwelveCharacters, passwordMaxLenght } = this.state;
const isFormValid = password !== "" && password === confirmPassword && hasNumber && hasLowercaseLetter && hasSpecialCharacter && hasUppercaseLetter && hasTwelveCharacters;
if (emailConfirmed) {
return <div className="alert alert-info">Success, your e-mail is confirmed. You can now log in.</div>;
}
return (
<>
<div>To activate your account, please enter a password</div>
<form onSubmit={this.handleSubmit}>
{this.renderError("_general")}
{this.renderInputWithChangeEvent("password", "", InputType.password, undefined, this.handlePasswordChange, undefined, this.labelPassword, passwordMaxLenght)}
<div className={hasTwelveCharacters ? "checked" : "unchecked"}>Password requires a minimum of 12 characters containing a combination of:</div>
<ul>
<li className={hasSpecialCharacter ? "checked" : ""}>At least 1 symbol</li>
<li className={hasNumber ? "checked" : ""}>At least 1 number</li>
<li className={hasLowercaseLetter ? "checked" : ""}>At least 1 lowercase letter</li>
<li className={hasUppercaseLetter ? "checked" : ""}>At least 1 uppercase letter</li>
</ul>
{this.renderInput("confirmPassword", "", InputType.password, undefined, undefined, this.labelConfirmPassword, passwordMaxLenght)}
{this.renderButton(this.labelConfirmEmail, "confirmEmail", undefined, undefined, isFormValid)}
</form>
</>
);
if (password && password !== confirmPassword) {
newErrors.confirmPassword = t("PasswordsMustMatch");
}
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
return;
}
setErrors({});
try {
const action: IEmailUserAction = {
email: emailUserAction.email,
token: emailUserAction.token,
password: password,
emailActionType: emailUserAction.emailActionType,
};
const callResult = await authentication.completeEmailAction(action);
if (callResult === 1) {
setEmailConfirmed(true);
setTimeout(() => {
window.location.replace("/login");
}, 1000);
}
} catch (ex: any) {
setGeneralError(ex?.message || t("AnErrorOccurred"));
}
};
const isFormValid =
password !== "" &&
password === confirmPassword &&
hasNumber &&
hasLowercaseLetter &&
hasSpecialCharacter &&
hasUppercaseLetter &&
hasTwelveCharacters;
if (emailConfirmed) {
return (
<div className="alert alert-info">
Success, your e-mail is confirmed. You can now log in.
</div>
);
}
return (
<>
<div>To activate your account, please enter a password</div>
<form onSubmit={handleSubmit}>
{generalError && <ErrorBlock error={generalError} />}
<Input
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>
<li className={hasSpecialCharacter ? "checked" : ""}>
At least 1 symbol
</li>
<li className={hasNumber ? "checked" : ""}>At least 1 number</li>
<li className={hasLowercaseLetter ? "checked" : ""}>
At least 1 lowercase letter
</li>
<li className={hasUppercaseLetter ? "checked" : ""}>
At least 1 uppercase letter
</li>
</ul>
<Input
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>
</>
);
};
export default EmailUserActionConfirmEmail;