summaryrefslogtreecommitdiff
path: root/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'web/src')
-rw-r--r--web/src/components/PrivacyPolicyDrawer.tsx54
-rw-r--r--web/src/components/PrivacyPolicyLink.tsx22
-rw-r--r--web/src/hooks/PersistentStorage.ts60
-rw-r--r--web/src/layouts/LoginLayout.tsx36
-rw-r--r--web/src/setupTests.js2
-rw-r--r--web/src/utils/Configuration.ts12
6 files changed, 177 insertions, 9 deletions
diff --git a/web/src/components/PrivacyPolicyDrawer.tsx b/web/src/components/PrivacyPolicyDrawer.tsx
new file mode 100644
index 000000000..dcdb363b5
--- /dev/null
+++ b/web/src/components/PrivacyPolicyDrawer.tsx
@@ -0,0 +1,54 @@
+import { Button, Drawer, DrawerProps, Grid, Typography } from "@mui/material";
+import { Trans, useTranslation } from "react-i18next";
+
+import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
+import { usePersistentStorageValue } from "@hooks/PersistentStorage";
+import { getPrivacyPolicyEnabled, getPrivacyPolicyRequireAccept } from "@utils/Configuration";
+
+const PrivacyPolicyDrawer = function (props: DrawerProps) {
+ const privacyEnabled = getPrivacyPolicyEnabled();
+ const privacyRequireAccept = getPrivacyPolicyRequireAccept();
+ const [accepted, setAccepted] = usePersistentStorageValue<boolean>("privacy-policy-accepted", false);
+ const { t: translate } = useTranslation();
+
+ return privacyEnabled && privacyRequireAccept && !accepted ? (
+ <Drawer {...props} anchor="bottom" open={!accepted}>
+ <Grid
+ container
+ alignItems="center"
+ justifyContent="center"
+ textAlign="center"
+ aria-labelledby="privacy-policy-drawer-title"
+ aria-describedby="privacy-policy-drawer-description"
+ >
+ <Grid container item xs={12} paddingY={2}>
+ <Grid item xs={12}>
+ <Typography id="privacy-policy-drawer-title" variant="h6" component="h2">
+ {translate("Privacy Policy")}
+ </Typography>
+ </Grid>
+ </Grid>
+ <Grid item xs={12}>
+ <Typography id="privacy-policy-drawer-description">
+ <Trans
+ i18nKey="You must view and accept the Privacy Policy before using"
+ components={[<PrivacyPolicyLink />]}
+ />{" "}
+ Authelia.
+ </Typography>
+ </Grid>
+ <Grid item xs={12} paddingY={2}>
+ <Button
+ onClick={() => {
+ setAccepted(true);
+ }}
+ >
+ {translate("Accept")}
+ </Button>
+ </Grid>
+ </Grid>
+ </Drawer>
+ ) : null;
+};
+
+export default PrivacyPolicyDrawer;
diff --git a/web/src/components/PrivacyPolicyLink.tsx b/web/src/components/PrivacyPolicyLink.tsx
new file mode 100644
index 000000000..c9b82fc77
--- /dev/null
+++ b/web/src/components/PrivacyPolicyLink.tsx
@@ -0,0 +1,22 @@
+import React, { Fragment } from "react";
+
+import { Link, LinkProps } from "@mui/material";
+import { useTranslation } from "react-i18next";
+
+import { getPrivacyPolicyURL } from "@utils/Configuration";
+
+const PrivacyPolicyLink = function (props: LinkProps) {
+ const hrefPrivacyPolicy = getPrivacyPolicyURL();
+
+ const { t: translate } = useTranslation();
+
+ return (
+ <Fragment>
+ <Link {...props} href={hrefPrivacyPolicy} target="_blank" rel="noopener" underline="hover">
+ {translate("Privacy Policy")}
+ </Link>
+ </Fragment>
+ );
+};
+
+export default PrivacyPolicyLink;
diff --git a/web/src/hooks/PersistentStorage.ts b/web/src/hooks/PersistentStorage.ts
new file mode 100644
index 000000000..ce129a271
--- /dev/null
+++ b/web/src/hooks/PersistentStorage.ts
@@ -0,0 +1,60 @@
+import { useEffect, useState } from "react";
+
+interface PersistentStorage {
+ getItem(key: string): string | null;
+ setItem(key: string, value: any): void;
+}
+
+class LocalStorage implements PersistentStorage {
+ getItem(key: string) {
+ const item = localStorage.getItem(key);
+
+ if (item === null) return undefined;
+
+ if (item === "null") return null;
+ if (item === "undefined") return undefined;
+
+ try {
+ return JSON.parse(item);
+ } catch {}
+
+ return item;
+ }
+ setItem(key: string, value: any) {
+ if (value === undefined) {
+ localStorage.removeItem(key);
+ } else {
+ localStorage.setItem(key, JSON.stringify(value));
+ }
+ }
+}
+
+class MockStorage implements PersistentStorage {
+ getItem() {
+ return null;
+ }
+ setItem() {}
+}
+
+const persistentStorage = window?.localStorage ? new LocalStorage() : new MockStorage();
+
+export function usePersistentStorageValue<T>(key: string, initialValue?: T) {
+ const [value, setValue] = useState<T>(() => {
+ const valueFromStorage = persistentStorage.getItem(key);
+
+ if (typeof initialValue === "object" && !Array.isArray(initialValue) && initialValue !== null) {
+ return {
+ ...initialValue,
+ ...valueFromStorage,
+ };
+ }
+
+ return valueFromStorage || initialValue;
+ });
+
+ useEffect(() => {
+ persistentStorage.setItem(key, value);
+ }, [key, value]);
+
+ return [value, setValue] as const;
+}
diff --git a/web/src/layouts/LoginLayout.tsx b/web/src/layouts/LoginLayout.tsx
index 9dd15e09d..71ce30ecc 100644
--- a/web/src/layouts/LoginLayout.tsx
+++ b/web/src/layouts/LoginLayout.tsx
@@ -1,13 +1,15 @@
-import React, { ReactNode, useEffect } from "react";
+import React, { Fragment, ReactNode, useEffect } from "react";
-import { Container, Grid, Link, Theme } from "@mui/material";
+import { Container, Divider, Grid, Link, Theme } from "@mui/material";
import { grey } from "@mui/material/colors";
import makeStyles from "@mui/styles/makeStyles";
import { useTranslation } from "react-i18next";
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
+import PrivacyPolicyDrawer from "@components/PrivacyPolicyDrawer";
+import PrivacyPolicyLink from "@components/PrivacyPolicyLink";
import TypographyWithTooltip from "@components/TypographyWithTootip";
-import { getLogoOverride } from "@utils/Configuration";
+import { getLogoOverride, getPrivacyPolicyEnabled } from "@utils/Configuration";
export interface Props {
id?: string;
@@ -23,15 +25,20 @@ const url = "https://www.authelia.com";
const LoginLayout = function (props: Props) {
const styles = useStyles();
+ const { t: translate } = useTranslation();
+
const logo = getLogoOverride() ? (
<img src="./static/media/logo.png" alt="Logo" className={styles.icon} />
) : (
<UserSvg className={styles.icon} />
);
- const { t: translate } = useTranslation();
+
+ const privacyEnabled = getPrivacyPolicyEnabled();
+
useEffect(() => {
document.title = `${translate("Login")} - Authelia`;
}, [translate]);
+
return (
<Grid id={props.id} className={styles.root} container spacing={0} alignItems="center" justifyContent="center">
<Container maxWidth="xs" className={styles.rootContainer}>
@@ -57,14 +64,25 @@ const LoginLayout = function (props: Props) {
{props.children}
</Grid>
{props.showBrand ? (
- <Grid item xs={12}>
- <Link href={url} target="_blank" underline="hover" className={styles.poweredBy}>
- {translate("Powered by")} Authelia
- </Link>
+ <Grid item container xs={12} alignItems="center" justifyContent="center">
+ <Grid item xs={4}>
+ <Link href={url} target="_blank" underline="hover" className={styles.footerLinks}>
+ {translate("Powered by")} Authelia
+ </Link>
+ </Grid>
+ {privacyEnabled ? (
+ <Fragment>
+ <Divider orientation="vertical" flexItem variant="middle" />
+ <Grid item xs={4}>
+ <PrivacyPolicyLink className={styles.footerLinks} />
+ </Grid>
+ </Fragment>
+ ) : null}
</Grid>
) : null}
</Grid>
</Container>
+ <PrivacyPolicyDrawer />
</Grid>
);
};
@@ -92,7 +110,7 @@ const useStyles = makeStyles((theme: Theme) => ({
paddingTop: theme.spacing(),
paddingBottom: theme.spacing(),
},
- poweredBy: {
+ footerLinks: {
fontSize: "0.7em",
color: grey[500],
},
diff --git a/web/src/setupTests.js b/web/src/setupTests.js
index 1c5931ad7..e6067ae8d 100644
--- a/web/src/setupTests.js
+++ b/web/src/setupTests.js
@@ -5,4 +5,6 @@ document.body.setAttribute("data-duoselfenrollment", "true");
document.body.setAttribute("data-rememberme", "true");
document.body.setAttribute("data-resetpassword", "true");
document.body.setAttribute("data-resetpasswordcustomurl", "");
+document.body.setAttribute("data-privacypolicyurl", "");
+document.body.setAttribute("data-privacypolicyaccept", "false");
document.body.setAttribute("data-theme", "light");
diff --git a/web/src/utils/Configuration.ts b/web/src/utils/Configuration.ts
index 25ff419d3..dcae6f690 100644
--- a/web/src/utils/Configuration.ts
+++ b/web/src/utils/Configuration.ts
@@ -27,6 +27,18 @@ export function getResetPasswordCustomURL() {
return getEmbeddedVariable("resetpasswordcustomurl");
}
+export function getPrivacyPolicyEnabled() {
+ return getEmbeddedVariable("privacypolicyurl") !== "";
+}
+
+export function getPrivacyPolicyURL() {
+ return getEmbeddedVariable("privacypolicyurl");
+}
+
+export function getPrivacyPolicyRequireAccept() {
+ return getEmbeddedVariable("privacypolicyaccept") === "true";
+}
+
export function getTheme() {
return getEmbeddedVariable("theme");
}