Login: add support oauth2
This commit is contained in:
parent
f631a208f7
commit
ed4b027bcc
23
packages/login/index.d.ts
vendored
23
packages/login/index.d.ts
vendored
@ -1,3 +1,8 @@
|
||||
import {
|
||||
IClientProps,
|
||||
INoAuthClientProps,
|
||||
IScope,
|
||||
} from "@docspace/common/utils/oauth/interfaces";
|
||||
import { Request } from "express";
|
||||
|
||||
type WindowI18nType = {
|
||||
@ -28,7 +33,8 @@ declare global {
|
||||
messageKey?: string;
|
||||
authError?: string;
|
||||
type?: string;
|
||||
clientId?: string;
|
||||
client_id?: string;
|
||||
state?: string;
|
||||
};
|
||||
|
||||
type PasswordHashType = {
|
||||
@ -130,18 +136,13 @@ declare global {
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface IOAuthClient {
|
||||
name: string;
|
||||
logo: string;
|
||||
privacyURL: string;
|
||||
termsURL: string;
|
||||
scopes: string[];
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
interface IOAuthState {
|
||||
client: IOAuthClient;
|
||||
client: IClientProps | INoAuthClientProps;
|
||||
clientId: string;
|
||||
state: string;
|
||||
self?: ISelf;
|
||||
scopes?: IScope[];
|
||||
isConsent: boolean;
|
||||
}
|
||||
|
||||
interface IInitialState {
|
||||
|
@ -7,6 +7,7 @@ import initLoginStore from "../store";
|
||||
import { Provider as MobxProvider } from "mobx-react";
|
||||
import SimpleNav from "../client/components/sub-components/SimpleNav";
|
||||
import { wrongPortalNameUrl } from "@docspace/common/constants";
|
||||
import Consent from "./components/Consent";
|
||||
|
||||
interface ILoginProps extends IInitialState {
|
||||
isDesktopEditor?: boolean;
|
||||
@ -35,6 +36,15 @@ const App: React.FC<ILoginProps> = (props) => {
|
||||
<MobxProvider {...loginStore}>
|
||||
<SimpleNav {...props} />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/login/consent"
|
||||
element={
|
||||
<Login
|
||||
isConsent={props.isAuth && !!props.oauth?.state}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/login/error" element={<InvalidRoute {...props} />} />
|
||||
<Route path="/login/code" element={<CodeLogin {...props} />} />
|
||||
<Route path="/login" element={<Login {...props} />} />
|
||||
|
@ -26,6 +26,8 @@ import { getBgPattern } from "@docspace/common/utils";
|
||||
import useIsomorphicLayoutEffect from "../hooks/useIsomorphicLayoutEffect";
|
||||
import { getLogoFromPath, getSystemTheme } from "@docspace/common/utils";
|
||||
import { TenantStatus } from "@docspace/common/constants";
|
||||
import Consent from "./sub-components/Consent";
|
||||
import { IClientProps, IScope } from "@docspace/common/utils/oauth/interfaces";
|
||||
|
||||
const themes = {
|
||||
Dark: Dark,
|
||||
@ -37,6 +39,7 @@ interface ILoginProps extends IInitialState {
|
||||
theme: IUserTheme;
|
||||
setTheme: (theme: IUserTheme) => void;
|
||||
isBaseTheme: boolean;
|
||||
isConsent?: boolean;
|
||||
}
|
||||
|
||||
const Login: React.FC<ILoginProps> = ({
|
||||
@ -51,6 +54,7 @@ const Login: React.FC<ILoginProps> = ({
|
||||
logoUrls,
|
||||
isBaseTheme,
|
||||
oauth,
|
||||
isConsent,
|
||||
}) => {
|
||||
const isOAuthPage = !!oauth?.client.name;
|
||||
|
||||
@ -60,9 +64,18 @@ const Login: React.FC<ILoginProps> = ({
|
||||
useEffect(() => {
|
||||
isRestoringPortal && window.location.replace("/preparation-portal");
|
||||
}, []);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [moreAuthVisible, setMoreAuthVisible] = useState(false);
|
||||
const [recoverDialogVisible, setRecoverDialogVisible] = useState(false);
|
||||
const [isConsentPage, setIsConsentPage] = useState(
|
||||
isConsent || oauth?.isConsent
|
||||
);
|
||||
const [scopes, setScopes] = useState(oauth?.scopes || ([] as IScope[]));
|
||||
const [oauthClient, setOAuthClient] = useState(
|
||||
oauth?.client || ({} as IClientProps)
|
||||
);
|
||||
const [self, setSelf] = useState(oauth?.self || ({} as ISelf));
|
||||
|
||||
const {
|
||||
enabledJoin,
|
||||
@ -243,67 +256,79 @@ const Login: React.FC<ILoginProps> = ({
|
||||
>
|
||||
{greetingSettings}
|
||||
</Text>
|
||||
<FormWrapper id="login-form" theme={theme}>
|
||||
{ssoExists() && !isOAuthPage && (
|
||||
<ButtonsWrapper>{ssoButton()}</ButtonsWrapper>
|
||||
)}
|
||||
{oauthDataExists() && !isOAuthPage && (
|
||||
<>
|
||||
<ButtonsWrapper>{providerButtons()}</ButtonsWrapper>
|
||||
{providers && providers.length > 2 && (
|
||||
<Link
|
||||
isHovered
|
||||
type="action"
|
||||
fontSize="13px"
|
||||
fontWeight="600"
|
||||
color={currentColorScheme?.main?.accent}
|
||||
className="more-label"
|
||||
onClick={moreAuthOpen}
|
||||
>
|
||||
{t("Common:ShowMore")}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(oauthDataExists() || ssoExists()) && !isOAuthPage && (
|
||||
<div className="line">
|
||||
<Text className="or-label">{t("Or")}</Text>
|
||||
</div>
|
||||
)}
|
||||
<LoginForm
|
||||
isBaseTheme={isBaseTheme}
|
||||
recaptchaPublicKey={portalSettings?.recaptchaPublicKey}
|
||||
isDesktop={!!isDesktopEditor}
|
||||
isLoading={isLoading}
|
||||
hashSettings={portalSettings?.passwordHash}
|
||||
setIsLoading={setIsLoading}
|
||||
openRecoverDialog={openRecoverDialog}
|
||||
match={match}
|
||||
enableAdmMess={enableAdmMess}
|
||||
cookieSettingsEnabled={cookieSettingsEnabled}
|
||||
isOAuthPage={isOAuthPage}
|
||||
oauth={oauth}
|
||||
{isConsentPage && isOAuthPage ? (
|
||||
<Consent
|
||||
oauth={{ ...oauth, scopes, client: oauthClient, self }}
|
||||
theme={theme}
|
||||
setIsConsentScreen={setIsConsentPage}
|
||||
/>
|
||||
</FormWrapper>
|
||||
<Toast />
|
||||
|
||||
<MoreLoginModal
|
||||
visible={moreAuthVisible}
|
||||
onClose={moreAuthClose}
|
||||
providers={providers}
|
||||
onSocialLoginClick={onSocialButtonClick}
|
||||
ssoLabel={ssoLabel}
|
||||
ssoUrl={ssoUrl}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<RecoverAccessModalDialog
|
||||
visible={recoverDialogVisible}
|
||||
onClose={closeRecoverDialog}
|
||||
textBody={t("RecoverTextBody")}
|
||||
emailPlaceholderText={t("RecoverContactEmailPlaceholder")}
|
||||
id="recover-access-modal"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<FormWrapper id="login-form" theme={theme}>
|
||||
{ssoExists() && !isOAuthPage && (
|
||||
<ButtonsWrapper>{ssoButton()}</ButtonsWrapper>
|
||||
)}
|
||||
{oauthDataExists() && !isOAuthPage && (
|
||||
<>
|
||||
<ButtonsWrapper>{providerButtons()}</ButtonsWrapper>
|
||||
{providers && providers.length > 2 && (
|
||||
<Link
|
||||
isHovered
|
||||
type="action"
|
||||
fontSize="13px"
|
||||
fontWeight="600"
|
||||
color={currentColorScheme?.main?.accent}
|
||||
className="more-label"
|
||||
onClick={moreAuthOpen}
|
||||
>
|
||||
{t("Common:ShowMore")}
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(oauthDataExists() || ssoExists()) && !isOAuthPage && (
|
||||
<div className="line">
|
||||
<Text className="or-label">{t("Or")}</Text>
|
||||
</div>
|
||||
)}
|
||||
<LoginForm
|
||||
isBaseTheme={isBaseTheme}
|
||||
recaptchaPublicKey={portalSettings?.recaptchaPublicKey}
|
||||
isDesktop={!!isDesktopEditor}
|
||||
isLoading={isLoading}
|
||||
hashSettings={portalSettings?.passwordHash}
|
||||
setIsLoading={setIsLoading}
|
||||
openRecoverDialog={openRecoverDialog}
|
||||
match={match}
|
||||
enableAdmMess={enableAdmMess}
|
||||
cookieSettingsEnabled={cookieSettingsEnabled}
|
||||
isOAuthPage={isOAuthPage}
|
||||
oauth={oauth}
|
||||
setIsConsentPage={setIsConsentPage}
|
||||
setScopes={setScopes}
|
||||
setOAuthClient={setOAuthClient}
|
||||
setSelf={setSelf}
|
||||
/>
|
||||
</FormWrapper>
|
||||
<Toast />
|
||||
<MoreLoginModal
|
||||
visible={moreAuthVisible}
|
||||
onClose={moreAuthClose}
|
||||
providers={providers}
|
||||
onSocialLoginClick={onSocialButtonClick}
|
||||
ssoLabel={ssoLabel}
|
||||
ssoUrl={ssoUrl}
|
||||
t={t}
|
||||
/>
|
||||
<RecoverAccessModalDialog
|
||||
visible={recoverDialogVisible}
|
||||
onClose={closeRecoverDialog}
|
||||
textBody={t("RecoverTextBody")}
|
||||
emailPlaceholderText={t("RecoverContactEmailPlaceholder")}
|
||||
id="recover-access-modal"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ColorTheme>
|
||||
</LoginContent>
|
||||
|
||||
|
180
packages/login/src/client/components/sub-components/Consent.tsx
Normal file
180
packages/login/src/client/components/sub-components/Consent.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
//@ts-ignore
|
||||
import api from "@docspace/common/api";
|
||||
|
||||
import ScopeList from "@docspace/common/utils/oauth/ScopeList";
|
||||
|
||||
//@ts-ignore
|
||||
import FormWrapper from "@docspace/components/form-wrapper";
|
||||
//@ts-ignore
|
||||
import Button from "@docspace/components/button";
|
||||
//@ts-ignore
|
||||
import Text from "@docspace/components/text";
|
||||
//@ts-ignore
|
||||
import Link from "@docspace/components/link";
|
||||
//@ts-ignore
|
||||
import Avatar from "@docspace/components/avatar";
|
||||
//@ts-ignore
|
||||
import { Base } from "@docspace/components/themes";
|
||||
|
||||
import OAuthClientInfo from "./oauth-client-info";
|
||||
import { getCookie } from "@docspace/common/utils";
|
||||
|
||||
const StyledFormWrapper = styled(FormWrapper)`
|
||||
width: 416px;
|
||||
max-width: 416px;
|
||||
|
||||
.button-container {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.description-container {
|
||||
width: 100%;
|
||||
|
||||
margin-bottom: 16px;
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.user-container {
|
||||
width: 100%;
|
||||
|
||||
padding-top: 16px;
|
||||
|
||||
border-top: 1px solid ${(props) => props.theme.separatorColor};
|
||||
|
||||
.block {
|
||||
height: 40px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledFormWrapper.defaultProps = { theme: Base };
|
||||
|
||||
interface IConsentProps {
|
||||
oauth: IOAuthState;
|
||||
theme: IUserTheme;
|
||||
setIsConsentScreen: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const Consent = ({ oauth, theme, setIsConsentScreen }: IConsentProps) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const { t } = useTranslation(["Consent", "Common"]);
|
||||
|
||||
const onAllowClick = () => {
|
||||
const clientId = oauth.clientId;
|
||||
const clientState = oauth.state || getCookie("client_state");
|
||||
const scope = oauth.client.scopes;
|
||||
|
||||
api.oauth.onOAuthSubmit(clientId, clientState, scope);
|
||||
};
|
||||
|
||||
const onDenyClick = () => {
|
||||
window.location.href = oauth.client.websiteUrl;
|
||||
};
|
||||
|
||||
const onChangeUserClick = () => {
|
||||
api.user.logout();
|
||||
setIsConsentScreen(false);
|
||||
navigate(`/login/${location.search}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledFormWrapper id={"consent"} theme={theme}>
|
||||
<OAuthClientInfo
|
||||
name={oauth.client.name}
|
||||
logo={oauth.client.logo}
|
||||
websiteUrl={oauth.client.websiteUrl}
|
||||
isConsentScreen
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<ScopeList
|
||||
t={t}
|
||||
selectedScopes={oauth.client.scopes || []}
|
||||
scopes={oauth.scopes || []}
|
||||
/>
|
||||
|
||||
<div className="button-container">
|
||||
<Button
|
||||
onClick={onAllowClick}
|
||||
label={"Allow"}
|
||||
size={"normal"}
|
||||
scale
|
||||
primary
|
||||
/>
|
||||
<Button onClick={onDenyClick} label={"Deny"} size={"normal"} scale />
|
||||
</div>
|
||||
<div className="description-container">
|
||||
<Text fontWeight={400} fontSize={"13px"} lineHeight={"20px"}>
|
||||
<Trans t={t} i18nKey={"ConsentDescription"} ns="Consent">
|
||||
Data shared with {{ displayName: oauth.self?.displayName }} will be
|
||||
governed by {{ nameApp: oauth.client.name }}
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type="page"
|
||||
isHovered={false}
|
||||
href={oauth.client.policyUrl}
|
||||
target={"_blank"}
|
||||
noHover
|
||||
>
|
||||
privacy policy
|
||||
</Link>
|
||||
and
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type="page"
|
||||
isHovered={false}
|
||||
href={oauth.client.termsUrl}
|
||||
target={"_blank"}
|
||||
noHover
|
||||
>
|
||||
terms of service
|
||||
</Link>
|
||||
. You can revoke this consent at any time in your DocSpace account
|
||||
settings.
|
||||
</Trans>
|
||||
</Text>
|
||||
</div>
|
||||
<div className="user-container">
|
||||
<div className="block">
|
||||
<Avatar size={"min"} source={oauth.self?.avatarSmall} />
|
||||
<div className="user-info">
|
||||
<Text lineHeight={"20px"}>
|
||||
{t("SignedInAs")} {oauth.self?.email}
|
||||
</Text>
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type="action"
|
||||
isHovered={false}
|
||||
noHover
|
||||
lineHeight={"20px"}
|
||||
onClick={onChangeUserClick}
|
||||
>
|
||||
{t("NotYou")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StyledFormWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Consent;
|
@ -10,7 +10,11 @@ import Link from "@docspace/components/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ForgotPasswordModalDialog from "./forgot-password-modal-dialog";
|
||||
import Button from "@docspace/components/button";
|
||||
import { checkIsSSR, createPasswordHash } from "@docspace/common/utils";
|
||||
import {
|
||||
checkIsSSR,
|
||||
createPasswordHash,
|
||||
setCookie,
|
||||
} from "@docspace/common/utils";
|
||||
import { checkPwd } from "@docspace/common/desktop";
|
||||
import { login } from "@docspace/common/utils/loginUtils";
|
||||
import toastr from "@docspace/components/toast/toastr";
|
||||
@ -21,6 +25,14 @@ import ReCAPTCHA from "react-google-recaptcha";
|
||||
import { OAuthLinksContainer, StyledCaptcha } from "../StyledLogin";
|
||||
import OAuthClientInfo from "./oauth-client-info";
|
||||
import SelectUser from "./SelectUser";
|
||||
import {
|
||||
getClient,
|
||||
getScopeList,
|
||||
loginWithOAuth,
|
||||
} from "@docspace/common/api/oauth";
|
||||
import { getUser } from "@docspace/common/api/people";
|
||||
import { onOAuthLogin } from "@docspace/common/api/oauth";
|
||||
import { IClientProps, IScope } from "@docspace/common/utils/oauth/interfaces";
|
||||
|
||||
interface ILoginFormProps {
|
||||
isLoading: boolean;
|
||||
@ -34,6 +46,10 @@ interface ILoginFormProps {
|
||||
isBaseTheme: boolean;
|
||||
oauth?: IOAuthState;
|
||||
isOAuthPage?: boolean;
|
||||
setIsConsentPage?: (val: boolean) => void;
|
||||
setScopes: (val: IScope[]) => void;
|
||||
setOAuthClient: (val: IClientProps) => void;
|
||||
setSelf: (val: ISelf) => void;
|
||||
}
|
||||
|
||||
const settings = {
|
||||
@ -56,15 +72,15 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
isBaseTheme,
|
||||
oauth,
|
||||
isOAuthPage,
|
||||
setIsConsentPage,
|
||||
setScopes,
|
||||
setOAuthClient,
|
||||
setSelf,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const captchaRef = useRef(null);
|
||||
|
||||
const [isOAuthSelectUser, setIsOAuthSelectUser] = useState(
|
||||
isOAuthPage && !!oauth?.self
|
||||
);
|
||||
|
||||
const [isEmailErrorShow, setIsEmailErrorShow] = useState(false);
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [identifier, setIdentifier] = useState("");
|
||||
@ -83,7 +99,7 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { t, ready } = useTranslation(["Login", "Common"]);
|
||||
const { t, ready } = useTranslation(["Login", "Common", "Consent"]);
|
||||
|
||||
const { message, confirmedEmail, authError } = match || {
|
||||
message: "",
|
||||
@ -91,12 +107,6 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
authError: "",
|
||||
};
|
||||
|
||||
const showLoginForm = () => setIsOAuthSelectUser(false);
|
||||
|
||||
const onOAuthLogin = () => {
|
||||
navigate(`/login/consent?clientId=${oauth?.client.clientId}`);
|
||||
};
|
||||
|
||||
const authCallback = (profile: string) => {
|
||||
localStorage.removeItem("profile");
|
||||
localStorage.removeItem("code");
|
||||
@ -219,18 +229,34 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
login(user, hash, session, captchaToken)
|
||||
.then((res: string | object) => {
|
||||
if (isOAuthPage) {
|
||||
return onOAuthLogin();
|
||||
}
|
||||
const isConfirm = typeof res === "string" && res.includes("confirm");
|
||||
const redirectPath = sessionStorage.getItem("referenceUrl");
|
||||
if (redirectPath && !isConfirm) {
|
||||
sessionStorage.removeItem("referenceUrl");
|
||||
window.location.href = redirectPath;
|
||||
return;
|
||||
}
|
||||
const requests = [];
|
||||
|
||||
if (typeof res === "string") window.location.replace(res);
|
||||
else window.location.replace("/"); //TODO: save { user, hash } for tfa
|
||||
setCookie("client_id", oauth?.clientId);
|
||||
|
||||
requests.push(getClient(oauth?.clientId || ""));
|
||||
requests.push(getScopeList());
|
||||
requests.push(getUser());
|
||||
requests.push(onOAuthLogin());
|
||||
|
||||
Promise.all(requests).then(([client, scopes, self]) => {
|
||||
setOAuthClient(client);
|
||||
setScopes(scopes);
|
||||
setSelf(self);
|
||||
setIsConsentPage && setIsConsentPage(true);
|
||||
setIsLoading(false);
|
||||
});
|
||||
} else {
|
||||
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 = "";
|
||||
@ -313,161 +339,149 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
<form className="auth-form-container">
|
||||
{oauth?.client && isOAuthPage && (
|
||||
<OAuthClientInfo
|
||||
name={oauth?.client.name}
|
||||
t={t}
|
||||
name={oauth.client.name}
|
||||
logo={oauth.client.logo}
|
||||
isAuth={!!oauth.self}
|
||||
websiteUrl={oauth.client.websiteUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOAuthSelectUser && oauth?.self ? (
|
||||
<SelectUser
|
||||
self={oauth?.self}
|
||||
onOAuthLogin={onOAuthLogin}
|
||||
showLoginForm={showLoginForm}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<FieldContainer
|
||||
isVertical={true}
|
||||
labelVisible={false}
|
||||
<>
|
||||
<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="email"
|
||||
hasError={isEmailErrorShow}
|
||||
errorMessage={
|
||||
errorText ? t(`Common:${errorText}`) : t("Common:RequiredField")
|
||||
} //TODO: Add wrong login server error
|
||||
>
|
||||
<EmailInput
|
||||
id="login_username"
|
||||
name="login"
|
||||
type="email"
|
||||
hasError={isEmailErrorShow}
|
||||
value={identifier}
|
||||
placeholder={t("RegistrationEmailWatermark")}
|
||||
size="large"
|
||||
scale={true}
|
||||
isAutoFocussed={true}
|
||||
tabIndex={1}
|
||||
isDisabled={isLoading}
|
||||
autoComplete="username"
|
||||
onChange={onChangeLogin}
|
||||
onBlur={onBlurEmail}
|
||||
onValidateInput={onValidateEmail}
|
||||
forwardedRef={inputRef}
|
||||
/>
|
||||
</FieldContainer>
|
||||
{(!IS_ROOMS_MODE || !isWithoutPasswordLogin) && (
|
||||
<>
|
||||
<FieldContainer
|
||||
isVertical={true}
|
||||
labelVisible={false}
|
||||
hasError={!passwordValid}
|
||||
errorMessage={!password.trim() ? t("Common:RequiredField") : ""} //TODO: Add wrong password server error
|
||||
>
|
||||
<PasswordInput
|
||||
className="password-input"
|
||||
simpleView={true}
|
||||
passwordSettings={settings}
|
||||
id="login_password"
|
||||
inputName="password"
|
||||
placeholder={t("Common:Password")}
|
||||
type="password"
|
||||
hasError={!passwordValid}
|
||||
inputValue={password}
|
||||
size="large"
|
||||
scale={true}
|
||||
tabIndex={1}
|
||||
isDisabled={isLoading}
|
||||
autoComplete="current-password"
|
||||
onChange={onChangePassword}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</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={
|
||||
!checkIsSSR() && (
|
||||
<HelpButton
|
||||
id="login_remember-hint"
|
||||
className="help-button"
|
||||
offsetRight={0}
|
||||
helpButtonHeaderContent={t("CookieSettingsTitle")}
|
||||
tooltipContent={
|
||||
<Text fontSize="12px">
|
||||
{t("RememberHelper")}
|
||||
</Text>
|
||||
}
|
||||
tooltipMaxWidth={isMobileOnly ? "240px" : "340px"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
fontSize="13px"
|
||||
className="login-link"
|
||||
type="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={isBaseTheme ? "light" : "dark"}
|
||||
onChange={onSuccessfullyComplete}
|
||||
/>
|
||||
</div>
|
||||
{isCaptchaError && (
|
||||
<Text>{t("Errors:LoginWithBruteForceCaptcha")}</Text>
|
||||
)}
|
||||
</StyledCaptcha>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
id="login_submit"
|
||||
className="login-button"
|
||||
primary
|
||||
size="medium"
|
||||
value={identifier}
|
||||
placeholder={t("RegistrationEmailWatermark")}
|
||||
size="large"
|
||||
scale={true}
|
||||
label={
|
||||
isLoading
|
||||
? t("Common:LoadingProcessing")
|
||||
: t("Common:LoginButton")
|
||||
}
|
||||
isAutoFocussed={true}
|
||||
tabIndex={1}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit}
|
||||
autoComplete="username"
|
||||
onChange={onChangeLogin}
|
||||
onBlur={onBlurEmail}
|
||||
onValidateInput={onValidateEmail}
|
||||
forwardedRef={inputRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</FieldContainer>
|
||||
{(!IS_ROOMS_MODE || !isWithoutPasswordLogin) && (
|
||||
<>
|
||||
<FieldContainer
|
||||
isVertical={true}
|
||||
labelVisible={false}
|
||||
hasError={!passwordValid}
|
||||
errorMessage={!password.trim() ? t("Common:RequiredField") : ""} //TODO: Add wrong password server error
|
||||
>
|
||||
<PasswordInput
|
||||
className="password-input"
|
||||
simpleView={true}
|
||||
passwordSettings={settings}
|
||||
id="login_password"
|
||||
inputName="password"
|
||||
placeholder={t("Common:Password")}
|
||||
type="password"
|
||||
hasError={!passwordValid}
|
||||
inputValue={password}
|
||||
size="large"
|
||||
scale={true}
|
||||
tabIndex={1}
|
||||
isDisabled={isLoading}
|
||||
autoComplete="current-password"
|
||||
onChange={onChangePassword}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</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={
|
||||
!checkIsSSR() && (
|
||||
<HelpButton
|
||||
id="login_remember-hint"
|
||||
className="help-button"
|
||||
offsetRight={0}
|
||||
helpButtonHeaderContent={t("CookieSettingsTitle")}
|
||||
tooltipContent={
|
||||
<Text fontSize="12px">{t("RememberHelper")}</Text>
|
||||
}
|
||||
tooltipMaxWidth={isMobileOnly ? "240px" : "340px"}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
fontSize="13px"
|
||||
className="login-link"
|
||||
type="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={isBaseTheme ? "light" : "dark"}
|
||||
onChange={onSuccessfullyComplete}
|
||||
/>
|
||||
</div>
|
||||
{isCaptchaError && (
|
||||
<Text>{t("Errors:LoginWithBruteForceCaptcha")}</Text>
|
||||
)}
|
||||
</StyledCaptcha>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
id="login_submit"
|
||||
className="login-button"
|
||||
primary
|
||||
size="medium"
|
||||
scale={true}
|
||||
label={
|
||||
isLoading ? t("Common:LoadingProcessing") : t("Common:LoginButton")
|
||||
}
|
||||
tabIndex={1}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
</>
|
||||
{/*Uncomment when add api*/}
|
||||
{(!IS_ROOMS_MODE || !isWithoutPasswordLogin) && !isOAuthPage && (
|
||||
<div className="login-or-access">
|
||||
@ -498,7 +512,6 @@ const LoginForm: React.FC<ILoginFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{IS_ROOMS_MODE && isWithoutPasswordLogin && !isOAuthPage && (
|
||||
<div className="login-link">
|
||||
<Link
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
//@ts-ignore
|
||||
import Text from "@docspace/components/text";
|
||||
@ -14,13 +15,13 @@ const StyledOAuthContainer = styled.div`
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
gap: 12px;
|
||||
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.row {
|
||||
@ -36,27 +37,72 @@ const StyledOAuthContainer = styled.div`
|
||||
interface IOAuthClientInfo {
|
||||
name: string;
|
||||
logo: string;
|
||||
isAuth: boolean;
|
||||
websiteUrl: string;
|
||||
|
||||
isConsentScreen?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const OAuthClientInfo = ({ name, logo, isAuth }: IOAuthClientInfo) => {
|
||||
const OAuthClientInfo = ({
|
||||
name,
|
||||
logo,
|
||||
websiteUrl,
|
||||
|
||||
isConsentScreen,
|
||||
|
||||
t,
|
||||
}: IOAuthClientInfo) => {
|
||||
return (
|
||||
<StyledOAuthContainer>
|
||||
<img src={logo} alt={"client-logo"} />
|
||||
<Text className={"row"} fontSize={"20px"} lineHeight={"30px"}>
|
||||
{isAuth ? "Selected account" : "Sign in"}{" "}
|
||||
<Text
|
||||
className={"row"}
|
||||
fontWeight={600}
|
||||
fontSize={"16px"}
|
||||
lineHeight={"22px"}
|
||||
>
|
||||
{isConsentScreen ? <>{t("Consent")}</> : <>{t("Common:LoginButton")}</>}
|
||||
</Text>
|
||||
<Text className={"row"}>
|
||||
to continue to{" "}
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type="page"
|
||||
fontWeight="600"
|
||||
isHovered={false}
|
||||
noHover
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
<img src={logo} alt={"client-logo"} />
|
||||
<Text
|
||||
className={"row"}
|
||||
fontWeight={isConsentScreen ? 400 : 600}
|
||||
fontSize={"16px"}
|
||||
lineHeight={"22px"}
|
||||
>
|
||||
{isConsentScreen ? (
|
||||
<Trans t={t} i18nKey={"ConsentSubHeader"} ns="Consent">
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type="page"
|
||||
isHovered={false}
|
||||
href={websiteUrl}
|
||||
target={"_blank"}
|
||||
noHover
|
||||
fontWeight={600}
|
||||
fontSize={"16px"}
|
||||
>
|
||||
{{ name }}
|
||||
</Link>{" "}
|
||||
would like the ability to access the following data in{" "}
|
||||
<strong>your DocSpace account</strong>:
|
||||
</Trans>
|
||||
) : (
|
||||
<>
|
||||
{t("Consent:ToContinue")}{" "}
|
||||
<Link
|
||||
className={"login-link"}
|
||||
type="page"
|
||||
isHovered={false}
|
||||
href={websiteUrl}
|
||||
target={"_blank"}
|
||||
noHover
|
||||
fontWeight={600}
|
||||
fontSize={"16px"}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</StyledOAuthContainer>
|
||||
);
|
||||
|
@ -22,7 +22,7 @@ i18next.init({
|
||||
load: "currentOnly",
|
||||
|
||||
saveMissing: true,
|
||||
ns: ["Login", "Errors", "Common"],
|
||||
ns: ["Login", "Errors", "Consent", "Common"],
|
||||
defaultNS: "Login",
|
||||
|
||||
resources,
|
||||
|
@ -62,22 +62,28 @@ app.get("*", async (req: ILoginRequest, res: Response, next) => {
|
||||
const hideAuthPage = initialState?.ssoSettings?.hideAuthPage;
|
||||
const ssoUrl = initialState?.capabilities?.ssoUrl;
|
||||
|
||||
const isOAuth = initialState.match?.type === "oauth2";
|
||||
const oauthClientId = initialState.match?.clientId || "";
|
||||
let isCorrectOAuth = false;
|
||||
const oauthClientId = initialState.match?.client_id || "";
|
||||
const oauthClientState = initialState.match?.state || "";
|
||||
|
||||
const isOAuth = initialState.match?.type === "oauth2" && !!oauthClientId;
|
||||
const isConsent = initialState.isAuth && isOAuth;
|
||||
|
||||
if (hideAuthPage && ssoUrl && query.skipssoredirect !== "true") {
|
||||
res.redirect(ssoUrl);
|
||||
return next();
|
||||
}
|
||||
|
||||
//TODO: get client by id
|
||||
if (isOAuth && oauthClientId) {
|
||||
let isCorrectOAuth = false;
|
||||
|
||||
if (isOAuth) {
|
||||
const oauthState: IOAuthState = await getOAuthState(
|
||||
oauthClientId,
|
||||
initialState?.isAuth
|
||||
initialState?.isAuth || false
|
||||
);
|
||||
|
||||
oauthState.state = oauthClientState;
|
||||
oauthState.isConsent = !!isConsent;
|
||||
|
||||
isCorrectOAuth = !!oauthState?.client.name;
|
||||
|
||||
if (isCorrectOAuth) {
|
||||
|
@ -12,7 +12,13 @@ import {
|
||||
} from "@docspace/common/api/settings";
|
||||
import { getUser } from "@docspace/common/api/people";
|
||||
import { checkIsAuthenticated } from "@docspace/common/api/user";
|
||||
import { getClient, getScopeList } from "@docspace/common/api/oauth";
|
||||
import { TenantStatus } from "@docspace/common/constants";
|
||||
import {
|
||||
IClientProps,
|
||||
INoAuthClientProps,
|
||||
IScope,
|
||||
} from "@docspace/common/utils/oauth/interfaces";
|
||||
|
||||
export const getAssets = (): assetsType => {
|
||||
const manifest = fs.readFileSync(
|
||||
@ -102,22 +108,32 @@ export const getInitialState = async (
|
||||
//TODO: get client by id for links
|
||||
export const getOAuthState = async (
|
||||
clientId: string,
|
||||
isAuth?: boolean
|
||||
isAuth: boolean
|
||||
): Promise<IOAuthState> => {
|
||||
const requests = [];
|
||||
|
||||
if (isAuth) requests.push(getUser());
|
||||
requests.push(getClient(clientId, isAuth));
|
||||
|
||||
const [self] = await Promise.all(requests);
|
||||
if (isAuth) {
|
||||
requests.push(getUser());
|
||||
requests.push(getScopeList());
|
||||
}
|
||||
|
||||
const client: IOAuthClient = {
|
||||
name: "Test",
|
||||
logo: "static/images/logo/leftmenu.svg?hash=c31b569ea8c6322337cd",
|
||||
privacyURL: "https://www.google.com/?hl=RU",
|
||||
termsURL: "https://www.google.com/?hl=RU",
|
||||
scopes: ["accounts:read"],
|
||||
clientId: "1",
|
||||
const [client, ...rest] = await Promise.all(requests);
|
||||
|
||||
const state: IOAuthState = {
|
||||
clientId,
|
||||
state: "",
|
||||
isConsent: false,
|
||||
client,
|
||||
self: undefined,
|
||||
scopes: undefined,
|
||||
};
|
||||
|
||||
return { client, self };
|
||||
if (isAuth) {
|
||||
state.self = rest[0] as ISelf;
|
||||
state.scopes = rest[1] as IScope[];
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user