change email

master
alex 2024-02-11 23:38:40 +01:00
parent 5b62d4ff3f
commit d6016c3d4d
5 changed files with 273 additions and 38 deletions

View File

@ -33,6 +33,7 @@
"email": "E-Mail",
"emailPlaceholder": "Geben Sie Ihre E-Mail ein",
"emailPlaceholderThirdPerson": "Geben Sie die E-Mail ein",
"emailPlaceholderNew": "Geben Sie Ihre neue E-Mail ein",
"password": "Passwort",
"passwordPlaceholder": "Geben Sie Ihr Passwort ein",
"noDataFound": "Keine Einträge gefunden",
@ -313,6 +314,16 @@
"analytics": "Analytik",
"analyticsDescription": "Dazu gehören Informationen über die Nutzung unseres Dashboards, wie die besuchten Seiten, die Verweildauer auf den Seiten und die allgemeine Interaktion mit den Seiten. Dies hilft uns, unser Dashboard zu verbessern und es benutzerfreundlicher zu gestalten."
},
"changeEmailModal": {
"title": "E-Mail-Adresse ändern",
"info": "Nachdem Sie Ihre neue E-Mail-Adresse angegeben haben, erhalten Sie eine E-Mail mit einem Bestätigungslink. Bitte klicken Sie auf diesen Link, um Ihre neue E-Mail-Adresse zu bestätigen. Ihre alte E-Mail-Adresse bleibt so lange aktiv, bis die neue E-Mail-Adresse verifiziert ist. Nach der Verifizierung werden alle Ihre Sitzungen abgemeldet und Sie müssen sich erneut anmelden.",
"request": {
"200": {
"title": "Anfrage zur E-Mail-Änderung gesendet",
"description": "Wir haben einen Bestätigungslink an Ihre neue E-Mail-Adresse gesendet. Bitte klicken Sie auf den Link, um Ihre neue E-Mail-Adresse zu bestätigen."
}
}
},
"changePassword": {
"cardTitle": "Passwort ändern",
"currentPassword": "Aktuelles Passwort",
@ -378,6 +389,7 @@
"title": "Verifizierung fehlgeschlagen",
"description": "Keine ausstehende Überprüfung gefunden"
},
"buttonToLogin": "Jetzt anmelden",
"content": [
{
"requesting": {
@ -387,6 +399,18 @@
"title": "E-Mail-Überprüfung erfolgreich",
"button": "Jetzt anmelden"
}
},
{
"init": {
"title": "Bestätigen Sie die E-Mail Änderung",
"subTitle": "Bestätigen Sie Ihre neue E-Mail Adresse, indem Sie Ihr Passwort eingeben"
},
"requesting": {
"title": "Bestätigung der E-Mail Änderung"
},
"success": {
"title": "E-Mail-Änderung erfolgreich"
}
}
]
},

View File

@ -33,6 +33,7 @@
"email": "Email",
"emailPlaceholder": "Enter your email",
"emailPlaceholderThirdPerson": "Enter the email",
"emailPlaceholderNew": "Enter your new email",
"password": "Password",
"passwordPlaceholder": "Enter your password",
"noDataFound": "No data found",
@ -316,6 +317,16 @@
"analytics": "Analytics",
"analyticsDescription": "This includes information about the use of our dashboard, such as the visited pages, the length of stay on the pages and the general interaction with the pages. This helps us to improve our dashboard and make it more user-friendly."
},
"changeEmailModal": {
"title": "Change email",
"info": "After submitting your new email, you will receive an email with a verification link. Please click on the link to verify your new email address. Your old email address will remain active until the new email address is verified. After verification all your sessions will be logged out and you have to log in again.",
"request": {
"200": {
"title": "Email change request sent",
"description": "We have sent an verification link to your new email address. Please click on the link to verify your new email address."
}
}
},
"changePassword": {
"cardTitle": "Change password",
"currentPassword": "Current password",
@ -381,6 +392,7 @@
"title": "Verification failed",
"description": "No pending verification found."
},
"buttonToLogin": "Login now",
"content": [
{
"requesting": {
@ -390,6 +402,18 @@
"title": "Email verification successful",
"button": "Login now"
}
},
{
"init": {
"title": "Confirm the email change",
"subTitle": "Confirm your new email address by entering your password"
},
"requesting": {
"title": "Validating email change"
},
"success": {
"title": "Email change successful"
}
}
]
},

View File

