Login(nextjs): add part of LoginForm
This commit is contained in:
parent
8b4b44d50d
commit
62ea894caa
4
packages/login-next/index.d.ts
vendored
4
packages/login-next/index.d.ts
vendored
@ -29,3 +29,7 @@ declare module "*.ico?url" {
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module "*.svg?url" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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};
|
||||
}
|
||||
`}
|
||||
`;
|
434
packages/login-next/src/components/LoginForm/index.tsx
Normal file
434
packages/login-next/src/components/LoginForm/index.tsx
Normal 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;
|
@ -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;
|
@ -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;
|
@ -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 ? (
|
||||
<>
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 =
|
||||
|
@ -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"]
|
||||
}
|
||||
|
11
yarn.lock
11
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user