htmlIsland support added, so the profile and login forms are now embedded into react controls.
This commit is contained in:
parent
62e0f966b8
commit
5fb966996a
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@ -28,8 +28,12 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<link rel="stylesheet" href="/styles.css">
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
<title>e-suite</title>
|
<title>e-suite</title>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@ -134,6 +134,7 @@
|
|||||||
"PressAgainToUnblock": "Press again to unblock",
|
"PressAgainToUnblock": "Press again to unblock",
|
||||||
"PrintSpecification": "Print Specification",
|
"PrintSpecification": "Print Specification",
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
|
"ProfileSaved": "Profile updated.",
|
||||||
"ResendConfirm": "Resend Confirm",
|
"ResendConfirm": "Resend Confirm",
|
||||||
"Required": "Required",
|
"Required": "Required",
|
||||||
"ResetPassword": "Reset Password",
|
"ResetPassword": "Reset Password",
|
||||||
|
|||||||
8
public/locales/en/htmlIsland.json
Normal file
8
public/locales/en/htmlIsland.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"island": {
|
||||||
|
"loadError": "Failed to load this section.",
|
||||||
|
"networkError": "Network error while saving.",
|
||||||
|
"serverError": "Server error while saving.",
|
||||||
|
"saveSuccess": "Saved successfully."
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -134,6 +134,7 @@
|
|||||||
"PressAgainToUnblock": "Appuyez de nouveau pour débloquer",
|
"PressAgainToUnblock": "Appuyez de nouveau pour débloquer",
|
||||||
"PrintSpecification": "Imprimer la spécification",
|
"PrintSpecification": "Imprimer la spécification",
|
||||||
"Profile": "Profil",
|
"Profile": "Profil",
|
||||||
|
"ProfileSaved": "Profil mis à jour.",
|
||||||
"ResendConfirm": "Renvoyer la confirmation",
|
"ResendConfirm": "Renvoyer la confirmation",
|
||||||
"Required": "Requis",
|
"Required": "Requis",
|
||||||
"ResetPassword": "Réinitialiser le mot de passe",
|
"ResetPassword": "Réinitialiser le mot de passe",
|
||||||
|
|||||||
8
public/locales/fr/htmlIsland.json
Normal file
8
public/locales/fr/htmlIsland.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"island": {
|
||||||
|
"loadError": "Échec du chargement de cette section.",
|
||||||
|
"networkError": "Erreur réseau lors de l’enregistrement.",
|
||||||
|
"serverError": "Erreur du serveur lors de l’enregistrement.",
|
||||||
|
"saveSuccess": "Enregistré avec succès."
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/App.tsx
27
src/App.tsx
@ -9,7 +9,6 @@ import ForgotPassword from "./modules/frame/components/ForgotPassword";
|
|||||||
import NotFound from "./modules/frame/components/NotFound";
|
import NotFound from "./modules/frame/components/NotFound";
|
||||||
import Logout from "./modules/frame/components/Logout";
|
import Logout from "./modules/frame/components/Logout";
|
||||||
import LoginForm from "./modules/frame/components/LoginForm";
|
import LoginForm from "./modules/frame/components/LoginForm";
|
||||||
import Redirect from "./components/common/Redirect";
|
|
||||||
import Mainframe from "./modules/frame/components/Mainframe";
|
import Mainframe from "./modules/frame/components/Mainframe";
|
||||||
import EmailUserAction from "./modules/frame/components/EmailUserAction";
|
import EmailUserAction from "./modules/frame/components/EmailUserAction";
|
||||||
import { HashNavigationProvider } from "./utils/HashNavigationContext";
|
import { HashNavigationProvider } from "./utils/HashNavigationContext";
|
||||||
@ -48,9 +47,7 @@ import { Namespaces } from "./i18n/i18n";
|
|||||||
|
|
||||||
function GetSecureRoutes() {
|
function GetSecureRoutes() {
|
||||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
const profileRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? (
|
const profileRoute = (
|
||||||
<Route path="/profile" element={<Redirect to="/account/profile" />} />
|
|
||||||
) : (
|
|
||||||
<Route
|
<Route
|
||||||
path="/profile"
|
path="/profile"
|
||||||
element={
|
element={
|
||||||
@ -444,19 +441,6 @@ function App() {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginRoute = window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN ? (
|
|
||||||
<Route path="/login" element={<Redirect to="/account/login" />} />
|
|
||||||
) : (
|
|
||||||
<Route
|
|
||||||
path="/login"
|
|
||||||
element={
|
|
||||||
<LoginFrame>
|
|
||||||
<LoginForm />
|
|
||||||
</LoginFrame>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<HashNavigationProvider>
|
<HashNavigationProvider>
|
||||||
@ -466,7 +450,14 @@ function App() {
|
|||||||
<main>
|
<main>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/env" element={<EnvPage />} />
|
<Route path="/env" element={<EnvPage />} />
|
||||||
{loginRoute}
|
<Route
|
||||||
|
path="/login"
|
||||||
|
element={
|
||||||
|
<LoginFrame>
|
||||||
|
<LoginForm />
|
||||||
|
</LoginFrame>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/forgot-password"
|
path="/forgot-password"
|
||||||
element={
|
element={
|
||||||
|
|||||||
181
src/components/common/HtmlIsland.tsx
Normal file
181
src/components/common/HtmlIsland.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Namespaces } from "../../i18n/i18n";
|
||||||
|
|
||||||
|
export interface HtmlIslandProps {
|
||||||
|
url: string;
|
||||||
|
islandId?: string;
|
||||||
|
successMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HtmlIsland: React.FC<HtmlIslandProps> = ({
|
||||||
|
url,
|
||||||
|
islandId,
|
||||||
|
successMessage,
|
||||||
|
}) => {
|
||||||
|
const [html, setHtml] = useState("");
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { t: tIsland } = useTranslation(Namespaces.HtmlIsland);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Load the island HTML
|
||||||
|
//
|
||||||
|
const loadIsland = useCallback(() => {
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
fetch(url, { credentials: "include" })
|
||||||
|
.then((r) => {
|
||||||
|
if (!r.ok) throw new Error(`Failed to load island: ${r.status}`);
|
||||||
|
return r.text();
|
||||||
|
})
|
||||||
|
.then((text) => setHtml(text))
|
||||||
|
.catch(() => {
|
||||||
|
setError(tIsland("island.loadError"));
|
||||||
|
toast.error(tIsland("island.loadError"));
|
||||||
|
});
|
||||||
|
}, [url, tIsland]);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Initial load + reload when URL changes
|
||||||
|
//
|
||||||
|
useEffect(() => {
|
||||||
|
loadIsland();
|
||||||
|
}, [loadIsland]);
|
||||||
|
|
||||||
|
//
|
||||||
|
// After HTML is injected, run scripts + bind form handling
|
||||||
|
//
|
||||||
|
useEffect(() => {
|
||||||
|
if (!html || !containerRef.current) return;
|
||||||
|
|
||||||
|
const container = containerRef.current;
|
||||||
|
|
||||||
|
//
|
||||||
|
// 1. Extract and execute <script> tags
|
||||||
|
//
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, "text/html");
|
||||||
|
const scripts = Array.from(doc.querySelectorAll("script"));
|
||||||
|
|
||||||
|
scripts.forEach((script) => {
|
||||||
|
const newScript = document.createElement("script");
|
||||||
|
|
||||||
|
if (script.src) {
|
||||||
|
newScript.src = script.src;
|
||||||
|
} else {
|
||||||
|
newScript.textContent = script.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (islandId) {
|
||||||
|
newScript.dataset.island = islandId;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(newScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// 2. Rebind jQuery Unobtrusive Validation (if available)
|
||||||
|
//
|
||||||
|
const w = window as any;
|
||||||
|
if (
|
||||||
|
w.jQuery &&
|
||||||
|
w.jQuery.validator &&
|
||||||
|
w.jQuery.validator.unobtrusive &&
|
||||||
|
container
|
||||||
|
) {
|
||||||
|
w.jQuery.validator.unobtrusive.parse(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// 3. Intercept form submissions (unless opted out)
|
||||||
|
//
|
||||||
|
const form = container.querySelector("form");
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
const shouldFullPageRedirect =
|
||||||
|
form.getAttribute("data-full-page-redirect") === "true";
|
||||||
|
|
||||||
|
if (shouldFullPageRedirect) {
|
||||||
|
// Do NOT intercept this form – let the browser handle POST + redirect
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitHandler = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
let response: Response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(form.action, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
credentials: "include",
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
setError(tIsland("island.networkError"));
|
||||||
|
toast.error(tIsland("island.networkError"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHttpRedirect = [301, 302, 303, 307, 308].includes(
|
||||||
|
response.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHttpRedirect) {
|
||||||
|
toast.success(
|
||||||
|
successMessage ? successMessage : tIsland("island.saveSuccess"),
|
||||||
|
);
|
||||||
|
loadIsland();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setError(tIsland("island.serverError"));
|
||||||
|
toast.error(tIsland("island.serverError"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHtml = await response.text();
|
||||||
|
setHtml(newHtml);
|
||||||
|
toast.success(
|
||||||
|
successMessage ? successMessage : tIsland("island.saveSuccess"),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
form.addEventListener("submit", submitHandler);
|
||||||
|
|
||||||
|
// Cleanup on re-render
|
||||||
|
return () => {
|
||||||
|
form.removeEventListener("submit", submitHandler);
|
||||||
|
};
|
||||||
|
}, [html, islandId, successMessage, tIsland, loadIsland]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#fee",
|
||||||
|
border: "1px solid #c00",
|
||||||
|
padding: "8px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: "#900",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={containerRef} dangerouslySetInnerHTML={{ __html: html }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HtmlIsland;
|
||||||
@ -8,6 +8,7 @@ import { fallbackLng } from "./generatedLocales";
|
|||||||
export const Namespaces = {
|
export const Namespaces = {
|
||||||
Common: "common",
|
Common: "common",
|
||||||
MailTypes: "mailTypes",
|
MailTypes: "mailTypes",
|
||||||
|
HtmlIsland: "htmlIsland",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type Namespace = (typeof Namespaces)[keyof typeof Namespaces];
|
export type Namespace = (typeof Namespaces)[keyof typeof Namespaces];
|
||||||
|
|||||||
21
src/modules/frame/components/ExternalLoginForm.tsx
Normal file
21
src/modules/frame/components/ExternalLoginForm.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import HtmlIsland from "../../../components/common/HtmlIsland";
|
||||||
|
import { Namespaces } from "../../../i18n/i18n";
|
||||||
|
|
||||||
|
export interface ExternalLoginFormProps {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExternalLoginForm: React.FC<ExternalLoginFormProps> = ({ url }) => {
|
||||||
|
const { t } = useTranslation(Namespaces.Common);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HtmlIsland
|
||||||
|
url={url}
|
||||||
|
islandId="loginform"
|
||||||
|
successMessage={t("ProfileSaved")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExternalLoginForm;
|
||||||
245
src/modules/frame/components/InternalLoginForm.tsx
Normal file
245
src/modules/frame/components/InternalLoginForm.tsx
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
|
import { Link, Navigate } from "react-router-dom";
|
||||||
|
import Joi from "joi";
|
||||||
|
import authentication from "../services/authenticationService";
|
||||||
|
import { InputType } from "../../../components/common/Input";
|
||||||
|
import { ButtonType } from "../../../components/common/Button";
|
||||||
|
import { useForm } from "../../../components/common/useForm";
|
||||||
|
import {
|
||||||
|
renderButton,
|
||||||
|
renderError,
|
||||||
|
renderInput,
|
||||||
|
} from "../../../components/common/formHelpers";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Namespaces } from "../../../i18n/i18n";
|
||||||
|
|
||||||
|
const InternalLoginForm: React.FC = () => {
|
||||||
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
|
const [isInNextStage, setIsInNextStage] = useState(false);
|
||||||
|
const [emailSent, setEmailSent] = useState(false);
|
||||||
|
const passwordMaxLength = 255;
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
loaded: true,
|
||||||
|
data: {
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
tfaNeeded: false,
|
||||||
|
requestTfaRemoval: false,
|
||||||
|
securityCode: "",
|
||||||
|
},
|
||||||
|
errors: {},
|
||||||
|
redirect: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = {
|
||||||
|
username: Joi.string()
|
||||||
|
.required()
|
||||||
|
.email({ tlds: { allow: false } })
|
||||||
|
.label(t("Email")),
|
||||||
|
password: Joi.string().required().label(t("Password")),
|
||||||
|
tfaNeeded: Joi.boolean().required(),
|
||||||
|
requestTfaRemoval: Joi.boolean().required(),
|
||||||
|
securityCode: Joi.string().allow("").label(t("Authenticate")),
|
||||||
|
};
|
||||||
|
|
||||||
|
form.schema = schema;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.location.replace("/login");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const performLogin = useCallback(
|
||||||
|
async (data: any) => {
|
||||||
|
try {
|
||||||
|
const result = await authentication.login(
|
||||||
|
data.username,
|
||||||
|
data.password,
|
||||||
|
data.securityCode,
|
||||||
|
data.requestTfaRemoval,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (result) {
|
||||||
|
case 1: {
|
||||||
|
const nextData = { ...form.state.data };
|
||||||
|
|
||||||
|
if (!nextData.tfaNeeded) {
|
||||||
|
nextData.tfaNeeded = true;
|
||||||
|
form.setState({ data: nextData });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
window.location.href = "/";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (ex: any) {
|
||||||
|
form.handleGeneralError(ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[form],
|
||||||
|
);
|
||||||
|
|
||||||
|
const doSubmit = async () => {
|
||||||
|
const { data } = form.state;
|
||||||
|
await performLogin(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
form.handleSubmit(e, async () => doSubmit());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextClick = async () => {
|
||||||
|
const data = { ...form.state.data };
|
||||||
|
const validationResult = schema.username.validate(data.username);
|
||||||
|
if (validationResult.error === undefined) {
|
||||||
|
setIsInNextStage(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleForgetPassword = async () => {
|
||||||
|
try {
|
||||||
|
await authentication.forgotPassword(form.state.data.username as string);
|
||||||
|
setEmailSent(true);
|
||||||
|
const nextData = { ...form.state.data, username: "", password: "" };
|
||||||
|
form.setState({ data: nextData });
|
||||||
|
} catch (ex: any) {
|
||||||
|
form.handleGeneralError(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const authenticationWorkAround = async () => {
|
||||||
|
const data = { ...form.state.data, requestTfaRemoval: true };
|
||||||
|
await performLogin(data);
|
||||||
|
form.setState({ data });
|
||||||
|
};
|
||||||
|
|
||||||
|
const { tfaNeeded, requestTfaRemoval } = form.state.data as any;
|
||||||
|
const result = schema.username.validate(form.state.data.username);
|
||||||
|
const validEmail = result.error === undefined;
|
||||||
|
|
||||||
|
if (authentication.getCurrentUser()) return <Navigate to="/" />;
|
||||||
|
|
||||||
|
const requestTfaRemovalPanel = (
|
||||||
|
<div>
|
||||||
|
An email has been sent to you so that you can regain control of your
|
||||||
|
account.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginPanel = (
|
||||||
|
<>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{renderInput(
|
||||||
|
"username",
|
||||||
|
"",
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.text,
|
||||||
|
isInNextStage,
|
||||||
|
"",
|
||||||
|
t("Email") as string,
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
"username",
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
{renderInput(
|
||||||
|
"password",
|
||||||
|
"",
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.password,
|
||||||
|
emailSent,
|
||||||
|
"",
|
||||||
|
t("Password") as string,
|
||||||
|
passwordMaxLength,
|
||||||
|
isInNextStage,
|
||||||
|
"current-password",
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
{!isInNextStage &&
|
||||||
|
renderButton(
|
||||||
|
t("Next"),
|
||||||
|
form.state.errors,
|
||||||
|
"next",
|
||||||
|
handleNextClick,
|
||||||
|
"login",
|
||||||
|
validEmail,
|
||||||
|
ButtonType.primary,
|
||||||
|
true,
|
||||||
|
)}
|
||||||
|
{isInNextStage && (
|
||||||
|
<div className="clickables">
|
||||||
|
{renderButton(
|
||||||
|
"Login",
|
||||||
|
form.state.errors,
|
||||||
|
"login",
|
||||||
|
undefined,
|
||||||
|
"login",
|
||||||
|
!emailSent,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
{isInNextStage && (
|
||||||
|
<div className="forgottenLink">
|
||||||
|
{renderButton(
|
||||||
|
t("ForgottenPassword") as string,
|
||||||
|
form.state.errors,
|
||||||
|
"forgot-password",
|
||||||
|
handleForgetPassword,
|
||||||
|
"forgot-password",
|
||||||
|
validEmail,
|
||||||
|
ButtonType.secondary,
|
||||||
|
true,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{emailSent && (
|
||||||
|
<div className="alert alert-info emailSent">
|
||||||
|
If you have a registered account, you will receive an email.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{renderError("_general", form.state.errors)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tfaPanel = (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{renderError("_general", form.state.errors)}
|
||||||
|
{renderInput(
|
||||||
|
"securityCode",
|
||||||
|
t("Authenticate") as string,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.text,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
{renderButton(t("Authenticate"), form.state.errors, "authenticate")}
|
||||||
|
<Link to="#" onClick={authenticationWorkAround}>
|
||||||
|
My Authenticator is not working
|
||||||
|
</Link>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{requestTfaRemoval
|
||||||
|
? requestTfaRemovalPanel
|
||||||
|
: tfaNeeded
|
||||||
|
? tfaPanel
|
||||||
|
: loginPanel}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InternalLoginForm;
|
||||||
@ -1,244 +1,19 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import ExternalLoginForm from "./ExternalLoginForm";
|
||||||
import { Link, Navigate } from "react-router-dom";
|
import InternalLoginForm from "./InternalLoginForm";
|
||||||
import Joi from "joi";
|
|
||||||
import authentication from "../services/authenticationService";
|
|
||||||
import { InputType } from "../../../components/common/Input";
|
|
||||||
import { ButtonType } from "../../../components/common/Button";
|
|
||||||
import { useForm } from "../../../components/common/useForm";
|
|
||||||
import {
|
|
||||||
renderButton,
|
|
||||||
renderError,
|
|
||||||
renderInput,
|
|
||||||
} from "../../../components/common/formHelpers";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Namespaces } from "../../../i18n/i18n";
|
|
||||||
|
|
||||||
const LoginForm: React.FC = () => {
|
const LoginForm: React.FC = () => {
|
||||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
if (window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN) {
|
||||||
const [isInNextStage, setIsInNextStage] = useState(false);
|
return (
|
||||||
const [emailSent, setEmailSent] = useState(false);
|
|
||||||
const passwordMaxLength = 255;
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
loaded: true,
|
|
||||||
data: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
tfaNeeded: false,
|
|
||||||
requestTfaRemoval: false,
|
|
||||||
securityCode: "",
|
|
||||||
},
|
|
||||||
errors: {},
|
|
||||||
redirect: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const schema = {
|
|
||||||
username: Joi.string()
|
|
||||||
.required()
|
|
||||||
.email({ tlds: { allow: false } })
|
|
||||||
.label(t("Email")),
|
|
||||||
password: Joi.string().required().label(t("Password")),
|
|
||||||
tfaNeeded: Joi.boolean().required(),
|
|
||||||
requestTfaRemoval: Joi.boolean().required(),
|
|
||||||
securityCode: Joi.string().allow("").label(t("Authenticate")),
|
|
||||||
};
|
|
||||||
|
|
||||||
form.schema = schema;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.location.replace("/login");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const performLogin = useCallback(
|
|
||||||
async (data: any) => {
|
|
||||||
try {
|
|
||||||
const result = await authentication.login(
|
|
||||||
data.username,
|
|
||||||
data.password,
|
|
||||||
data.securityCode,
|
|
||||||
data.requestTfaRemoval,
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case 1: {
|
|
||||||
const nextData = { ...form.state.data };
|
|
||||||
|
|
||||||
if (!nextData.tfaNeeded) {
|
|
||||||
nextData.tfaNeeded = true;
|
|
||||||
form.setState({ data: nextData });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2:
|
|
||||||
window.location.href = "/";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (ex: any) {
|
|
||||||
form.handleGeneralError(ex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[form],
|
|
||||||
);
|
|
||||||
|
|
||||||
const doSubmit = async () => {
|
|
||||||
const { data } = form.state;
|
|
||||||
await performLogin(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
form.handleSubmit(e, async () => doSubmit());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextClick = async () => {
|
|
||||||
const data = { ...form.state.data };
|
|
||||||
const validationResult = schema.username.validate(data.username);
|
|
||||||
if (validationResult.error === undefined) {
|
|
||||||
setIsInNextStage(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleForgetPassword = async () => {
|
|
||||||
try {
|
|
||||||
await authentication.forgotPassword(form.state.data.username as string);
|
|
||||||
setEmailSent(true);
|
|
||||||
const nextData = { ...form.state.data, username: "", password: "" };
|
|
||||||
form.setState({ data: nextData });
|
|
||||||
} catch (ex: any) {
|
|
||||||
form.handleGeneralError(ex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const authenticationWorkAround = async () => {
|
|
||||||
const data = { ...form.state.data, requestTfaRemoval: true };
|
|
||||||
await performLogin(data);
|
|
||||||
form.setState({ data });
|
|
||||||
};
|
|
||||||
|
|
||||||
const { tfaNeeded, requestTfaRemoval } = form.state.data as any;
|
|
||||||
const result = schema.username.validate(form.state.data.username);
|
|
||||||
const validEmail = result.error === undefined;
|
|
||||||
|
|
||||||
if (authentication.getCurrentUser()) return <Navigate to="/" />;
|
|
||||||
|
|
||||||
const requestTfaRemovalPanel = (
|
|
||||||
<div>
|
|
||||||
An email has been sent to you so that you can regain control of your
|
|
||||||
account.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const loginPanel = (
|
|
||||||
<>
|
<>
|
||||||
<form onSubmit={handleSubmit}>
|
<ExternalLoginForm url="/account/login" />
|
||||||
{renderInput(
|
|
||||||
"username",
|
|
||||||
"",
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.text,
|
|
||||||
isInNextStage,
|
|
||||||
"",
|
|
||||||
t("Email") as string,
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
"username",
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
{renderInput(
|
|
||||||
"password",
|
|
||||||
"",
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.password,
|
|
||||||
emailSent,
|
|
||||||
"",
|
|
||||||
t("Password") as string,
|
|
||||||
passwordMaxLength,
|
|
||||||
isInNextStage,
|
|
||||||
"current-password",
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
{!isInNextStage &&
|
|
||||||
renderButton(
|
|
||||||
t("Next"),
|
|
||||||
form.state.errors,
|
|
||||||
"next",
|
|
||||||
handleNextClick,
|
|
||||||
"login",
|
|
||||||
validEmail,
|
|
||||||
ButtonType.primary,
|
|
||||||
true,
|
|
||||||
)}
|
|
||||||
{isInNextStage && (
|
|
||||||
<div className="clickables">
|
|
||||||
{renderButton(
|
|
||||||
"Login",
|
|
||||||
form.state.errors,
|
|
||||||
"login",
|
|
||||||
undefined,
|
|
||||||
"login",
|
|
||||||
!emailSent,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
{isInNextStage && (
|
|
||||||
<div className="forgottenLink">
|
|
||||||
{renderButton(
|
|
||||||
t("ForgottenPassword") as string,
|
|
||||||
form.state.errors,
|
|
||||||
"forgot-password",
|
|
||||||
handleForgetPassword,
|
|
||||||
"forgot-password",
|
|
||||||
validEmail,
|
|
||||||
ButtonType.secondary,
|
|
||||||
true,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{emailSent && (
|
|
||||||
<div className="alert alert-info emailSent">
|
|
||||||
If you have a registered account, you will receive an email.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{renderError("_general", form.state.errors)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
const tfaPanel = (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
{renderError("_general", form.state.errors)}
|
|
||||||
{renderInput(
|
|
||||||
"securityCode",
|
|
||||||
t("Authenticate") as string,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.text,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
{renderButton(t("Authenticate"), form.state.errors, "authenticate")}
|
|
||||||
<Link to="#" onClick={authenticationWorkAround}>
|
|
||||||
My Authenticator is not working
|
|
||||||
</Link>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{requestTfaRemoval
|
<InternalLoginForm />
|
||||||
? requestTfaRemovalPanel
|
</>
|
||||||
: tfaNeeded
|
|
||||||
? tfaPanel
|
|
||||||
: loginPanel}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,352 +1,12 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import ExternalProfile from "./components/ExternalProfile";
|
||||||
import Joi from "joi";
|
import InternalProfile from "./components/InternalProfile";
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Namespaces } from "../../i18n/i18n";
|
|
||||||
import { useForm } from "../../components/common/useForm";
|
|
||||||
import profileService from "./services/profileService";
|
|
||||||
import { InputType } from "../../components/common/Input";
|
|
||||||
import { TwoFactorAuthenticationSettings } from "./models/TwoFactorAuthenticationSettings";
|
|
||||||
import Loading from "../../components/common/Loading";
|
|
||||||
import {
|
|
||||||
renderError,
|
|
||||||
renderButton,
|
|
||||||
renderInput,
|
|
||||||
renderToggle,
|
|
||||||
renderDropSection,
|
|
||||||
} from "../../components/common/formHelpers";
|
|
||||||
|
|
||||||
const Profile: React.FC = () => {
|
const Profile: React.FC = () => {
|
||||||
const { t } = useTranslation<typeof Namespaces.Common>();
|
if (window.__RUNTIME_CONFIG__.EXTERNAL_LOGIN) {
|
||||||
|
return <ExternalProfile url="/account/profile" />;
|
||||||
const labelFirstName = t("FirstName");
|
|
||||||
const labelMiddleNames = t("MiddleNames");
|
|
||||||
const labelLastName = t("LastName");
|
|
||||||
const labelEmail = t("Email");
|
|
||||||
const labelNewPassword = t("NewPassword");
|
|
||||||
const labelConfirmPassword = t("ConfirmPassword");
|
|
||||||
const labelUsingTwoFactorAuthentication = t("TwoFactorAuthentication");
|
|
||||||
const labelTfaCode = t("AuthenticationCode");
|
|
||||||
const labelApply = t("Save");
|
|
||||||
|
|
||||||
const [twoFactorAuthenticationSettings, setTwoFactorAuthenticationSettings] =
|
|
||||||
useState<TwoFactorAuthenticationSettings>({
|
|
||||||
manualEntrySetupCode: "",
|
|
||||||
qrCodeImageUrl: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
loaded: false,
|
|
||||||
data: {
|
|
||||||
firstName: "",
|
|
||||||
middleNames: "",
|
|
||||||
lastName: "",
|
|
||||||
email: "",
|
|
||||||
newPassword: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
originalUsingTwoFactorAuthentication: false,
|
|
||||||
usingTwoFactorAuthentication: false,
|
|
||||||
tfaCode: "",
|
|
||||||
},
|
|
||||||
errors: {},
|
|
||||||
redirect: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
form.schema = {
|
|
||||||
firstName: Joi.string().required().label(labelFirstName),
|
|
||||||
middleNames: Joi.string().allow("").required().label(labelMiddleNames),
|
|
||||||
lastName: Joi.string().required().label(labelLastName),
|
|
||||||
email: Joi.string()
|
|
||||||
.required()
|
|
||||||
.email({ tlds: { allow: false } })
|
|
||||||
.label(labelEmail),
|
|
||||||
newPassword: Joi.string().allow("").min(5).label(labelNewPassword),
|
|
||||||
confirmPassword: Joi.string()
|
|
||||||
.when("newPassword", {
|
|
||||||
is: "",
|
|
||||||
then: Joi.allow("").optional(),
|
|
||||||
otherwise: Joi.valid(Joi.ref("newPassword")).error(() => {
|
|
||||||
const e = new Error("Passwords must match");
|
|
||||||
e.name = "confirmPassword";
|
|
||||||
return e;
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.label(labelConfirmPassword),
|
|
||||||
originalUsingTwoFactorAuthentication: Joi.boolean().required(),
|
|
||||||
usingTwoFactorAuthentication: Joi.boolean()
|
|
||||||
.required()
|
|
||||||
.label(labelUsingTwoFactorAuthentication),
|
|
||||||
tfaCode: Joi.string()
|
|
||||||
.when("originalUsingTwoFactorAuthentication", {
|
|
||||||
is: Joi.ref("usingTwoFactorAuthentication"),
|
|
||||||
then: Joi.allow("").optional(),
|
|
||||||
otherwise: Joi.when("usingTwoFactorAuthentication", {
|
|
||||||
is: true,
|
|
||||||
then: Joi.string()
|
|
||||||
.length(6)
|
|
||||||
.required()
|
|
||||||
.error(() => {
|
|
||||||
const e = new Error(
|
|
||||||
"You must enter the code from the authenicator",
|
|
||||||
);
|
|
||||||
e.name = "tfaCode";
|
|
||||||
return e;
|
|
||||||
}),
|
|
||||||
otherwise: Joi.allow("").optional(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.label(labelTfaCode),
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadProfile = async () => {
|
|
||||||
try {
|
|
||||||
const profile = await profileService.getMyProfile();
|
|
||||||
if (profile) {
|
|
||||||
const {
|
|
||||||
firstName,
|
|
||||||
middleNames,
|
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
usingTwoFactorAuthentication,
|
|
||||||
twoFactorAuthenticationSettings,
|
|
||||||
} = profile;
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
firstName,
|
|
||||||
middleNames,
|
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
newPassword: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
originalUsingTwoFactorAuthentication: usingTwoFactorAuthentication,
|
|
||||||
usingTwoFactorAuthentication,
|
|
||||||
tfaCode: "",
|
|
||||||
};
|
|
||||||
form.setState({ loaded: true, data });
|
|
||||||
setTwoFactorAuthenticationSettings(twoFactorAuthenticationSettings);
|
|
||||||
}
|
}
|
||||||
} catch (ex: any) {
|
|
||||||
form.handleGeneralError(ex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
return <InternalProfile />;
|
||||||
void loadProfile();
|
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
const doSubmit = async (buttonName: string) => {
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
firstName,
|
|
||||||
middleNames,
|
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
usingTwoFactorAuthentication,
|
|
||||||
tfaCode,
|
|
||||||
newPassword,
|
|
||||||
confirmPassword,
|
|
||||||
} = form.state.data;
|
|
||||||
|
|
||||||
let password = "";
|
|
||||||
if (newPassword === confirmPassword) password = newPassword as string;
|
|
||||||
|
|
||||||
const response = await profileService.putMyProfile(
|
|
||||||
firstName as string,
|
|
||||||
middleNames as string,
|
|
||||||
lastName as string,
|
|
||||||
email as string,
|
|
||||||
usingTwoFactorAuthentication as boolean,
|
|
||||||
tfaCode as string,
|
|
||||||
password,
|
|
||||||
);
|
|
||||||
if (response) {
|
|
||||||
await loadProfile();
|
|
||||||
toast.info(t("YourProfileSettingsHaveBeenSaved"));
|
|
||||||
}
|
|
||||||
} catch (ex: any) {
|
|
||||||
form.handleGeneralError(ex);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
form.handleSubmit(e, doSubmit);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { loaded } = form.state;
|
|
||||||
const { usingTwoFactorAuthentication, newPassword } = form.state.data;
|
|
||||||
const passwordMaxLength = 255;
|
|
||||||
|
|
||||||
const tfaEnabled = usingTwoFactorAuthentication
|
|
||||||
? t("Enabled")
|
|
||||||
: t("Disabled");
|
|
||||||
|
|
||||||
let tfaImageBlock = null;
|
|
||||||
if (twoFactorAuthenticationSettings)
|
|
||||||
tfaImageBlock = (
|
|
||||||
<React.Fragment>
|
|
||||||
<label>{twoFactorAuthenticationSettings.manualEntrySetupCode}</label>
|
|
||||||
<img
|
|
||||||
src={twoFactorAuthenticationSettings.qrCodeImageUrl}
|
|
||||||
alt={twoFactorAuthenticationSettings.manualEntrySetupCode}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
const tfaSection = (
|
|
||||||
<div>
|
|
||||||
{renderToggle(
|
|
||||||
"usingTwoFactorAuthentication",
|
|
||||||
labelUsingTwoFactorAuthentication,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
form.handleToggleChange,
|
|
||||||
)}
|
|
||||||
{tfaImageBlock}
|
|
||||||
{renderInput(
|
|
||||||
"tfaCode",
|
|
||||||
labelTfaCode,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.text,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
let passwordSection = (
|
|
||||||
<React.Fragment>
|
|
||||||
{renderInput(
|
|
||||||
"newPassword",
|
|
||||||
labelNewPassword,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.password,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newPassword !== "")
|
|
||||||
passwordSection = (
|
|
||||||
<React.Fragment>
|
|
||||||
{renderInput(
|
|
||||||
"newPassword",
|
|
||||||
labelNewPassword,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.password,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
passwordMaxLength,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
{renderInput(
|
|
||||||
"confirmPassword",
|
|
||||||
labelConfirmPassword,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.password,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
passwordMaxLength,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Loading loaded={loaded}>
|
|
||||||
<h1>{t("Profile")}</h1>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
{renderError("_general", form.state.errors)}
|
|
||||||
{renderInput(
|
|
||||||
"email",
|
|
||||||
labelEmail,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.text,
|
|
||||||
true,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
{renderInput(
|
|
||||||
"firstName",
|
|
||||||
labelFirstName,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.text,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
{renderInput(
|
|
||||||
"middleNames",
|
|
||||||
labelMiddleNames,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.text,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
{renderInput(
|
|
||||||
"lastName",
|
|
||||||
labelLastName,
|
|
||||||
form.state.data,
|
|
||||||
form.state.errors,
|
|
||||||
InputType.text,
|
|
||||||
false,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
0,
|
|
||||||
true,
|
|
||||||
undefined,
|
|
||||||
form.handleChange,
|
|
||||||
)}
|
|
||||||
|
|
||||||
{passwordSection}
|
|
||||||
|
|
||||||
{renderDropSection(
|
|
||||||
"turnOnTfa",
|
|
||||||
<label>Two Factor Authentication {tfaEnabled}</label>,
|
|
||||||
tfaSection,
|
|
||||||
form.state.errors,
|
|
||||||
)}
|
|
||||||
<br />
|
|
||||||
{renderButton(labelApply, form.state.errors, "apply")}
|
|
||||||
</form>
|
|
||||||
</Loading>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Profile;
|
export default Profile;
|
||||||
|
|||||||
21
src/modules/profile/components/ExternalProfile.tsx
Normal file
21
src/modules/profile/components/ExternalProfile.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import HtmlIsland from "../../../components/common/HtmlIsland";
|
||||||
|
import { Namespaces } from "../../../i18n/i18n";
|
||||||
|
|
||||||
|
export interface ExternalProfileProps {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExternalProfile: React.FC<ExternalProfileProps> = ({ url }) => {
|
||||||
|
const { t } = useTranslation(Namespaces.Common);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HtmlIsland
|
||||||
|
url={url}
|
||||||
|
islandId="profile"
|
||||||
|
successMessage={t("ProfileSaved")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExternalProfile;
|
||||||
352
src/modules/profile/components/InternalProfile.tsx
Normal file
352
src/modules/profile/components/InternalProfile.tsx
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Joi from "joi";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Namespaces } from "../../../i18n/i18n";
|
||||||
|
import { useForm } from "../../../components/common/useForm";
|
||||||
|
import profileService from "../services/profileService";
|
||||||
|
import { InputType } from "../../../components/common/Input";
|
||||||
|
import { TwoFactorAuthenticationSettings } from "../models/TwoFactorAuthenticationSettings";
|
||||||
|
import Loading from "../../../components/common/Loading";
|
||||||
|
import {
|
||||||
|
renderError,
|
||||||
|
renderButton,
|
||||||
|
renderInput,
|
||||||
|
renderToggle,
|
||||||
|
renderDropSection,
|
||||||
|
} from "../../../components/common/formHelpers";
|
||||||
|
|
||||||
|
const InternalProfile: React.FC = () => {
|
||||||
|
const { t } = useTranslation<typeof Namespaces.Common>();
|
||||||
|
//Internal login version
|
||||||
|
const labelFirstName = t("FirstName");
|
||||||
|
const labelMiddleNames = t("MiddleNames");
|
||||||
|
const labelLastName = t("LastName");
|
||||||
|
const labelEmail = t("Email");
|
||||||
|
const labelNewPassword = t("NewPassword");
|
||||||
|
const labelConfirmPassword = t("ConfirmPassword");
|
||||||
|
const labelUsingTwoFactorAuthentication = t("TwoFactorAuthentication");
|
||||||
|
const labelTfaCode = t("AuthenticationCode");
|
||||||
|
const labelApply = t("Save");
|
||||||
|
|
||||||
|
const [twoFactorAuthenticationSettings, setTwoFactorAuthenticationSettings] =
|
||||||
|
useState<TwoFactorAuthenticationSettings>({
|
||||||
|
manualEntrySetupCode: "",
|
||||||
|
qrCodeImageUrl: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
loaded: false,
|
||||||
|
data: {
|
||||||
|
firstName: "",
|
||||||
|
middleNames: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
newPassword: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
originalUsingTwoFactorAuthentication: false,
|
||||||
|
usingTwoFactorAuthentication: false,
|
||||||
|
tfaCode: "",
|
||||||
|
},
|
||||||
|
errors: {},
|
||||||
|
redirect: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
form.schema = {
|
||||||
|
firstName: Joi.string().required().label(labelFirstName),
|
||||||
|
middleNames: Joi.string().allow("").required().label(labelMiddleNames),
|
||||||
|
lastName: Joi.string().required().label(labelLastName),
|
||||||
|
email: Joi.string()
|
||||||
|
.required()
|
||||||
|
.email({ tlds: { allow: false } })
|
||||||
|
.label(labelEmail),
|
||||||
|
newPassword: Joi.string().allow("").min(5).label(labelNewPassword),
|
||||||
|
confirmPassword: Joi.string()
|
||||||
|
.when("newPassword", {
|
||||||
|
is: "",
|
||||||
|
then: Joi.allow("").optional(),
|
||||||
|
otherwise: Joi.valid(Joi.ref("newPassword")).error(() => {
|
||||||
|
const e = new Error("Passwords must match");
|
||||||
|
e.name = "confirmPassword";
|
||||||
|
return e;
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.label(labelConfirmPassword),
|
||||||
|
originalUsingTwoFactorAuthentication: Joi.boolean().required(),
|
||||||
|
usingTwoFactorAuthentication: Joi.boolean()
|
||||||
|
.required()
|
||||||
|
.label(labelUsingTwoFactorAuthentication),
|
||||||
|
tfaCode: Joi.string()
|
||||||
|
.when("originalUsingTwoFactorAuthentication", {
|
||||||
|
is: Joi.ref("usingTwoFactorAuthentication"),
|
||||||
|
then: Joi.allow("").optional(),
|
||||||
|
otherwise: Joi.when("usingTwoFactorAuthentication", {
|
||||||
|
is: true,
|
||||||
|
then: Joi.string()
|
||||||
|
.length(6)
|
||||||
|
.required()
|
||||||
|
.error(() => {
|
||||||
|
const e = new Error(
|
||||||
|
"You must enter the code from the authenicator",
|
||||||
|
);
|
||||||
|
e.name = "tfaCode";
|
||||||
|
return e;
|
||||||
|
}),
|
||||||
|
otherwise: Joi.allow("").optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.label(labelTfaCode),
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProfile = async () => {
|
||||||
|
try {
|
||||||
|
const profile = await profileService.getMyProfile();
|
||||||
|
if (profile) {
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
middleNames,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
usingTwoFactorAuthentication,
|
||||||
|
twoFactorAuthenticationSettings,
|
||||||
|
} = profile;
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
firstName,
|
||||||
|
middleNames,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
newPassword: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
originalUsingTwoFactorAuthentication: usingTwoFactorAuthentication,
|
||||||
|
usingTwoFactorAuthentication,
|
||||||
|
tfaCode: "",
|
||||||
|
};
|
||||||
|
form.setState({ loaded: true, data });
|
||||||
|
setTwoFactorAuthenticationSettings(twoFactorAuthenticationSettings);
|
||||||
|
}
|
||||||
|
} catch (ex: any) {
|
||||||
|
form.handleGeneralError(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void loadProfile();
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const doSubmit = async (buttonName: string) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
middleNames,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
usingTwoFactorAuthentication,
|
||||||
|
tfaCode,
|
||||||
|
newPassword,
|
||||||
|
confirmPassword,
|
||||||
|
} = form.state.data;
|
||||||
|
|
||||||
|
let password = "";
|
||||||
|
if (newPassword === confirmPassword) password = newPassword as string;
|
||||||
|
|
||||||
|
const response = await profileService.putMyProfile(
|
||||||
|
firstName as string,
|
||||||
|
middleNames as string,
|
||||||
|
lastName as string,
|
||||||
|
email as string,
|
||||||
|
usingTwoFactorAuthentication as boolean,
|
||||||
|
tfaCode as string,
|
||||||
|
password,
|
||||||
|
);
|
||||||
|
if (response) {
|
||||||
|
await loadProfile();
|
||||||
|
toast.info(t("YourProfileSettingsHaveBeenSaved"));
|
||||||
|
}
|
||||||
|
} catch (ex: any) {
|
||||||
|
form.handleGeneralError(ex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
form.handleSubmit(e, doSubmit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { loaded } = form.state;
|
||||||
|
const { usingTwoFactorAuthentication, newPassword } = form.state.data;
|
||||||
|
const passwordMaxLength = 255;
|
||||||
|
|
||||||
|
const tfaEnabled = usingTwoFactorAuthentication
|
||||||
|
? t("Enabled")
|
||||||
|
: t("Disabled");
|
||||||
|
|
||||||
|
let tfaImageBlock = null;
|
||||||
|
if (twoFactorAuthenticationSettings)
|
||||||
|
tfaImageBlock = (
|
||||||
|
<React.Fragment>
|
||||||
|
<label>{twoFactorAuthenticationSettings.manualEntrySetupCode}</label>
|
||||||
|
<img
|
||||||
|
src={twoFactorAuthenticationSettings.qrCodeImageUrl}
|
||||||
|
alt={twoFactorAuthenticationSettings.manualEntrySetupCode}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
const tfaSection = (
|
||||||
|
<div>
|
||||||
|
{renderToggle(
|
||||||
|
"usingTwoFactorAuthentication",
|
||||||
|
labelUsingTwoFactorAuthentication,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
form.handleToggleChange,
|
||||||
|
)}
|
||||||
|
{tfaImageBlock}
|
||||||
|
{renderInput(
|
||||||
|
"tfaCode",
|
||||||
|
labelTfaCode,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.text,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
let passwordSection = (
|
||||||
|
<React.Fragment>
|
||||||
|
{renderInput(
|
||||||
|
"newPassword",
|
||||||
|
labelNewPassword,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.password,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newPassword !== "")
|
||||||
|
passwordSection = (
|
||||||
|
<React.Fragment>
|
||||||
|
{renderInput(
|
||||||
|
"newPassword",
|
||||||
|
labelNewPassword,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.password,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
passwordMaxLength,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
{renderInput(
|
||||||
|
"confirmPassword",
|
||||||
|
labelConfirmPassword,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.password,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
passwordMaxLength,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Loading loaded={loaded}>
|
||||||
|
<h1>{t("Profile")}</h1>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
{renderError("_general", form.state.errors)}
|
||||||
|
{renderInput(
|
||||||
|
"email",
|
||||||
|
labelEmail,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.text,
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
{renderInput(
|
||||||
|
"firstName",
|
||||||
|
labelFirstName,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.text,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
{renderInput(
|
||||||
|
"middleNames",
|
||||||
|
labelMiddleNames,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.text,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
{renderInput(
|
||||||
|
"lastName",
|
||||||
|
labelLastName,
|
||||||
|
form.state.data,
|
||||||
|
form.state.errors,
|
||||||
|
InputType.text,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
form.handleChange,
|
||||||
|
)}
|
||||||
|
|
||||||
|
{passwordSection}
|
||||||
|
|
||||||
|
{renderDropSection(
|
||||||
|
"turnOnTfa",
|
||||||
|
<label>Two Factor Authentication {tfaEnabled}</label>,
|
||||||
|
tfaSection,
|
||||||
|
form.state.errors,
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
|
{renderButton(labelApply, form.state.errors, "apply")}
|
||||||
|
</form>
|
||||||
|
</Loading>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InternalProfile;
|
||||||
Loading…
Reference in New Issue
Block a user