@ -1,8 +1,9 @@
import { Form, Input, InputNumber, Skeleton } from "antd";
import { Button, Form, Input, InputNumber, Skeleton, Space } from "antd";
import { Constants, isEmailValid, myFetch } from "../../utils";
import { useRef } from "react";
import { createElement, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useAppContext } from "../../Contexts/AppContext";
import { EditOutlined } from "@ant-design/icons";
export function MyUsernameFormInput({
propsFormItem,
@ -76,6 +77,7 @@ export function MyEmailFormInput({
hasFeedback,
showSkeleton,
thirdPerson,
newEmail,
}) {
const { t } = useTranslation();
@ -94,6 +96,8 @@ export function MyEmailFormInput({
inputPlaceholder={
thirdPerson
? t("common.emailPlaceholderThirdPerson")
: newEmail
? t("common.emailPlaceholderNew")
: t("common.emailPlaceholder")
}
ruleMessageValueMinLengthRequired={t("common.inputRules.emailMinLength", {
@ -307,6 +311,7 @@ export function MyFormInput({
inputType,
inputNotRequired,
showSkeleton,
InputFatherElement,
}) {
const commonProps = {
...propsInput,
@ -359,6 +364,8 @@ export function MyFormInput({
),
};
const inputComponent = inputComponents[inputType] || inputComponents.default;
return (
<Form.Item
{...propsFormItem}
@ -367,7 +374,11 @@ export function MyFormInput({
required
rules={myFormItemRules}
>
{inputComponents[inputType] || inputComponents.default}
{InputFatherElement ? (
createElement(InputFatherElement, { children: inputComponent })
) : (
<>{inputComponent}</>
)}
</Form.Item>
);
}

View File

@ -3,6 +3,7 @@ import {
Card,
Col,
Form,
Input,
Popconfirm,
Radio,
Row,
@ -30,7 +31,7 @@ import {
RequestState,
RequestStateItem,
} from "../../Components/MyRequestStateItem";
import { LogoutOutlined } from "@ant-design/icons";
import { EditOutlined, LogoutOutlined } from "@ant-design/icons";
import {
MyEmailFormInput,
MyFormInput,
@ -205,8 +206,7 @@ function YourProfile({
if (
language === undefined ||
analyticsEnabled === undefined ||
username === undefined ||
email === undefined
username === undefined
)
return;
@ -246,10 +246,6 @@ function YourProfile({
body.username = username;
}
if (email !== requestData.email) {
body.email = email;
}
if (Object.keys(body).length === 0) {
setGlobalRequestState({
...globalRequestState,
@ -277,7 +273,6 @@ function YourProfile({
language: language,
analytics_enabled: analyticsEnabled,
username: username,
email: email,
});
sideBarContext.setUsername(username);
@ -298,7 +293,7 @@ function YourProfile({
});
});
}, 500);
}, [language, analyticsEnabled, username, email]);
}, [language, analyticsEnabled, username]);
return (
<Card title={t("userProfile.yourProfile.cardTitle")}>
@ -344,16 +339,107 @@ function YourProfile({
showSkeleton={requestState === RequestState.INIT}
/>
<MyEmailFormInput
<MyFormInput
formItemName="email"
label={t("common.email")}
propsInput={{
disabled: true,
value: email,
}}
showSkeleton={requestState === RequestState.INIT}
hasFeedback
disableEmailCheck={requestData.email === email}
InputFatherElement={(props) => (
<Space.Compact block>
{props.children}
<YourProfileChangeEmailModal />
</Space.Compact>
)}
/>
</Form>
</Card>
);
}
function YourProfileChangeEmailModal() {
const { t } = useTranslation();
const [form] = Form.useForm();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isRequesting, setIsRequesting] = useState(false);
const handleModalClose = () => setIsModalOpen(false);
useEffect(() => {
if (!isModalOpen) return;
form.resetFields();
}, [isModalOpen]);
return (
<>
{notificationContextHolder}
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => setIsModalOpen(true)}
/>
<MyModal
title={t("userProfile.changeEmailModal.title")}
isOpen={isModalOpen}
onCancel={handleModalClose}
footer={
<MyModalCloseConfirmButtonFooter
onCancel={handleModalClose}
isConfirmButtonLoading={isRequesting}
onConfirm={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(true);
myFetch({
url: "/user/profile/email",
method: "POST",
body: {
email: values.email,
},
})
.then(() => {
setIsRequesting(false);
setIsModalOpen(false);
notificationApi["info"]({
message: t(
"userProfile.changeEmailModal.request.200.title"
),
description: t(
"userProfile.changeEmailModal.request.200.description"
),
});
})
.catch(() => setIsRequesting(false));
})
.catch(() => setIsRequesting(false));
}}
/>
}
>
<Form form={form} layout="vertical" requiredMark={false}>
<Typography.Paragraph type="secondary">
{t("userProfile.changeEmailModal.info")}
</Typography.Paragraph>
<MyEmailFormInput hasFeedback newEmail />
</Form>
</MyModal>
</>
);
}
function ChangePassword({
notificationApi,
globalRequestState,

View File

@ -1,39 +1,106 @@
import { useParams } from "react-router-dom";
import MyModal from "../../Components/MyModal";
import { Button, ConfigProvider, Flex, Result, Spin, notification } from "antd";
import {
Button,
ConfigProvider,
Flex,
Form,
Result,
Spin,
notification,
} from "antd";
import { useEffect, useState } from "react";
import MyAppLogo from "../../Components/MyAppLogo";
import PageNotFound, { ButtonBackHome } from "../PageNotFound";
import { Constants, myFetch } from "../../utils";
import {
Constants,
EncodeStringToBase64,
myFetch,
showInputsInvalidNotification,
showPasswordIncorrectNotification,
} from "../../utils";
import { useTranslation } from "react-i18next";
import { RequestState } from "../../Components/MyRequestStateItem";
import { MyPasswordFormInput } from "../../Components/MyFormInputs";
export default function Verification() {
const { state, emailVerificationId } = useParams();
const { t } = useTranslation();
const [notificationApi, notificationContextHolder] =
notification.useNotification();
const [form] = Form.useForm();
const [isRequesting, setIsRequesting] = useState(RequestState.REQUESTING);
const [isRequesting, setIsRequesting] = useState(RequestState.INIT);
const ButtonToLogin = () => {
return (
<Button
type="primary"
onClick={() => {
window.location.href = `${Constants.DASHBOARD_ADDRESS}${Constants.ROUTE_PATHS.AUTHENTICATION.LOGIN}`;
}}
>
{t("verification.buttonToLogin")}
</Button>
);
};
const content = [
{
requesting: {
title: t("verification.content.0.requesting.title"),
extra: <Spin size="large" />,
},
sucess: {
success: {
title: t("verification.content.0.success.title"),
extra: <ButtonToLogin />,
},
},
{
init: {
title: t("verification.content.1.init.title"),
subTitle: t("verification.content.1.init.subTitle"),
extra: (
<Button
type="primary"
onClick={() => {
window.location.href = `${Constants.DASHBOARD_ADDRESS}${Constants.ROUTE_PATHS.AUTHENTICATION.LOGIN}`;
}}
>
{t("verification.content.0.success.button")}
</Button>
<Form form={form} layout="vertical" requiredMark={false}>
<MyPasswordFormInput />
<Button
type="primary"
htmlType="submit"
block
onClick={() => {
form
.validateFields()
.then((values) => {
setIsRequesting(RequestState.REQUESTING);
sendVerifyRequest({
password: EncodeStringToBase64(values.password),
})
.then(() => setIsRequesting(RequestState.SUCCESS))
.catch(() => {
setIsRequesting(RequestState.INIT);
showPasswordIncorrectNotification(notificationApi, t);
});
})
.catch(() =>
showInputsInvalidNotification(notificationApi, t)
);
}}
>
{t("common.button.confirm")}
</Button>
</Form>
),
},
requesting: {
title: t("verification.content.1.requesting.title"),
extra: <Spin size="large" />,
},
success: {
title: t("verification.content.1.success.title"),
extra: <ButtonToLogin />,
},
},
];
@ -47,19 +114,31 @@ export default function Verification() {
state > content.length - 1
) {
elementContent = <PageNotFound useHref />;
} else if (isRequesting === RequestState.INIT) {
elementContent = (
<Result
status="info"
title={content[parsedState].init?.title}
subTitle={content[parsedState].init?.subTitle}
extra={content[parsedState].init?.extra}
/>
);
} else if (isRequesting === RequestState.REQUESTING) {
elementContent = (
<Result
title={content[parsedState].requesting.title}
extra={<Spin size="large" />}
status="info"
title={content[parsedState].requesting?.title}
subTitle={content[parsedState].requesting?.subTitle}
extra={content[parsedState].requesting?.extra}
/>
);
} else if (isRequesting === RequestState.SUCCESS) {
elementContent = (
<Result
status="success"
title={content[parsedState].sucess.title}
extra={content[parsedState].sucess.extra}
title={content[parsedState].success?.title}
subTitle={content[parsedState].success?.subTitle}
extra={content[parsedState].success?.extra}
/>
);
} else if (isRequesting === RequestState.FAILED) {
@ -75,18 +154,29 @@ export default function Verification() {
elementContent = <PageNotFound useHref />;
}
const sendVerifyRequest = (body) => {
return myFetch({
method: "POST",
url: `/user/verify/${state}/${emailVerificationId}`,
notificationApi: notificationApi,
body: body,
t: t,
});
};
useEffect(() => {
if (emailVerificationId === undefined || state === undefined) {
setIsRequesting(false);
setIsRequesting(RequestState.FAILED);
return;
}
myFetch({
url: `/user/verify/${state}/${emailVerificationId}`,
notificationApi: notificationApi,
t: t,
})
.then(() => setIsRequesting(RequestState.SUCCESS))
sendVerifyRequest()
.then((res) => {
// if an action by the user is required, do not change the state (e.g. for providing a new password)
if (res.status !== undefined && res.status === "actionRequired") return;
setIsRequesting(RequestState.SUCCESS);
})
.catch(() => setIsRequesting(RequestState.FAILED));
}, []);