Login: add support oauth2

This commit is contained in:
Timofey Boyko 2023-10-27 11:23:41 +03:00
parent f631a208f7
commit ed4b027bcc
9 changed files with 577 additions and 280 deletions

View File

@ -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 {

View File

@ -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} />} />

View File

@ -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>

View 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;

View File

@ -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

View File

@ -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>
);

View File

@ -22,7 +22,7 @@ i18next.init({
load: "currentOnly",
saveMissing: true,
ns: ["Login", "Errors", "Common"],
ns: ["Login", "Errors", "Consent", "Common"],
defaultNS: "Login",
resources,

View File

@ -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) {

View File

@ -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;
};