feat(web): privacy policy url (#4625)
This allows users to customize a privacy policy URL at the bottom of the login view. Closes #2639pull/4816/head
parent
df52b1b4c4
commit
a566c16d08
|
@ -505,7 +505,6 @@ authentication_backend:
|
|||
# variant: standard
|
||||
# cost: 12
|
||||
|
||||
|
||||
##
|
||||
## Password Policy Configuration.
|
||||
##
|
||||
|
@ -540,6 +539,23 @@ password_policy:
|
|||
## Configures the minimum score allowed.
|
||||
min_score: 3
|
||||
|
||||
##
|
||||
## Privacy Policy Configuration
|
||||
##
|
||||
## Parameters used for displaying the privacy policy link and drawer.
|
||||
privacy_policy:
|
||||
|
||||
## Enables the display of the privacy policy using the policy_url.
|
||||
enabled: false
|
||||
|
||||
## Enables the display of the privacy policy drawer which requires users accept the privacy policy
|
||||
## on a per-browser basis.
|
||||
require_user_acceptance: false
|
||||
|
||||
## The URL of the privacy policy document. Must be an absolute URL and must have the 'https://' scheme.
|
||||
## If the privacy policy enabled option is true, this MUST be provided.
|
||||
policy_url: ''
|
||||
|
||||
##
|
||||
## Access Control Configuration
|
||||
##
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
title: "Privacy Policy"
|
||||
description: "Privacy Policy Configuration."
|
||||
lead: "This describes a section of the configuration for enabling a Privacy Policy link display."
|
||||
date: 2020-02-29T01:43:59+01:00
|
||||
draft: false
|
||||
images: []
|
||||
menu:
|
||||
configuration:
|
||||
parent: "miscellaneous"
|
||||
weight: 199100
|
||||
toc: true
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
privacy_policy:
|
||||
enabled: false
|
||||
require_user_acceptance: false
|
||||
policy_url: ''
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
### enabled
|
||||
|
||||
{{< confkey type="boolean" default="false" required="no" >}}
|
||||
|
||||
Enables the display of the Privacy Policy link.
|
||||
|
||||
### require_user_acceptance
|
||||
|
||||
{{< confkey type="boolean" default="false" required="no" >}}
|
||||
|
||||
Requires users accept per-browser the Privacy Policy via a Dialog Drawer at the bottom of the page. The fact they have
|
||||
accepted is recorded and checked in the browser
|
||||
[localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage).
|
||||
|
||||
If the user has not accepted the policy they should not be able to interact with the Authelia UI via normal means.
|
||||
|
||||
Administrators who are required to abide by the [GDPR] or other privacy laws should be advised that
|
||||
[OpenID Connect 1.0](../identity-providers/open-id-connect.md) clients configured with the `implicit` consent mode are
|
||||
unlikely to trigger the display of the Authelia UI if the user is already authenticated.
|
||||
|
||||
We wont be adding checks like this to the `implicit` consent mode when that mode in particular is unlikely to be
|
||||
compliant with those laws, and that mode is not strictly compliant with the OpenID Connect 1.0 specifications. It is
|
||||
therefore recommended if `require_user_acceptance` is enabled then administrators should avoid using the `implicit`
|
||||
consent mode or do so at their own risk.
|
||||
|
||||
### policy_url
|
||||
|
||||
{{< confkey type="string" required="situational" >}}
|
||||
|
||||
The privacy policy URL is a URL which optionally is displayed in the frontend linking users to the administrators
|
||||
privacy policy. This is useful for users who wish to abide by laws such as the [GDPR].
|
||||
Administrators can view the particulars of what _Authelia_ collects out of the box with our
|
||||
[Privacy Policy](https://www.authelia.com/privacy/#application).
|
||||
|
||||
This value must be an absolute URL, and must have the `https://` scheme.
|
||||
|
||||
This option is required if the [enabled](#enabled) option is true.
|
||||
|
||||
[GDPR]: https://gdpr-info.eu/
|
||||
|
||||
_**Example:**_
|
||||
|
||||
```yaml
|
||||
privacy_policy:
|
||||
enabled: true
|
||||
policy_url: 'https://www.example.com/privacy-policy'
|
||||
```
|
File diff suppressed because one or more lines are too long
|
@ -505,7 +505,6 @@ authentication_backend:
|
|||
# variant: standard
|
||||
# cost: 12
|
||||
|
||||
|
||||
##
|
||||
## Password Policy Configuration.
|
||||
##
|
||||
|
@ -540,6 +539,23 @@ password_policy:
|
|||
## Configures the minimum score allowed.
|
||||
min_score: 3
|
||||
|
||||
##
|
||||
## Privacy Policy Configuration
|
||||
##
|
||||
## Parameters used for displaying the privacy policy link and drawer.
|
||||
privacy_policy:
|
||||
|
||||
## Enables the display of the privacy policy using the policy_url.
|
||||
enabled: false
|
||||
|
||||
## Enables the display of the privacy policy drawer which requires users accept the privacy policy
|
||||
## on a per-browser basis.
|
||||
require_user_acceptance: false
|
||||
|
||||
## The URL of the privacy policy document. Must be an absolute URL and must have the 'https://' scheme.
|
||||
## If the privacy policy enabled option is true, this MUST be provided.
|
||||
policy_url: ''
|
||||
|
||||
##
|
||||
## Access Control Configuration
|
||||
##
|
||||
|
|
|
@ -23,4 +23,5 @@ type Configuration struct {
|
|||
Telemetry TelemetryConfig `koanf:"telemetry"`
|
||||
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
||||
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
|
||||
PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"`
|
||||
}
|
||||
|
|
|
@ -268,4 +268,7 @@ var Keys = []string{
|
|||
"password_policy.standard.require_special",
|
||||
"password_policy.zxcvbn.enabled",
|
||||
"password_policy.zxcvbn.min_score",
|
||||
"privacy_policy.enabled",
|
||||
"privacy_policy.require_user_acceptance",
|
||||
"privacy_policy.policy_url",
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// PrivacyPolicy is the privacy policy configuration.
|
||||
type PrivacyPolicy struct {
|
||||
Enabled bool `koanf:"enabled"`
|
||||
RequireUserAcceptance bool `koanf:"require_user_acceptance"`
|
||||
PolicyURL *url.URL `koanf:"policy_url"`
|
||||
}
|
|
@ -68,6 +68,8 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
|
|||
ValidateNTP(config, validator)
|
||||
|
||||
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
|
||||
|
||||
ValidatePrivacyPolicy(&config.PrivacyPolicy, validator)
|
||||
}
|
||||
|
||||
func validateDefault2FAMethod(config *schema.Configuration, validator *schema.StructValidator) {
|
||||
|
|
|
@ -294,22 +294,17 @@ const (
|
|||
errFmtPasswordPolicyZXCVBNMinScoreInvalid = "password_policy: zxcvbn: option 'min_score' is invalid: must be between 1 and 4 but it's configured as %d"
|
||||
)
|
||||
|
||||
const (
|
||||
errPrivacyPolicyEnabledWithoutURL = "privacy_policy: option 'policy_url' must be provided when the option 'enabled' is true"
|
||||
errFmtPrivacyPolicyURLNotHTTPS = "privacy_policy: option 'policy_url' must have the 'https' scheme but it's configured as '%s'"
|
||||
)
|
||||
|
||||
const (
|
||||
errFmtDuoMissingOption = "duo_api: option '%s' is required when duo is enabled but it is missing"
|
||||
)
|
||||
|
||||
// Error constants.
|
||||
const (
|
||||
/*
|
||||
errFmtDeprecatedConfigurationKey = "the %s configuration option is deprecated and will be " +
|
||||
"removed in %s, please use %s instead"
|
||||
|
||||
Uncomment for use when deprecating keys.
|
||||
|
||||
TODO: Create a method from within Koanf to automatically remap deprecated keys and produce warnings.
|
||||
TODO (cont): The main consideration is making sure we do not overwrite the destination key name if it already exists.
|
||||
*/
|
||||
|
||||
errFmtInvalidDefault2FAMethod = "option 'default_2fa_method' is configured as '%s' but must be one of " +
|
||||
"the following values: '%s'"
|
||||
errFmtInvalidDefault2FAMethodDisabled = "option 'default_2fa_method' is configured as '%s' " +
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// ValidatePasswordPolicy validates and update Password Policy configuration.
|
||||
// ValidatePasswordPolicy validates and updates the Password Policy configuration.
|
||||
func ValidatePasswordPolicy(config *schema.PasswordPolicyConfiguration, validator *schema.StructValidator) {
|
||||
if !utils.IsBoolCountLessThanN(1, true, config.Standard.Enabled, config.ZXCVBN.Enabled) {
|
||||
validator.Push(fmt.Errorf(errPasswordPolicyMultipleDefined))
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
// ValidatePrivacyPolicy validates and updates the Privacy Policy configuration.
|
||||
func ValidatePrivacyPolicy(config *schema.PrivacyPolicy, validator *schema.StructValidator) {
|
||||
if !config.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
switch config.PolicyURL {
|
||||
case nil:
|
||||
validator.Push(fmt.Errorf(errPrivacyPolicyEnabledWithoutURL))
|
||||
default:
|
||||
if config.PolicyURL.Scheme != schemeHTTPS {
|
||||
validator.Push(fmt.Errorf(errFmtPrivacyPolicyURLNotHTTPS, config.PolicyURL.Scheme))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
|
||||
func TestValidatePrivacyPolicy(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
have schema.PrivacyPolicy
|
||||
expected string
|
||||
}{
|
||||
{"ShouldValidateDefaultConfig", schema.PrivacyPolicy{}, ""},
|
||||
{"ShouldValidateValidEnabledPolicy", schema.PrivacyPolicy{Enabled: true, PolicyURL: MustParseURL("https://example.com/privacy")}, ""},
|
||||
{"ShouldValidateValidEnabledPolicyWithUserAcceptance", schema.PrivacyPolicy{Enabled: true, RequireUserAcceptance: true, PolicyURL: MustParseURL("https://example.com/privacy")}, ""},
|
||||
{"ShouldNotValidateOnInvalidScheme", schema.PrivacyPolicy{Enabled: true, PolicyURL: MustParseURL("http://example.com/privacy")}, "privacy_policy: option 'policy_url' must have the 'https' scheme but it's configured as 'http'"},
|
||||
{"ShouldNotValidateOnMissingURL", schema.PrivacyPolicy{Enabled: true}, "privacy_policy: option 'policy_url' must be provided when the option 'enabled' is true"},
|
||||
}
|
||||
|
||||
validator := schema.NewStructValidator()
|
||||
|
||||
for _, tc := range testCases {
|
||||
validator.Clear()
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ValidatePrivacyPolicy(&tc.have, validator)
|
||||
|
||||
assert.Len(t, validator.Warnings(), 0)
|
||||
|
||||
if tc.expected == "" {
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
} else {
|
||||
assert.EqualError(t, validator.Errors()[0], tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@
|
|||
"Password": "Password",
|
||||
"Passwords do not match": "Passwords do not match.",
|
||||
"Powered by": "Powered by",
|
||||
"Privacy Policy": "Privacy Policy",
|
||||
"Push Notification": "Push Notification",
|
||||
"Register device": "Register device",
|
||||
"Register your first device by clicking on the link below": "Register your first device by clicking on the link below.",
|
||||
|
@ -67,6 +68,7 @@
|
|||
"Use OpenID to verify your identity": "Use OpenID to verify your identity",
|
||||
"Username": "Username",
|
||||
"You must open the link from the same device and browser that initiated the registration process": "You must open the link from the same device and browser that initiated the registration process",
|
||||
"You must view and accept the Privacy Policy before using": "You must view and accept the <0>Privacy Policy</0> before using",
|
||||
"You're being signed out and redirected": "You're being signed out and redirected",
|
||||
"Your supplied password does not meet the password policy requirements": "Your supplied password does not meet the password policy requirements."
|
||||
}
|
||||
|
|
|
@ -212,6 +212,11 @@ func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileO
|
|||
EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil),
|
||||
}
|
||||
|
||||
if config.PrivacyPolicy.Enabled {
|
||||
opts.PrivacyPolicyURL = config.PrivacyPolicy.PolicyURL.String()
|
||||
opts.PrivacyPolicyAccept = strconv.FormatBool(config.PrivacyPolicy.RequireUserAcceptance)
|
||||
}
|
||||
|
||||
if !config.DuoAPI.Disable {
|
||||
opts.DuoSelfEnrollment = strconv.FormatBool(config.DuoAPI.EnableSelfEnrollment)
|
||||
}
|
||||
|
@ -226,6 +231,8 @@ type TemplatedFileOptions struct {
|
|||
RememberMe string
|
||||
ResetPassword string
|
||||
ResetPasswordCustomURL string
|
||||
PrivacyPolicyURL string
|
||||
PrivacyPolicyAccept string
|
||||
Session string
|
||||
Theme string
|
||||
|
||||
|
@ -251,6 +258,8 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri
|
|||
RememberMe: options.RememberMe,
|
||||
ResetPassword: options.ResetPassword,
|
||||
ResetPasswordCustomURL: options.ResetPasswordCustomURL,
|
||||
PrivacyPolicyURL: options.PrivacyPolicyURL,
|
||||
PrivacyPolicyAccept: options.PrivacyPolicyAccept,
|
||||
Session: options.Session,
|
||||
Theme: options.Theme,
|
||||
}
|
||||
|
@ -298,6 +307,8 @@ type TemplatedFileCommonData struct {
|
|||
RememberMe string
|
||||
ResetPassword string
|
||||
ResetPasswordCustomURL string
|
||||
PrivacyPolicyURL string
|
||||
PrivacyPolicyAccept string
|
||||
Session string
|
||||
Theme string
|
||||
}
|
||||
|
|
|
@ -4,4 +4,6 @@ VITE_DUO_SELF_ENROLLMENT={{ .DuoSelfEnrollment }}
|
|||
VITE_REMEMBER_ME={{ .RememberMe }}
|
||||
VITE_RESET_PASSWORD={{ .ResetPassword }}
|
||||
VITE_RESET_PASSWORD_CUSTOM_URL={{ .ResetPasswordCustomURL }}
|
||||
VITE_PRIVACY_POLICY_URL={{ .PrivacyPolicyURL }}
|
||||
VITE_PRIVACY_POLICY_ACCEPT={{ .PrivacyPolicyAccept }}
|
||||
VITE_THEME={{ .Theme }}
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
data-rememberme="%VITE_REMEMBER_ME%"
|
||||
data-resetpassword="%VITE_RESET_PASSWORD%"
|
||||
data-resetpasswordcustomurl="%VITE_RESET_PASSWORD_CUSTOM_URL%"
|
||||
data-privacypolicyurl="%VITE_PRIVACY_POLICY_URL%"
|
||||
data-privacypolicyaccept="%VITE_PRIVACY_POLICY_ACCEPT%"
|
||||
data-theme="%VITE_THEME%"
|
||||
>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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],
|
||||
},
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue