Login: fix oauth pages

This commit is contained in:
Timofey Boyko 2024-05-30 10:07:09 +03:00
parent 996c79d617
commit 5e70e6fee5
24 changed files with 498 additions and 305 deletions

View File

@ -8,7 +8,6 @@ import { Button, ButtonSize } from "@docspace/shared/components/button";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
// @ts-ignore
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
interface DeleteClientDialogProps {
@ -55,7 +54,7 @@ const DeleteClientDialog = (props: DeleteClientDialogProps) => {
label={t("Common:OkButton")}
size={ButtonSize.normal}
scale
primary={true}
primary
isLoading={isRequestRunning}
onClick={onDisableClick}
/>
@ -88,6 +87,7 @@ export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
};
const onDisable = async () => {
if (!bufferSelection) return;
setActiveClient(bufferSelection.clientId);
await deleteClient([bufferSelection.clientId]);
setActiveClient("");

View File

@ -8,7 +8,6 @@ import { Button, ButtonSize } from "@docspace/shared/components/button";
import { toastr } from "@docspace/shared/components/toast";
import { TData } from "@docspace/shared/components/toast/Toast.type";
// @ts-ignore
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
interface DisableClientDialogProps {
@ -55,7 +54,7 @@ const DisableClientDialog = (props: DisableClientDialogProps) => {
label={t("Common:OkButton")}
size={ButtonSize.normal}
scale
primary={true}
primary
isLoading={isRequestRunning}
onClick={onDisableClick}
/>
@ -88,6 +87,8 @@ export default inject(({ oauthStore }: { oauthStore: OAuthStoreProps }) => {
};
const onDisable = async () => {
if (!bufferSelection) return;
setActiveClient(bufferSelection.clientId);
await changeClientStatus(bufferSelection.clientId, false);
setActiveClient("");

View File

@ -5,11 +5,8 @@ import { useTranslation } from "react-i18next";
import { IClientProps, IScope } from "@docspace/shared/utils/oauth/interfaces";
import ScopeList from "@docspace/shared/utils/oauth/ScopeList";
import getCorrectDate from "@docspace/shared/utils/getCorrectDate";
import { getCookie } from "@docspace/shared/utils/cookie";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { Text } from "@docspace/shared/components/text";
@ -17,23 +14,18 @@ import {
ContextMenuButton,
ContextMenuButtonDisplayType,
} from "@docspace/shared/components/context-menu-button";
// @ts-ignore
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import {
Avatar,
AvatarRole,
AvatarSize,
} from "@docspace/shared/components/avatar";
import {
LinkTarget,
LinkType,
} from "@docspace/shared/components/link/Link.enums";
import { Link } from "@docspace/shared/components/link";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
import { Base } from "@docspace/shared/themes";
import { TTranslation } from "@docspace/shared/types";
import { ContextMenuModel } from "@docspace/shared/components/context-menu";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
const StyledContainer = styled.div<{
showDescription: boolean;
withShowText: boolean;
@ -289,7 +281,7 @@ const InfoDialog = ({
fontWeight="600"
isHovered
onClick={() => setShowDescription((val) => !val)}
type={"action"}
type={LinkType.action}
>
{showDescription ? "Hide" : "Show more"}
</Link>
@ -314,8 +306,8 @@ const InfoDialog = ({
fontWeight="600"
isHovered
href={client?.websiteUrl}
type={"action"}
target={"_blank"}
type={LinkType.action}
target={LinkTarget.blank}
>
{client?.websiteUrl}
</Link>
@ -385,8 +377,8 @@ const InfoDialog = ({
fontWeight="600"
isHovered
href={client?.policyUrl}
type={"action"}
target={"_blank"}
type={LinkType.action}
target={LinkTarget.blank}
>
{t("PrivacyPolicy")}
</Link>
@ -398,8 +390,8 @@ const InfoDialog = ({
fontWeight="600"
isHovered
href={client?.termsUrl}
type={"action"}
target={"_blank"}
type={LinkType.action}
target={LinkTarget.blank}
>
{t("Terms of Service")}
</Link>

View File

@ -1,27 +1,25 @@
import React from "react";
import { inject, observer } from "mobx-react";
import styled from "styled-components";
import styled, { useTheme } from "styled-components";
import { useTranslation } from "react-i18next";
import { IClientProps } from "@docspace/shared/utils/oauth/interfaces";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { SocialButton } from "@docspace/shared/components/social-button";
import { Text } from "@docspace/shared/components/text";
import { Textarea } from "@docspace/shared/components/textarea";
import OnlyofficeLight from "PUBLIC_DIR/images/onlyoffice.light.react.svg";
import OnlyofficeDark from "PUBLIC_DIR/images/onlyoffice.dark.react.svg";
// @ts-ignore
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { Base } from "@docspace/shared/themes";
import { generatePKCEPair } from "@docspace/shared/utils/oauth";
import { AuthenticationMethod } from "@docspace/shared/enums";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import OnlyofficeLight from "PUBLIC_DIR/images/onlyoffice.light.react.svg";
import OnlyofficeDark from "PUBLIC_DIR/images/onlyoffice.dark.react.svg";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
const StyledContainer = styled.div`
width: 100%;
height: 100%;
@ -152,17 +150,15 @@ interface PreviewDialogProps {
setPreviewDialogVisible?: (value: boolean) => void;
client?: IClientProps;
theme?: any;
}
const PreviewDialog = ({
visible,
setPreviewDialogVisible,
client,
theme,
}: PreviewDialogProps) => {
const { t } = useTranslation(["OAuth", "Common", "Webhooks"]);
const theme = useTheme();
const [codeVerifier, setCodeVerifier] = React.useState("");
const [codeChallenge, setCodeChallenge] = React.useState("");
@ -181,16 +177,16 @@ const PreviewDialog = ({
const encodingScopes = encodeURI(scopesString || "");
const getData = React.useCallback(() => {
const { verifier, challenge, state } = generatePKCEPair();
const { verifier, challenge, state: s } = generatePKCEPair();
setCodeVerifier(verifier);
setCodeChallenge(challenge);
setState(state);
setState(s);
}, []);
React.useEffect(() => {
getData();
}, []);
}, [getData]);
const getLink = () => {
return `${

View File

@ -7,11 +7,10 @@ import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { ModalDialogType } from "@docspace/shared/components/modal-dialog/ModalDialog.enums";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { toastr } from "@docspace/shared/components/toast";
// @ts-ignore
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { OAuthStoreProps } from "SRC_DIR/store/OAuthStore";
interface ResetDialogProps {
isVisible?: boolean;
onClose?: () => void;

View File

@ -25,7 +25,7 @@
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "^18.2.53",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.0.4",

View File

@ -25,7 +25,7 @@
"devDependencies": {
"@svgr/webpack": "^8.1.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react": "^18.2.53",
"@types/react-dom": "^18",
"@types/react-google-recaptcha": "^2.1.9",
"babel-plugin-styled-components": "^2.1.4",

View File

@ -2,7 +2,7 @@
"Consent": "Consent",
"ConsentSubHeader": "{{name}} would like the ability to access the following data in <strong>your DocSpace account</strong>:",
"ConsentDescription": "Data shared with <strong>{{displayName}}</strong> will be governed by <strong>{{nameApp}}</strong> <6>privacy policy</6> and <6>terms of service</6>. You can revoke this consent at any time in your DocSpace account settings.",
"ToContinue": "to continue to",
"ToContinue": "To continue to",
"SignedInAs": "Signed in as",
"NotYou": "Not you?"
}

View File

@ -0,0 +1,52 @@
// (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 { IClientProps } from "@docspace/shared/utils/oauth/interfaces";
import Consent from "@/components/Consent";
import { getOAuthClient, getScopeList, getUser } from "@/utils/actions";
async function Page({
searchParams,
}: {
searchParams: { [key: string]: string };
}) {
const clientId = searchParams.clientId ?? searchParams.client_id;
const [client, scopes, user] = await Promise.all([
getOAuthClient(clientId, true),
getScopeList(),
getUser(),
]);
if (!client || (client && !("clientId" in client)) || !scopes || !user)
return "";
return (
<Consent client={client as IClientProps} scopes={scopes} user={user} />
);
}
export default Page;

View File

@ -24,6 +24,7 @@
// 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 { cookies } from "next/headers";
import { SYSTEM_THEME_KEY } from "@docspace/shared/constants";

View File

@ -24,15 +24,26 @@
// 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 { getSettings } from "@/utils/actions";
import { INoAuthClientProps } from "@docspace/shared/utils/oauth/interfaces";
import { getOAuthClient, getSettings } from "@/utils/actions";
import Login from "@/components/Login";
import LoginForm from "@/components/LoginForm";
import ThirdParty from "@/components/ThirdParty";
import RecoverAccess from "@/components/RecoverAccess";
import Register from "@/components/Register";
async function Page() {
const settings = await getSettings();
async function Page({
searchParams,
}: {
searchParams: { [key: string]: string };
}) {
const clientId = searchParams.clientId;
const [settings, client] = await Promise.all([
getSettings(),
clientId ? getOAuthClient(clientId, false) : undefined,
]);
return (
<Login>
@ -41,10 +52,12 @@ async function Page() {
<LoginForm
hashSettings={settings?.passwordHash}
cookieSettingsEnabled={settings?.cookieSettingsEnabled}
clientId={clientId}
client={client as INoAuthClientProps}
/>
<ThirdParty />
{!clientId && <ThirdParty />}
{settings.enableAdmMess && <RecoverAccess />}
{settings.enabledJoin && (
{settings.enabledJoin && !clientId && (
<Register
id="login_register"
enabledJoin

View File

@ -1,228 +0,0 @@
import React from "react";
import { useNavigate, useLocation } from "react-router-dom";
import styled from "styled-components";
import { useTranslation, Trans } from "react-i18next";
import api from "@docspace/shared/api";
import ScopeList from "@docspace/shared/utils/oauth/ScopeList";
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
import {
Avatar,
AvatarRole,
AvatarSize,
} from "@docspace/shared/components/avatar";
import { Base } from "@docspace/shared/themes";
import OAuthClientInfo from "./oauth-client-info";
import { deleteCookie, setCookie } from "@docspace/shared/utils/cookie";
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.oauth.infoDialog.separatorColor};
.block {
height: 40px;
display: flex;
align-items: center;
gap: 8px;
}
}
`;
StyledFormWrapper.defaultProps = { theme: Base };
interface IConsentProps {
oauth: IOAuthState;
theme: IUserTheme;
hashSettings: null | PasswordHashType;
setHashSettings: (hashSettings: PasswordHashType | null) => void;
setIsConsentScreen: (value: boolean) => void;
}
const Consent = ({
oauth,
theme,
setIsConsentScreen,
hashSettings,
setHashSettings,
}: IConsentProps) => {
const navigate = useNavigate();
const location = useLocation();
const { t } = useTranslation(["Consent", "Common"]);
const onAllowClick = async () => {
const clientId = oauth.clientId;
let clientState = "";
const scope = oauth.client.scopes;
await api.oauth.onOAuthLogin(clientId);
const cookie = document.cookie.split(";");
cookie.forEach((c) => {
if (c.includes("client_state"))
clientState = c.replace("client_state=", "").trim();
});
deleteCookie("client_state");
await api.oauth.onOAuthSubmit(clientId, clientState, scope);
};
const onDenyClick = async () => {
const clientId = oauth.clientId;
let clientState = "";
await api.oauth.onOAuthLogin(clientId);
const cookie = document.cookie.split(";");
cookie.forEach((c) => {
if (c.includes("client_state"))
clientState = c.replace("client_state=", "").trim();
});
deleteCookie("client_state");
await api.oauth.onOAuthCancel(clientId, clientState);
};
const onChangeUserClick = async () => {
await api.user.logout();
if (!hashSettings) {
const portalSettings = await api.settings.getSettings();
setHashSettings(portalSettings.passwordHash);
}
setIsConsentScreen(false);
navigate(`/login/${location.search}`);
};
return (
<StyledFormWrapper id={"consent"}>
<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={ButtonSize.normal}
scale
primary
/>
<Button
onClick={onDenyClick}
label={"Deny"}
size={ButtonSize.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={LinkType.page}
isHovered={false}
href={oauth.client.policyUrl}
target={LinkTarget.blank}
noHover
>
privacy policy
</Link>
and
<Link
className={"login-link"}
type={LinkType.page}
isHovered={false}
href={oauth.client.termsUrl}
target={LinkTarget.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={AvatarSize.min}
role={AvatarRole.user}
source={oauth.self?.avatarSmall || ""}
/>
<div className="user-info">
<Text lineHeight={"20px"}>
{t("SignedInAs")} {oauth.self?.email}
</Text>
<Link
className={"login-link"}
type={LinkType.action}
isHovered={false}
noHover
lineHeight={"20px"}
onClick={onChangeUserClick}
>
{t("NotYou")}
</Link>
</div>
</div>
</div>
</StyledFormWrapper>
);
};
export default Consent;

View File

@ -0,0 +1,257 @@
/* eslint-disable @next/next/no-img-element */
// (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 React from "react";
import styled from "styled-components";
import { useTranslation, Trans } from "react-i18next";
import ScopeList from "@docspace/shared/utils/oauth/ScopeList";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
import {
Avatar,
AvatarRole,
AvatarSize,
} from "@docspace/shared/components/avatar";
import { deleteCookie } from "@docspace/shared/utils/cookie";
import { IClientProps, IScope } from "@docspace/shared/utils/oauth/interfaces";
import { TUser } from "@docspace/shared/api/people/types";
import api from "@docspace/shared/api";
import OAuthClientInfo from "./ConsentInfo";
import { useRouter } from "next/navigation";
const StyledButtonContainer = styled.div`
margin-top: 32px;
margin-bottom: 16px;
width: 100%;
display: flex;
flex-direction: row;
gap: 8px;
`;
const StyledDescriptionContainer = styled.div`
width: 100%;
margin-bottom: 16px;
p {
width: 100%;
}
`;
const StyledUserContainer = styled.div`
width: 100%;
padding-top: 16px;
border-top: 1px solid
${(props) => props.theme.oauth.infoDialog.separatorColor};
.block {
height: 40px;
display: flex;
align-items: center;
gap: 8px;
}
`;
interface IConsentProps {
client: IClientProps;
scopes: IScope[];
user: TUser;
}
const Consent = ({ client, scopes, user }: IConsentProps) => {
const { t } = useTranslation(["Consent", "Common"]);
const router = useRouter();
const [isAllowRunning, setIsAllowRunning] = React.useState(false);
const [isDenyRunning, setIsDenyRunning] = React.useState(false);
const onAllowClick = async () => {
if (!("clientId" in client)) return;
if (isAllowRunning || isDenyRunning) return;
setIsAllowRunning(true);
const clientId = client.clientId;
let clientState = "";
console.log(clientState);
const scope = client.scopes;
const cookie = document.cookie.split(";");
cookie.forEach((c) => {
if (c.includes("client_state"))
clientState = c.replace("client_state=", "").trim();
});
deleteCookie("client_state");
console.log(clientState);
await api.oauth.onOAuthSubmit(clientId, clientState, scope);
setIsAllowRunning(false);
};
const onDenyClick = async () => {
if (!("clientId" in client)) return;
if (isAllowRunning || isDenyRunning) return;
setIsDenyRunning(true);
const clientId = client.clientId;
let clientState = "";
// await api.oauth.onOAuthLogin(clientId);
const cookie = document.cookie.split(";");
cookie.forEach((c) => {
if (c.includes("client_state"))
clientState = c.replace("client_state=", "").trim();
});
deleteCookie("client_state");
await api.oauth.onOAuthCancel(clientId, clientState);
setIsDenyRunning(false);
};
const onChangeUserClick = async () => {
await api.user.logout();
router.push(`/?clientId=${client.clientId}`);
};
return (
<>
<OAuthClientInfo
name={client.name}
logo={client.logo}
websiteUrl={client.websiteUrl}
isConsentScreen
/>
<ScopeList
t={t}
selectedScopes={client.scopes || []}
scopes={scopes || []}
/>
<StyledButtonContainer>
<Button
onClick={onAllowClick}
label={"Allow"}
size={ButtonSize.normal}
scale
primary
isDisabled={isDenyRunning}
isLoading={isAllowRunning}
/>
<Button
onClick={onDenyClick}
label={"Deny"}
size={ButtonSize.normal}
scale
isDisabled={isAllowRunning}
isLoading={isDenyRunning}
/>
</StyledButtonContainer>
<StyledDescriptionContainer>
<Text fontWeight={400} fontSize={"13px"} lineHeight={"20px"}>
<Trans t={t} i18nKey={"ConsentDescription"} ns="Consent">
Data shared with {{ displayName: self.displayName }} will be
governed by {{ nameApp: client.name }}
<Link
className={"login-link"}
type={LinkType.page}
isHovered={false}
href={client.policyUrl}
target={LinkTarget.blank}
noHover
>
privacy policy
</Link>
and
<Link
className={"login-link"}
type={LinkType.page}
isHovered={false}
href={client.termsUrl}
target={LinkTarget.blank}
noHover
>
terms of service
</Link>
. You can revoke this consent at any time in your DocSpace account
settings.
</Trans>
</Text>
</StyledDescriptionContainer>
<StyledUserContainer>
<div className="block">
<Avatar
size={AvatarSize.min}
role={AvatarRole.user}
source={user.avatarSmall || ""}
/>
<div className="user-info">
<Text lineHeight={"20px"}>
{t("SignedInAs")} {user.email}
</Text>
<Link
className={"login-link"}
type={LinkType.action}
isHovered={false}
noHover
lineHeight={"20px"}
onClick={onChangeUserClick}
>
{t("NotYou")}
</Link>
</div>
</div>
</StyledUserContainer>
</>
);
};
export default Consent;

View File

@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import React from "react";
import styled from "styled-components";
import { Trans } from "react-i18next";
import { Trans, useTranslation } from "react-i18next";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkTarget, LinkType } from "@docspace/shared/components/link";
@ -38,7 +39,6 @@ interface IOAuthClientInfo {
websiteUrl: string;
isConsentScreen?: boolean;
t: any;
}
const OAuthClientInfo = ({
@ -47,25 +47,27 @@ const OAuthClientInfo = ({
websiteUrl,
isConsentScreen,
t,
}: IOAuthClientInfo) => {
const { t } = useTranslation(["Consent", "Common"]);
return (
<StyledOAuthContainer>
{!isConsentScreen && (
<Text
className={"row"}
className="row"
fontWeight={600}
fontSize={"16px"}
lineHeight={"22px"}
fontSize="16px"
lineHeight="22px"
>
{isConsentScreen ? <>{t("Consent")}</> : <>{t("Common:LoginButton")}</>}
{t("Common:LoginButton")}
</Text>
)}
<img src={logo} alt={"client-logo"} />
<Text
className={"row"}
className="row"
fontWeight={isConsentScreen ? 400 : 600}
fontSize={"16px"}
lineHeight={"22px"}
fontSize="16px"
lineHeight="22px"
>
{isConsentScreen ? (
<Trans t={t} i18nKey={"ConsentSubHeader"} ns="Consent">
@ -77,7 +79,7 @@ const OAuthClientInfo = ({
target={LinkTarget.blank}
noHover
fontWeight={600}
fontSize={"16px"}
fontSize="16px"
>
{name}
</Link>{" "}
@ -95,7 +97,7 @@ const OAuthClientInfo = ({
target={LinkTarget.blank}
noHover
fontWeight={600}
fontSize={"16px"}
fontSize="16px"
>
{name}
</Link>

View File

@ -58,11 +58,14 @@ import ForgotContainer from "./sub-components/ForgotContainer";
import { StyledCaptcha } from "./LoginForm.styled";
import { LoginDispatchContext, LoginValueContext } from "../Login";
import OAuthClientInfo from "../ConsentInfo";
const LoginForm = ({
hashSettings,
cookieSettingsEnabled,
reCaptchaPublicKey,
clientId,
client,
}: LoginFormProps) => {
const { isLoading, isModalOpen } = useContext(LoginValueContext);
const { setIsLoading } = useContext(LoginDispatchContext);
@ -334,8 +337,18 @@ const LoginForm = ({
const passwordErrorMessage = errorMessage();
console.log(client);
return (
<form className="auth-form-container">
{client && (
<OAuthClientInfo
name={client.name}
logo={client.logo}
websiteUrl={client.websiteUrl}
/>
)}
<EmailContainer
emailFromInvitation={emailFromInvitation}
isEmailErrorShow={isEmailErrorShow}
@ -346,7 +359,6 @@ const LoginForm = ({
onBlurEmail={onBlurEmail}
onValidateEmail={onValidateEmail}
/>
<PasswordContainer
isLoading={isLoading}
emailFromInvitation={emailFromInvitation}
@ -355,14 +367,12 @@ const LoginForm = ({
password={password}
onChangePassword={onChangePassword}
/>
<ForgotContainer
cookieSettingsEnabled={cookieSettingsEnabled}
isChecked={isChecked}
identifier={identifier}
onChangeCheckbox={onChangeCheckbox}
/>
{reCaptchaPublicKey && isCaptcha && (
<StyledCaptcha isCaptchaError={isCaptchaError}>
<div className="captcha-wrapper">
@ -378,7 +388,6 @@ const LoginForm = ({
)}
</StyledCaptcha>
)}
<Button
id="login_submit"
className="login-button"

View File

@ -40,6 +40,24 @@ export function middleware(request: NextRequest) {
}
const isAuth = !!request.cookies.get("asc_auth_key")?.value;
const isOAuth = request.nextUrl.searchParams.get("type") === "oauth2";
if (isOAuth) {
const oauthClientId =
request.nextUrl.searchParams.get("client_id") ??
request.nextUrl.searchParams.get("clientId");
if (oauthClientId === "error")
return NextResponse.redirect(`${redirectUrl}/login/error`);
if (isAuth) {
if (request.nextUrl.pathname === "/consent") return;
return NextResponse.redirect(
`${redirectUrl}/login/consent${request.nextUrl.search}`,
);
}
}
const url = request.nextUrl.clone();
url.pathname = "/";
@ -49,5 +67,5 @@ export function middleware(request: NextRequest) {
// See "Matching Paths" below to learn more
export const config = {
matcher: ["/health", "/", "/not-found"],
matcher: ["/health", "/", "/not-found", "/consent"],
};

View File

@ -34,6 +34,7 @@ import {
} from "@docspace/shared/api/settings/types";
import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types";
import { ThemeKeys } from "@docspace/shared/enums";
import { INoAuthClientProps } from "@docspace/shared/utils/oauth/interfaces";
export type TDataContext = {
settings?: TSettings;
@ -85,6 +86,8 @@ export type LoginFormProps = {
hashSettings?: TPasswordHash;
reCaptchaPublicKey?: string;
cookieSettingsEnabled: boolean;
clientId?: string;
client?: INoAuthClientProps;
};
export type ForgotPasswordModalDialogProps = {

View File

@ -26,10 +26,10 @@
"use server";
import { cookies } from "next/headers";
import { headers } from "next/headers";
import { createRequest } from "@docspace/shared/utils/next-ssr-helper";
import { TUser } from "@docspace/shared/api/people/types";
import {
TCapabilities,
TGetColorTheme,
@ -38,7 +38,11 @@ import {
TThirdPartyProvider,
TVersionBuild,
} from "@docspace/shared/api/settings/types";
import { TenantStatus } from "@docspace/shared/enums";
import {
INoAuthClientProps,
IScope,
} from "@docspace/shared/utils/oauth/interfaces";
import { transformToClientProps } from "@docspace/shared/utils/oauth";
export const checkIsAuthenticated = async () => {
const [request] = createRequest(["/authentication"], [["", ""]], "GET");
@ -143,3 +147,67 @@ export async function getSSO() {
return sso.response as TGetSsoSettings;
}
export async function getUser() {
const hdrs = headers();
const cookie = hdrs.get("cookie");
const [getUser] = createRequest([`/people/@self`], [["", ""]], "GET");
if (!cookie?.includes("asc_auth_key")) return undefined;
const userRes = await fetch(getUser);
if (userRes.status === 401) return undefined;
if (!userRes.ok) return;
const user = await userRes.json();
return user.response as TUser;
}
export async function getScopeList() {
const [getScopeList] = createRequest([`/scopes`], [["", ""]], "GET");
const scopeList = await fetch(getScopeList);
if (!scopeList.ok) return;
const scopes = await scopeList.json();
return scopes as IScope[];
}
export async function getOAuthClient(clientId: string, isAuth = true) {
if (!isAuth) {
const [getOAuthClient] = createRequest(
[`/clients/${clientId}/info`],
[["", ""]],
"GET",
);
const oauthClient = await fetch(getOAuthClient);
console.log(oauthClient);
if (!oauthClient.ok) return;
const client = (await oauthClient.json()) as INoAuthClientProps;
return client;
}
const [getOAuthClient] = createRequest(
[`/clients/${clientId}`],
[["", ""]],
"GET",
);
const oauthClient = await fetch(getOAuthClient);
if (!oauthClient.ok) return;
const client = await oauthClient.json();
return transformToClientProps(client);
}

View File

@ -1,5 +1,5 @@
{
"date": "2024528_131959",
"date": "2024529_112928",
"checksums": {
"api.js": "0efbae3383bf6c6b6f26d573eee164d2",
"api.poly.js": "2a2ac2c0e4a7007b61d2d1ff7b00a22e",

View File

@ -21,11 +21,10 @@ export const getClient = async (
const client = (await request({
method: "get",
url: `/clients/${clientId}/info`,
})) as IClientResDTO;
})) as INoAuthClientProps;
return {
...client,
websiteUrl: client?.website_url || "",
};
}
@ -127,9 +126,11 @@ export const getScopeList = async (): Promise<IScope[]> => {
export const onOAuthLogin = (clientId: string) => {
const formData = new FormData();
formData.set("client_id", clientId);
return request({
method: "post",
url: `/oauth2/login?client_id=${clientId}`,
url: `/oauth2/login?clientId=${clientId}`,
data: formData,
withRedirect: true,
headers: {
@ -148,6 +149,8 @@ export const onOAuthSubmit = (
formData.append("client_id", clientId);
formData.append("state", clientState);
// return;
scope.forEach((s) => {
formData.append("scope", s);
});

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use client";
import React, { useState } from "react";
import { useTheme } from "styled-components";

View File

@ -3324,7 +3324,7 @@ const Dark: TTheme = {
infoDialog: {
descLinkColor: "#adadad",
blockHeaderColor: "#858585",
separatorColor: "#ffffff",
separatorColor: "#474747",
},
list: {
descriptionColor: "#858585",

View File

@ -187,6 +187,7 @@ class AxiosClient {
skipRedirect = false,
) => {
const onSuccess = (response: TRes) => {
console.log("suc");
const error = this.getResponseError(response);
if (error) throw new Error(error);
@ -194,6 +195,8 @@ class AxiosClient {
if (response.headers["x-redirect-uri"] && options.withRedirect) {
const redirectUri = response.headers["x-redirect-uri"];
console.log("call");
if (typeof redirectUri === "string")
return window.location.replace(redirectUri);
}

View File

@ -70,6 +70,8 @@ export const createRequest = (
if (baseURL && process.env.API_HOST?.trim()) hdrs.set("origin", baseURL);
hdrs.set("x-docspace-address", baseURL);
const urls = paths.map((path) => `${apiURL}${path}`);
const requests = urls.map(