Login(nextjs): add part of LoginForm

This commit is contained in:
Timofey Boyko 2024-04-01 09:56:54 +03:00
parent 8b4b44d50d
commit 62ea894caa
12 changed files with 939 additions and 44 deletions

View File

@ -29,3 +29,7 @@ declare module "*.ico?url" {
export default content;
}
declare module "*.svg?url" {
const content: string;
export default content;
}

View File

@ -15,6 +15,7 @@
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-google-recaptcha": "^3.1.0",
"react-i18next": "^13.2.1",
"sass": "^1.59.3",
"styled-components": "^5.3.9"
@ -24,6 +25,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-google-recaptcha": "^2.1.9",
"babel-plugin-styled-components": "^2.1.4",
"eslint": "^8",
"eslint-config-next": "14.0.4",

View File

@ -27,6 +27,7 @@
"use client";
import { useContext, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
import { TenantStatus } from "@docspace/shared/enums";
@ -37,17 +38,23 @@ import {
getLogoFromPath,
getOAuthToken,
} from "@docspace/shared/utils/common";
import { checkIsSSR } from "@docspace/shared/utils/device";
import RecoverAccessModalDialog from "@docspace/shared/components/recover-access-modal-dialog/RecoverAccessModalDialog";
import { Scrollbar } from "@docspace/shared/components/scrollbar";
import { ColorTheme, ThemeId } from "@docspace/shared/components/color-theme";
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
import { Link, LinkType } from "@docspace/shared/components/link";
import { SocialButtonsGroup } from "@docspace/shared/components/social-buttons-group";
import { Text } from "@docspace/shared/components/text";
import SSOIcon from "PUBLIC_DIR/images/sso.react.svg?url";
import { DataContext } from "@/providers/DataProvider";
import { LoginProps } from "@/types";
import useRecoverDialog from "@/hooks/useRecoverDialog";
import GreetingContainer from "../GreetingContainer";
import Register from "../Register";
import { LoginContent, LoginFormWrapper } from "./Login.styled";
@ -63,7 +70,7 @@ const Login = ({ searchParams }: LoginProps) => {
});
const theme = useTheme();
const { t } = useTranslation(["Login"]);
const { settings, capabilities, thirdPartyProvider, whiteLabel } =
useContext(DataContext);
const {
@ -96,7 +103,7 @@ const Login = ({ searchParams }: LoginProps) => {
};
const onSocialButtonClick = useCallback(
async (e: React.MouseEvent<HTMLButtonElement | HTMLElement>) => {
async (e: React.MouseEvent<Element, MouseEvent>) => {
const target = e.target as HTMLElement;
let targetElement = target;
@ -144,14 +151,21 @@ const Login = ({ searchParams }: LoginProps) => {
);
const bgPattern = getBgPattern(theme.currentColorScheme?.id);
const isRegisterContainerVisible = !checkIsSSR() && settings.enabledJoin;
const logo = whiteLabel && Object.values(whiteLabel)[1];
const logoUrl = !logo
? undefined
: !theme?.isBase
? getLogoFromPath(logo.path.dark)
: getLogoFromPath(logo.path.light);
? (getLogoFromPath(logo.path.dark) as string)
: (getLogoFromPath(logo.path.light) as string);
const ssoProps = ssoExists()
? {
ssoUrl: capabilities?.ssoUrl,
ssoLabel: capabilities?.ssoLabel,
ssoSVG: SSOIcon as string,
}
: {};
return (
<LoginFormWrapper id="login-page" bgPattern={bgPattern}>
@ -160,7 +174,7 @@ const Login = ({ searchParams }: LoginProps) => {
<LoginContent>
<ColorTheme
themeId={ThemeId.LinkForgotPassword}
isRegisterContainerVisible={isRegisterContainerVisible}
isRegisterContainerVisible={settings.enabledJoin}
>
<GreetingContainer
roomName={invitationLinkData.roomName}
@ -170,16 +184,44 @@ const Login = ({ searchParams }: LoginProps) => {
greetingSettings={settings.greetingSettings}
type={invitationLinkData.type}
/>
<FormWrapper id="login-form">asd</FormWrapper>
<FormWrapper id="login-form">
{(oauthDataExists() || ssoExists()) && (
<>
<div className="line">
<Text className="or-label">
{t("Common:orContinueWith")}
</Text>
</div>
<SocialButtonsGroup
providers={thirdPartyProvider}
onClick={onSocialButtonClick}
t={t}
isDisabled={isLoading}
{...ssoProps}
/>
</>
)}
{settings.enableAdmMess && (
<Link
fontWeight={600}
fontSize="13px"
type={LinkType.action}
isHovered
className="login-link recover-link"
onClick={openRecoverDialog}
>
{t("RecoverAccess")}
</Link>
)}
</FormWrapper>
</ColorTheme>
</LoginContent>
{isRegisterContainerVisible && (
{settings.enabledJoin && (
<Register
id="login_register"
enabledJoin={enabledJoin}
currentColorScheme={currentColorScheme}
trustedDomains={portalSettings?.trustedDomains}
trustedDomainsType={portalSettings?.trustedDomainsType}
enabledJoin
trustedDomains={settings.trustedDomains}
trustedDomainsType={settings.trustedDomainsType}
/>
)}
</Scrollbar>

View File

@ -0,0 +1,51 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled, { css } from "styled-components";
export const StyledCaptcha = styled.div<{ isCaptchaError: boolean }>`
margin: 24px 0;
width: fit-content;
.captcha-wrapper {
${(props) =>
props.isCaptchaError &&
css`
border: ${props.theme.login.captcha.border};
padding: 4px 4px 4px 2px;
`};
margin-bottom: 2px;
}
${(props) =>
props.isCaptchaError &&
css`
p {
color: ${props.theme.login.captcha.color};
}
`}
`;

View File

@ -0,0 +1,434 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import ReCAPTCHA from "react-google-recaptcha";
import { isMobileOnly } from "react-device-detect";
import { useTheme } from "styled-components";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { PasswordInput } from "@docspace/shared/components/password-input";
import { Checkbox } from "@docspace/shared/components/checkbox";
import { HelpButton } from "@docspace/shared/components/help-button";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkType } from "@docspace/shared/components/link";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { createPasswordHash } from "@docspace/shared/utils/common";
import { checkIsSSR } from "@docspace/shared/utils";
import { checkPwd } from "@docspace/shared/utils/desktop";
import { login } from "@docspace/shared/utils/loginUtils";
import { toastr } from "@docspace/shared/components/toast";
import { thirdPartyLogin } from "@docspace/shared/api/user";
import { setWithCredentialsStatus } from "@docspace/shared/api/client";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types";
import { LoginFormProps } from "@/types";
import EmailContainer from "./sub-components/EmailContainer";
import ForgotPasswordModalDialog from "./sub-components/ForgotPasswordModalDialog";
import { StyledCaptcha } from "./LoginForm.styled";
const settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false,
};
const LoginForm = ({
isLoading,
hashSettings,
isDesktop,
match,
setIsLoading,
cookieSettingsEnabled,
recaptchaPublicKey,
emailFromInvitation,
}: LoginFormProps) => {
const [isEmailErrorShow, setIsEmailErrorShow] = useState(false);
const [errorText, setErrorText] = useState("");
const [identifier, setIdentifier] = useState(emailFromInvitation ?? "");
const [passwordValid, setPasswordValid] = useState(true);
const [identifierValid, setIdentifierValid] = useState(true);
const [password, setPassword] = useState("");
const [isDisabled, setIsDisabled] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const [isDialogVisible, setIsDialogVisible] = useState(false);
const [isCaptcha, setIsCaptcha] = useState(false);
const [isCaptchaSuccessful, setIsCaptchaSuccess] = useState(false);
const [isCaptchaError, setIsCaptchaError] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const captchaRef = useRef<ReCAPTCHA>(null);
const { t, ready } = useTranslation(["Login", "Common"]);
const theme = useTheme();
const { message, confirmedEmail, authError } = match || {
message: "",
confirmedEmail: "",
authError: "",
};
const authCallback = (profile: string) => {
localStorage.removeItem("profile");
localStorage.removeItem("code");
thirdPartyLogin(profile)
.then((response) => {
if (!(response || response.token || response.confirmUrl))
throw new Error("Empty API response");
setWithCredentialsStatus(true);
if (response.confirmUrl) {
return window.location.replace(response.confirmUrl);
}
const redirectPath = sessionStorage.getItem("referenceUrl");
if (redirectPath) {
sessionStorage.removeItem("referenceUrl");
window.location.href = redirectPath;
} else {
window.location.replace("/");
}
})
.catch(() => {
toastr.error(
t("Common:ProviderNotConnected"),
t("Common:ProviderLoginError"),
);
});
};
useEffect(() => {
const profile = localStorage.getItem("profile");
if (!profile) return;
authCallback(profile);
}, []);
useEffect(() => {
message && setErrorText(message);
confirmedEmail && setIdentifier(confirmedEmail);
const messageEmailConfirmed = t("MessageEmailConfirmed");
const messageAuthorize = t("MessageAuthorize");
const text = `${messageEmailConfirmed} ${messageAuthorize}`;
confirmedEmail && ready && toastr.success(text);
authError && ready && toastr.error(t("Common:ProviderLoginError"));
focusInput();
window.authCallback = authCallback;
}, [message, confirmedEmail]);
const onChangeLogin = (e: React.ChangeEvent<HTMLInputElement>) => {
//console.log("onChangeLogin", e.target.value);
setIdentifier(e.target.value);
if (!IS_ROOMS_MODE) setIsEmailErrorShow(false);
onClearErrors();
};
const onClearErrors = () => {
if (IS_ROOMS_MODE) {
!identifierValid && setIdentifierValid(true);
errorText && setErrorText("");
setIsEmailErrorShow(false);
} else {
!passwordValid && setPasswordValid(true);
}
};
const onSubmit = () => {
//errorText && setErrorText("");
let captchaToken = "";
if (recaptchaPublicKey && isCaptcha) {
if (!isCaptchaSuccessful) {
setIsCaptchaError(true);
return;
}
captchaToken = captchaRef.current.getValue();
}
let hasError = false;
const user = identifier.trim();
if (!user) {
hasError = true;
setIdentifierValid(false);
setIsEmailErrorShow(true);
}
if (IS_ROOMS_MODE && identifierValid) {
window.location.replace("/login/code"); //TODO: confirm link?
return;
}
const pass = password.trim();
if (!pass) {
hasError = true;
setPasswordValid(false);
}
if (!identifierValid) hasError = true;
if (hasError) return;
setIsLoading(true);
const hash = createPasswordHash(pass, hashSettings);
isDesktop && checkPwd();
const session = !isChecked;
login(user, hash, session, captchaToken)
.then((res: string | object) => {
const isConfirm = typeof res === "string" && res.includes("confirm");
const redirectPath = sessionStorage.getItem("referenceUrl");
if (redirectPath && !isConfirm) {
sessionStorage.removeItem("referenceUrl");
window.location.href = redirectPath;
return;
}
if (typeof res === "string") window.location.replace(res);
else window.location.replace("/"); //TODO: save { user, hash } for tfa
})
.catch((error) => {
let errorMessage = "";
if (typeof error === "object") {
errorMessage =
error?.response?.data?.error?.message ||
error?.statusText ||
error?.message ||
"";
} else {
errorMessage = error;
}
if (recaptchaPublicKey && error?.response?.status === 403) {
setIsCaptcha(true);
}
if (isCaptcha && captchaRef.current) {
captchaRef.current.reset();
}
setIsEmailErrorShow(true);
setErrorText(errorMessage);
setPasswordValid(!errorMessage);
setIsLoading(false);
focusInput();
});
};
const onBlurEmail = () => {
!identifierValid && setIsEmailErrorShow(true);
};
const onValidateEmail = (res: TValidate) => {
setIdentifierValid(res.isValid);
setErrorText(res.errors?.[0] ?? "");
return undefined;
};
const focusInput = () => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
};
const onChangePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
onClearErrors();
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
onClearErrors();
!isDisabled && onSubmit();
e.preventDefault();
}
};
const onChangeCheckbox = () => setIsChecked(!isChecked);
const onClick = () => {
setIsDialogVisible(true);
setIsDisabled(true);
};
const onDialogClose = () => {
setIsDialogVisible(false);
setIsDisabled(false);
setIsLoading(false);
};
const onSuccessfullyComplete = () => {
setIsCaptchaSuccess(true);
};
const errorMessage = () => {
if (!password.trim()) {
return t("Common:RequiredField");
}
if (emailFromInvitation) {
return errorText ? t(`Common:${errorText}`) : t("Common:RequiredField");
}
};
const passwordErrorMessage = errorMessage();
return (
<form className="auth-form-container">
<EmailContainer
emailFromInvitation={emailFromInvitation}
isEmailErrorShow={isEmailErrorShow}
errorText={errorText}
identifier={identifier}
isLoading={isLoading}
onChangeLogin={onChangeLogin}
onBlurEmail={onBlurEmail}
onValidateEmail={onValidateEmail}
/>
<FieldContainer
isVertical
labelVisible={false}
hasError={!passwordValid}
errorMessage={passwordErrorMessage} //TODO: Add wrong password server error
>
<PasswordInput
className="password-input"
simpleView
passwordSettings={settings}
id="login_password"
inputName="password"
placeholder={t("Common:Password")}
hasError={!passwordValid}
inputValue={password}
size={InputSize.large}
scale
tabIndex={1}
isDisabled={isLoading}
autoComplete="current-password"
onChange={onChangePassword}
isAutoFocussed={!!emailFromInvitation}
inputType={InputType.password}
/>
</FieldContainer>
<div className="login-forgot-wrapper">
<div className="login-checkbox-wrapper">
<div className="remember-wrapper">
{!cookieSettingsEnabled && (
<Checkbox
id="login_remember"
className="login-checkbox"
isChecked={isChecked}
onChange={onChangeCheckbox}
label={t("Common:Remember")}
helpButton={
<HelpButton
id="login_remember-hint"
className="help-button"
offsetRight={0}
tooltipContent={
<Text fontSize="12px">{t("RememberHelper")}</Text>
}
tooltipMaxWidth={isMobileOnly ? "240px" : "340px"}
/>
}
/>
)}
</div>
<Link
fontSize="13px"
className="login-link"
type={LinkType.page}
isHovered={false}
onClick={onClick}
id="login_forgot-password-link"
>
{t("ForgotPassword")}
</Link>
</div>
</div>
{isDialogVisible && (
<ForgotPasswordModalDialog
isVisible={isDialogVisible}
userEmail={identifier}
onDialogClose={onDialogClose}
/>
)}
{recaptchaPublicKey && isCaptcha && (
<StyledCaptcha isCaptchaError={isCaptchaError}>
<div className="captcha-wrapper">
<ReCAPTCHA
sitekey={recaptchaPublicKey}
ref={captchaRef}
theme={theme.isBase ? "light" : "dark"}
onChange={onSuccessfullyComplete}
/>
</div>
{isCaptchaError && (
<Text>{t("Errors:LoginWithBruteForceCaptcha")}</Text>
)}
</StyledCaptcha>
)}
<Button
id="login_submit"
className="login-button"
primary
size={ButtonSize.medium}
scale
label={
isLoading ? t("Common:LoadingProcessing") : t("Common:LoginButton")
}
tabIndex={1}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</form>
);
};
export default LoginForm;

View File

@ -0,0 +1,139 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { Trans, useTranslation } from "react-i18next";
import { EmailInput } from "@docspace/shared/components/email-input";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkType } from "@docspace/shared/components/link";
import { IconButton } from "@docspace/shared/components/icon-button";
import ArrowIcon from "PUBLIC_DIR/images/arrow.left.react.svg?url";
import { DEFAULT_EMAIL_TEXT } from "@/utils/constants";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types";
interface IEmailContainer {
emailFromInvitation?: string;
isEmailErrorShow: boolean;
errorText?: string;
identifier: string;
isLoading: boolean;
onChangeLogin: (e: React.ChangeEvent<HTMLInputElement>) => void;
onBlurEmail: () => void;
onValidateEmail: (res: TValidate) => undefined;
}
const EmailContainer = ({
emailFromInvitation,
isEmailErrorShow,
errorText,
identifier,
isLoading,
onChangeLogin,
onBlurEmail,
onValidateEmail,
}: IEmailContainer) => {
const { t } = useTranslation();
if (emailFromInvitation) {
const onClickBack = () => {
history.go(-1);
};
return (
<div className="invitation-info-container">
<div className="sign-in-container">
<div className="back-title">
<IconButton size={16} iconName={ArrowIcon} onClick={onClickBack} />
<Text fontWeight={600} onClick={onClickBack}>
{t("Common:Back")}
</Text>
</div>
<Text fontWeight={600} fontSize={"16px"}>
{t("Common:LoginButton")}
</Text>
</div>
<Text>
<Trans
t={t}
i18nKey="UserIsAlreadyRegistered"
ns="Login"
defaults={DEFAULT_EMAIL_TEXT}
values={{
email: emailFromInvitation,
}}
components={{
1: (
<Link
fontWeight={600}
className="login-link"
type={LinkType.page}
isHovered={false}
/>
),
}}
/>
</Text>
</div>
);
}
return (
<FieldContainer
isVertical={true}
labelVisible={false}
hasError={isEmailErrorShow}
errorMessage={
errorText ? t(`Common:${errorText}`) : t("Common:RequiredField")
} //TODO: Add wrong login server error
>
<EmailInput
id="login_username"
name="login"
type={InputType.email}
hasError={isEmailErrorShow}
value={identifier}
placeholder={t("RegistrationEmailWatermark")}
size={InputSize.large}
scale={true}
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="username"
onChange={onChangeLogin}
onBlur={onBlurEmail}
onValidateInput={onValidateEmail}
/>
</FieldContainer>
);
};
export default EmailContainer;

View File

@ -0,0 +1,200 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { EmailInput } from "@docspace/shared/components/email-input";
import { Text } from "@docspace/shared/components/text";
import {
ModalDialog,
ModalDialogType,
} from "@docspace/shared/components/modal-dialog";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { toastr } from "@docspace/shared/components/toast";
import { sendInstructionsToChangePassword } from "@docspace/shared/api/people";
import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { ForgotPasswordModalDialogProps } from "@/types";
import ModalDialogContainer from "../../ModalDialogContainer";
const ForgotPasswordModalDialog = ({
isVisible,
userEmail,
onDialogClose,
}: ForgotPasswordModalDialogProps) => {
const [email, setEmail] = useState(userEmail);
const [emailError, setEmailError] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [errorText, setErrorText] = useState("");
const [isShowError, setIsShowError] = useState(false);
const { t } = useTranslation(["Login", "Common"]);
const onChangeEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
//console.log("onChangeEmail", event.target.value);
setEmail(event.target.value);
setEmailError(false);
setIsShowError(false);
};
const onSendPasswordInstructions = React.useCallback(async () => {
if (!email || !email.trim() || emailError) {
setEmailError(true);
setIsShowError(true);
} else {
setIsLoading(true);
try {
const res = (await sendInstructionsToChangePassword(email)) as string;
toastr.success(res);
} catch (e) {
toastr.error(e as string);
} finally {
onDialogClose();
}
}
}, [email, emailError, onDialogClose]);
const onKeyDown = React.useCallback(
(e: KeyboardEvent) => {
//console.log("onKeyDown", e.key);
if (e.key === "Enter") {
onSendPasswordInstructions();
e.preventDefault();
}
},
[onSendPasswordInstructions],
);
const onValidateEmail = (res: TValidate) => {
setEmailError(!res.isValid);
setErrorText(res.errors?.[0] ?? "");
return undefined;
};
const onBlurEmail = () => {
setIsShowError(true);
};
React.useEffect(() => {
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [onKeyDown]);
return (
<ModalDialogContainer
displayType={ModalDialogType.modal}
autoMaxHeight
visible={isVisible}
onClose={onDialogClose}
id="forgot-password-modal"
>
<ModalDialog.Header>
<Text isBold fontSize="21px">
{t("PasswordRecoveryTitle")}
</Text>
</ModalDialog.Header>
<ModalDialog.Body>
<Text
key="text-body"
className="text-body"
isBold={false}
fontSize="13px"
noSelect
>
{t("MessageSendPasswordRecoveryInstructionsOnEmail")}
</Text>
<FieldContainer
className="email-reg-field"
key="e-mail"
isVertical
hasError={isShowError && emailError}
labelVisible={false}
errorMessage={
errorText ? t(`Common:${errorText}`) : t("Common:RequiredField")
}
>
<EmailInput
hasError={isShowError && emailError}
placeholder={t("Common:RegistrationEmail")}
isAutoFocussed
id="forgot-password-modal_email"
name="e-mail"
type={InputType.text}
size={InputSize.base}
scale
tabIndex={2}
isDisabled={isLoading}
value={email}
onChange={onChangeEmail}
onValidateInput={onValidateEmail}
onBlur={onBlurEmail}
/>
</FieldContainer>
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
id="forgot-password-modal_send"
className="modal-dialog-button"
key="ForgotSendBtn"
label={
isLoading ? t("Common:LoadingProcessing") : t("Common:SendButton")
}
size={ButtonSize.normal}
scale
primary
onClick={onSendPasswordInstructions}
isLoading={isLoading}
isDisabled={isLoading}
tabIndex={2}
/>
<Button
id="forgot-password-modal_cancel"
className="modal-dialog-button"
key="CancelBtn"
label={t("Common:CancelButton")}
size={ButtonSize.normal}
scale
primary={false}
onClick={onDialogClose}
isLoading={isLoading}
isDisabled={isLoading}
tabIndex={2}
/>
</ModalDialog.Footer>
</ModalDialogContainer>
);
};
export default ForgotPasswordModalDialog;

View File

@ -88,7 +88,7 @@ const Register = (props: RegisterProps) => {
setIsShowError(true);
};
const onSendRegisterRequest = async () => {
const onSendRegisterRequest = React.useCallback(async () => {
if (!email.trim() || emailErr) {
setEmailErr(true);
setIsShowError(true);
@ -105,14 +105,17 @@ const Register = (props: RegisterProps) => {
onRegisterModalClose();
}
}
};
}, []);
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
onSendRegisterRequest();
e.preventDefault();
}
};
const onKeyDown = React.useCallback(
(e: KeyboardEvent) => {
if (e.key === "Enter") {
onSendRegisterRequest();
e.preventDefault();
}
},
[onSendRegisterRequest],
);
return enabledJoin && !isAuthenticated ? (
<>

View File

@ -30,6 +30,7 @@ import {
TCapabilities,
TGetColorTheme,
TGetSsoSettings,
TPasswordHash,
TSettings,
TThirdPartyProvider,
TVersionBuild,
@ -56,7 +57,7 @@ export type GreetingContainersProps = {
firstName?: string;
lastName?: string;
greetingSettings?: string;
logoUrl: string;
logoUrl?: string;
type: string;
};
@ -89,3 +90,22 @@ export type RegisterModalDialogProps = {
errorText?: string;
isShowError?: boolean;
};
export type LoginFormProps = {
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
hashSettings: TPasswordHash;
isDesktop: boolean;
match: { [key: string]: string };
openRecoverDialog: () => void;
enableAdmMess: boolean;
recaptchaPublicKey?: string;
emailFromInvitation?: string;
cookieSettingsEnabled: boolean;
};
export type ForgotPasswordModalDialogProps = {
isVisible: boolean;
userEmail?: string;
onDialogClose: () => void;
};

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
export const DEFAULT_EMAIL_TEXT =
"User <1>{{email}}</1> is already registered in this DocSpace, enter your password or go back to continue with another email.";
export const DEFAULT_ROOM_TEXT =
"<strong>{{firstName}} {{lastName}}</strong> invites you to join the room <strong>{{roomName}}</strong> for secure document collaboration.";
export const DEFAULT_PORTAL_TEXT =

View File

@ -1,21 +1,17 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"types": [
"./index.d.ts"
],
"lib": ["dom", "dom.iterable", "esnext"],
"types": ["./index.d.ts"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"module": "ESNext",
"moduleResolution": "node",
"rootDir": "./",
"baseUrl": "./",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
@ -26,15 +22,8 @@
}
],
"paths": {
"@/*": [
"./src/*"
],
"PUBLIC_DIR/*": [
"../../public/*"
],
"ASSETS_DIR/*": [
"./public/*"
]
"@/*": ["./src/*"],
"PUBLIC_DIR/*": ["../../public/*"]
}
},
"include": [
@ -45,7 +34,5 @@
"next.config.js",
"./.next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

View File

@ -3191,6 +3191,7 @@ __metadata:
"@types/node": "npm:^20"
"@types/react": "npm:^18"
"@types/react-dom": "npm:^18"
"@types/react-google-recaptcha": "npm:^2.1.9"
babel-plugin-styled-components: "npm:^2.1.4"
eslint: "npm:^8"
eslint-config-next: "npm:14.0.4"
@ -3199,6 +3200,7 @@ __metadata:
prettier: "npm:^3.2.4"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-google-recaptcha: "npm:^3.1.0"
react-i18next: "npm:^13.2.1"
sass: "npm:^1.59.3"
shx: "npm:^0.3.4"
@ -9157,6 +9159,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-google-recaptcha@npm:^2.1.9":
version: 2.1.9
resolution: "@types/react-google-recaptcha@npm:2.1.9"
dependencies:
"@types/react": "npm:*"
checksum: 5a90bb50fe0a49f2e2ceb7950859cfdbe019aa0a75261451fb50ad83dcd4aa360a1941ab6c9b3c627f2b7a0dab2c6c09d6297469d3d4d02ebaf36b3fc34429b6
languageName: node
linkType: hard
"@types/react-syntax-highlighter@npm:11.0.4":
version: 11.0.4
resolution: "@types/react-syntax-highlighter@npm:11.0.4"