diff options
Diffstat (limited to 'web/src')
| -rw-r--r-- | web/src/components/PrivacyPolicyDrawer.tsx | 54 | ||||
| -rw-r--r-- | web/src/components/PrivacyPolicyLink.tsx | 22 | ||||
| -rw-r--r-- | web/src/hooks/PersistentStorage.ts | 60 | ||||
| -rw-r--r-- | web/src/layouts/LoginLayout.tsx | 36 | ||||
| -rw-r--r-- | web/src/setupTests.js | 2 | ||||
| -rw-r--r-- | web/src/utils/Configuration.ts | 12 |
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"); } |
