From 4db909ad0f0466c8e56379097a8305d5e394b5ff Mon Sep 17 00:00:00 2001 From: Darya Umrikhina Date: Fri, 19 Jul 2024 15:14:39 +0400 Subject: [PATCH] Login:Components:CreateUserForm: add CreateUserForm --- .../CreateUserForm/CreateUserForm.styled.tsx | 129 ++++ .../src/components/CreateUserForm/index.tsx | 687 ++++++++++++++++++ 2 files changed, 816 insertions(+) create mode 100644 packages/login/src/components/CreateUserForm/CreateUserForm.styled.tsx create mode 100644 packages/login/src/components/CreateUserForm/index.tsx diff --git a/packages/login/src/components/CreateUserForm/CreateUserForm.styled.tsx b/packages/login/src/components/CreateUserForm/CreateUserForm.styled.tsx new file mode 100644 index 0000000000..d8d94dd524 --- /dev/null +++ b/packages/login/src/components/CreateUserForm/CreateUserForm.styled.tsx @@ -0,0 +1,129 @@ +// (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 + +"use client"; + +import styled, { css } from "styled-components"; +import { mobile, tablet } from "@docspace/shared/utils"; + +export const RegisterContainer = styled.div<{ + registrationForm: boolean; +}>` + + height: 100%; + width: 100%; + + .or-label { + margin: 0 8px; + } + + .line { + display: flex; + width: 100%; + align-items: center; + color: ${(props) => props.theme.invitePage.borderColor}; + padding-top: 35px; + margin-bottom: 32px; + } + + .line:before, + .line:after { + content: ""; + flex-grow: 1; + background: ${(props) => props.theme.invitePage.borderColor}; + height: 1px; + font-size: 0px; + line-height: 0px; + margin: 0px; + } + + .auth-form-fields { + width: 100%; + + .password-field{ + margin-bottom: 24px; + } + + .email-container{ + ${(props) => props.registrationForm && "display:none"}; + } + @media ${tablet} { + width: 100%; + } + @media ${mobile} { + width: 100%; + } + } + + .password-field-wrapper { + width: 100%; + } + + .greeting-container{ + margin-bottom: 32px; + p{ + text-align: center; + } + .back-sign-in-container { + display: flex; + align-items: center; + justify-content: center; + position: relative; + + margin-bottom: 16px; + .back-button { + position: absolute; + max-width: 60px; + text-overflow: ellipsis; + overflow: hidden; + ${(props) => + props.theme.interfaceDirection === "rtl" + ? css` + right: 0; + ` + : css` + left: 0; + `}; + display: flex; + gap: 4px; + + svg { + ${(props) => + props.theme.interfaceDirection === "rtl" && + " transform: rotate(180deg)"}; + } + + p { + color: ${(props) => props.theme.login.backTitle.color}; + } + + p:hover { + cursor: pointer; + } + } + } + } +}`; diff --git a/packages/login/src/components/CreateUserForm/index.tsx b/packages/login/src/components/CreateUserForm/index.tsx new file mode 100644 index 0000000000..e333708c9f --- /dev/null +++ b/packages/login/src/components/CreateUserForm/index.tsx @@ -0,0 +1,687 @@ +// (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 + +"use client"; + +import SsoReactSvgUrl from "PUBLIC_DIR/images/sso.react.svg?url"; + +import { ConfirmRouteContext } from "@/app/(root)/confirm/confirmRoute"; +import withLoader from "@/app/(root)/confirm/withLoader"; +import { TPasswordHash } from "@docspace/shared/api/settings/types"; +import { toastr } from "@docspace/shared/components/toast"; +import { + COOKIE_EXPIRATION_YEAR, + LANGUAGE, + PROVIDERS_DATA, +} from "@docspace/shared/constants"; +import { combineUrl } from "@docspace/shared/utils/combineUrl"; +import { + createPasswordHash, + getLoginLink, + getOAuthToken, +} from "@docspace/shared/utils/common"; +import { setCookie } from "@docspace/shared/utils/cookie"; +import { + ChangeEvent, + KeyboardEvent, + MouseEvent, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { useTranslation } from "react-i18next"; +import { DeviceType } from "@docspace/shared/enums"; +import useDeviceType from "@/hooks/useDeviceType"; +import { RegisterContainer } from "./CreateUserForm.styled"; +import { FieldContainer } from "@docspace/shared/components/field-container"; +import { EmailInput, TValidate } from "@docspace/shared/components/email-input"; +import { Button, ButtonSize } from "@docspace/shared/components/button"; +import { + InputSize, + InputType, + TextInput, +} from "@docspace/shared/components/text-input"; +import { PasswordInput } from "@docspace/shared/components/password-input"; +import { getPasswordErrorMessage } from "@docspace/shared/utils/getPasswordErrorMessage"; +import { TError, WithLoaderProps } from "@/types"; +import { TUser } from "@docspace/shared/api/people/types"; +import { SocialButtonsGroup } from "@docspace/shared/components/social-buttons-group"; +import { Text } from "@docspace/shared/components/text"; + +import { login } from "@docspace/shared/api/user"; +import { + createUser, + getUserByEmail, + getUserFromConfirm, + signupOAuth, +} from "@/utils/actions"; + +export type CreateUserFormProps = { + userNameRegex: string; + passwordHash: TPasswordHash; + defaultPage?: string; +} & WithLoaderProps; + +const CreateUserForm = (props: CreateUserFormProps) => { + const { + userNameRegex, + passwordHash, + defaultPage = "/", + passwordSettings, + capabilities, + thirdPartyProviders, + } = props; + const { linkData, roomData } = useContext(ConfirmRouteContext); + const { t, i18n } = useTranslation(["Confirm", "Common", "Wizard"]); + const { currentDeviceType } = useDeviceType(); + + const currentCultureName = i18n.language; + const isDesktopView = currentDeviceType === DeviceType.desktop; + + const inputRef = useRef(null); + + const emailFromLink = linkData?.email ? linkData.email : ""; + const roomName = roomData?.title; + + const [email, setEmail] = useState(emailFromLink); + const [emailValid, setEmailValid] = useState(true); + const [emailErrorText, setEmailErrorText] = useState(); + + const [password, setPassword] = useState(""); + const [passwordValid, setPasswordValid] = useState(true); + + const [fname, setFname] = useState(""); + const [fnameValid, setFnameValid] = useState(true); + const [sname, setSname] = useState(""); + const [snameValid, setSnameValid] = useState(true); + + const [isLoading, setIsLoading] = useState(false); + + const [errorText, setErrorText] = useState(""); + + const [user, setUser] = useState(); + + const [isEmailErrorShow, setIsEmailErrorShow] = useState(false); + const [isPasswordErrorShow, setIsPasswordErrorShow] = useState(false); + + const [registrationForm, setRegistrationForm] = useState(!!emailFromLink); + + const focusInput = () => { + if (inputRef.current) { + inputRef.current.focus(); + } + }; + + const nameRegex = new RegExp(userNameRegex, "gu"); + + const authCallback = useCallback( + async (profile: string) => { + const signupAccount: { [key: string]: string } = { + EmployeeType: linkData.emplType ?? "", + Email: linkData.email ?? "", + Key: linkData.key ?? "", + SerializedProfile: profile, + culture: currentCultureName, + }; + + // remove from component? + return signupOAuth(signupAccount) + .then(() => { + const url = roomData.roomId + ? `/rooms/shared/filter?folder=${roomData.roomId}/` + : defaultPage; + window.location.replace(url); + }) + .catch((e) => { + toastr.error(e); + }); + }, + [ + currentCultureName, + defaultPage, + linkData.email, + linkData.emplType, + linkData.key, + roomData.roomId, + ], + ); + + useEffect(() => { + const fetchData = async () => { + if (linkData.type === "LinkInvite") { + const uid = linkData?.uid ?? ""; + const confirmKey = linkData?.confirmHeader || null; + // remove from component? + const user = await getUserFromConfirm(uid, confirmKey); + setUser(user); + } + window.authCallback = authCallback; + + focusInput(); + }; + + fetchData(); + }, [authCallback, linkData?.confirmHeader, linkData.type, linkData?.uid]); + + const onContinue = async () => { + setIsLoading(true); + + let hasError = false; + + const emailRegex = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$"; + const validationEmail = new RegExp(emailRegex); + + if (!validationEmail.test(email.trim())) { + hasError = true; + setEmailValid(!hasError); + } + + if (hasError) { + setIsLoading(false); + return; + } + + const headerKey = linkData?.confirmHeader ?? null; + + try { + const toBinaryStr = (str: string) => { + const encoder = new TextEncoder(); + const charCodes = encoder.encode(str); + return String.fromCharCode(...charCodes); + }; + + const loginData = window.btoa( + toBinaryStr( + JSON.stringify({ + type: "invitation", + email, + roomName, + firstName: user?.firstName, + lastName: user?.lastName, + }), + ), + ); + + const response = await getUserByEmail(email, headerKey); + + if (typeof response === "number") { + const isNotExistUser = response === 404; + + if (isNotExistUser) { + setRegistrationForm(true); + } + setIsLoading(false); + + return; + } + + setCookie(LANGUAGE, currentCultureName, { + "max-age": COOKIE_EXPIRATION_YEAR, + }); + + window.location.href = combineUrl( + window.ClientConfig?.proxy?.url, + "/login", + `?loginData=${loginData}`, + ); + } catch (error) { + /* const knownError = error as TError; + console.log("knownError", error.status); + + const status = + typeof knownError === "object" ? knownError.response?.status : ""; + const isNotExistUser = status === 404; + + if (isNotExistUser) { + setRegistrationForm(true); + } */ + } + + setIsLoading(false); + }; + + const onSubmit = () => { + const type = parseInt(linkData?.emplType ?? ""); + console.log("here"); + + setIsLoading(true); + setErrorText(""); + + let hasError = false; + + if (!fname.trim() || !fnameValid) { + hasError = true; + setFnameValid(!hasError); + } + + if (!sname.trim() || !snameValid) { + hasError = true; + setSnameValid(!hasError); + } + + const emailRegex = "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$"; + const validationEmail = new RegExp(emailRegex); + + if (!validationEmail.test(email.trim())) { + hasError = true; + setEmailValid(!hasError); + } + + if (!passwordValid || !password.trim()) { + hasError = true; + setPasswordValid(!hasError); + setIsPasswordErrorShow(true); + } + + if (hasError) { + setIsLoading(false); + return false; + } + + const hash = createPasswordHash(password, passwordHash); + + const loginData = { + userName: email, + passwordHash: hash, + }; + + const personalData: { [key: string]: string | number } = { + firstname: fname.trim(), + lastname: sname.trim(), + email: email, + cultureName: currentCultureName, + }; + + if (!!type) { + personalData.type = type; + } + + if (!!linkData.key) { + personalData.key = linkData.key; + } + + const headerKey = linkData?.confirmHeader ?? ""; + + createConfirmUser(personalData, loginData, headerKey).catch((error) => { + const knownError = error as TError; + let errorMessage: string; + + if (typeof knownError === "object") { + errorMessage = + knownError?.response?.data?.error?.message || + knownError?.statusText || + knownError?.message || + ""; + } else { + errorMessage = knownError; + } + + console.error("confirm error", errorMessage); + setIsEmailErrorShow(true); + setEmailErrorText(errorMessage); + setEmailValid(false); + setIsLoading(false); + }); + }; + + const createConfirmUser = async ( + registerData: { [key: string]: string | number }, + loginData: { userName: string; passwordHash: string }, + key: string, + ) => { + const fromInviteLink = + linkData.type === "LinkInvite" || linkData.type === "EmpInvite" + ? true + : false; + + const data = Object.assign({ fromInviteLink }, registerData, loginData); + + await createUser(data, key); + + const { userName, passwordHash } = loginData; + + const res = await login(userName, passwordHash); + + const finalUrl = roomData.roomId + ? `/rooms/shared/filter?folder=${roomData.roomId}` + : defaultPage; + + const isConfirm = typeof res === "string" && res.includes("confirm"); + + if (isConfirm) { + sessionStorage.setItem("referenceUrl", finalUrl); + + return window.location.replace(typeof res === "string" ? res : "/"); + } + + window.location.replace(finalUrl); + }; + + const onChangeEmail = (e: ChangeEvent) => { + setEmail(e.target.value); + setIsEmailErrorShow(false); + }; + const onChangeFname = (e: ChangeEvent) => { + setFname(e.target.value); + setFnameValid(nameRegex.test(e.target.value.trim())); + setErrorText(""); + }; + + const onChangeSname = (e: ChangeEvent) => { + setSname(e.target.value); + setSnameValid(nameRegex.test(e.target.value.trim())); + setErrorText(""); + }; + + const onChangePassword = (e: ChangeEvent) => { + setPassword(e.target.value); + setErrorText(""); + setIsPasswordErrorShow(false); + }; + + const onKeyPress = (e: KeyboardEvent) => { + if (e.key === "Enter") { + registrationForm ? onSubmit() : onContinue(); + } + }; + const onValidatePassword = (progressScore: boolean) => { + setPasswordValid(progressScore); + }; + + const onBlurEmail = () => { + setIsEmailErrorShow(true); + }; + + const onBlurPassword = () => { + setIsPasswordErrorShow(true); + }; + + const onSocialButtonClick = useCallback( + (e: MouseEvent) => { + const target = e.target as HTMLElement; + let targetElement = target; + + if ( + !(targetElement instanceof HTMLButtonElement) && + target.parentElement + ) { + targetElement = target.parentElement; + } + + const providerName = targetElement.dataset.providername; + const url = targetElement.dataset.url || ""; + + try { + const tokenGetterWin = isDesktopView + ? (window.location.href = url) + : window.open( + url, + "login", + "width=800,height=500,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no", + ); + + getOAuthToken(tokenGetterWin).then((code) => { + const token = window.btoa( + JSON.stringify({ + auth: providerName, + mode: "popup", + callback: "authCallback", + }), + ); + + if (tokenGetterWin && typeof tokenGetterWin === "object") + tokenGetterWin.location.href = getLoginLink(token, code); + }); + } catch (err) { + console.log(err); + } + }, + [isDesktopView], + ); + + const oauthDataExists = () => { + if (!capabilities?.oauthEnabled) return false; + + let existProviders = 0; + thirdPartyProviders && thirdPartyProviders.length > 0; + thirdPartyProviders?.map((item) => { + let key = item.provider as keyof typeof PROVIDERS_DATA; + if (PROVIDERS_DATA.hasOwnProperty(key) && !PROVIDERS_DATA[key]) return; + existProviders++; + }); + + return !!existProviders; + }; + + const ssoExists = () => { + if (capabilities?.ssoUrl) return true; + else return false; + }; + + const onValidateEmail = (result: TValidate): undefined => { + setEmailValid(result.isValid); + setEmailErrorText(result.errors?.[0]); + }; + + const ssoProps = ssoExists() + ? { + ssoUrl: capabilities?.ssoUrl, + ssoLabel: capabilities?.ssoLabel, + ssoSVG: SsoReactSvgUrl, + } + : {}; + + return ( + +
+
+ + + +
+ + {registrationForm && ( +
+ {/* */} + + + + + + + + + + +
+ )} +
+ + {!emailFromLink && (oauthDataExists() || ssoExists()) && ( + <> +
+ + {t("Common:orContinueWith")} + +
+ + + )} +
+ ); +}; + +export default withLoader(CreateUserForm);