Login: fix oauth pages
This commit is contained in:
parent
996c79d617
commit
5e70e6fee5
@ -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("");
|
||||
|
@ -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("");
|
||||
|
@ -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>
|
||||
|
@ -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 `${
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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?"
|
||||
}
|
||||
|
52
packages/login/src/app/(root)/consent/page.tsx
Normal file
52
packages/login/src/app/(root)/consent/page.tsx
Normal 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;
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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;
|
257
packages/login/src/components/Consent.tsx
Normal file
257
packages/login/src/components/Consent.tsx
Normal 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;
|
@ -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>
|
@ -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"
|
||||
|
@ -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"],
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"date": "2024528_131959",
|
||||
"date": "2024529_112928",
|
||||
"checksums": {
|
||||
"api.js": "0efbae3383bf6c6b6f26d573eee164d2",
|
||||
"api.poly.js": "2a2ac2c0e4a7007b61d2d1ff7b00a22e",
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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";
|
||||
|
||||
|
@ -3324,7 +3324,7 @@ const Dark: TTheme = {
|
||||
infoDialog: {
|
||||
descLinkColor: "#adadad",
|
||||
blockHeaderColor: "#858585",
|
||||
separatorColor: "#ffffff",
|
||||
separatorColor: "#474747",
|
||||
},
|
||||
list: {
|
||||
descriptionColor: "#858585",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user