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
|
# variant: standard
|
||||||
# cost: 12
|
# cost: 12
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## Password Policy Configuration.
|
## Password Policy Configuration.
|
||||||
##
|
##
|
||||||
|
@ -540,6 +539,23 @@ password_policy:
|
||||||
## Configures the minimum score allowed.
|
## Configures the minimum score allowed.
|
||||||
min_score: 3
|
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
|
## 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
|
# variant: standard
|
||||||
# cost: 12
|
# cost: 12
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## Password Policy Configuration.
|
## Password Policy Configuration.
|
||||||
##
|
##
|
||||||
|
@ -540,6 +539,23 @@ password_policy:
|
||||||
## Configures the minimum score allowed.
|
## Configures the minimum score allowed.
|
||||||
min_score: 3
|
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
|
## Access Control Configuration
|
||||||
##
|
##
|
||||||
|
|
|
@ -23,4 +23,5 @@ type Configuration struct {
|
||||||
Telemetry TelemetryConfig `koanf:"telemetry"`
|
Telemetry TelemetryConfig `koanf:"telemetry"`
|
||||||
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
Webauthn WebauthnConfiguration `koanf:"webauthn"`
|
||||||
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
|
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
|
||||||
|
PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -268,4 +268,7 @@ var Keys = []string{
|
||||||
"password_policy.standard.require_special",
|
"password_policy.standard.require_special",
|
||||||
"password_policy.zxcvbn.enabled",
|
"password_policy.zxcvbn.enabled",
|
||||||
"password_policy.zxcvbn.min_score",
|
"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)
|
ValidateNTP(config, validator)
|
||||||
|
|
||||||
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
|
ValidatePasswordPolicy(&config.PasswordPolicy, validator)
|
||||||
|
|
||||||
|
ValidatePrivacyPolicy(&config.PrivacyPolicy, validator)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateDefault2FAMethod(config *schema.Configuration, validator *schema.StructValidator) {
|
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"
|
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 (
|
const (
|
||||||
errFmtDuoMissingOption = "duo_api: option '%s' is required when duo is enabled but it is missing"
|
errFmtDuoMissingOption = "duo_api: option '%s' is required when duo is enabled but it is missing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error constants.
|
// Error constants.
|
||||||
const (
|
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 " +
|
errFmtInvalidDefault2FAMethod = "option 'default_2fa_method' is configured as '%s' but must be one of " +
|
||||||
"the following values: '%s'"
|
"the following values: '%s'"
|
||||||
errFmtInvalidDefault2FAMethodDisabled = "option 'default_2fa_method' is configured as '%s' " +
|
errFmtInvalidDefault2FAMethodDisabled = "option 'default_2fa_method' is configured as '%s' " +
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"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) {
|
func ValidatePasswordPolicy(config *schema.PasswordPolicyConfiguration, validator *schema.StructValidator) {
|
||||||
if !utils.IsBoolCountLessThanN(1, true, config.Standard.Enabled, config.ZXCVBN.Enabled) {
|
if !utils.IsBoolCountLessThanN(1, true, config.Standard.Enabled, config.ZXCVBN.Enabled) {
|
||||||
validator.Push(fmt.Errorf(errPasswordPolicyMultipleDefined))
|
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",
|
"Password": "Password",
|
||||||
"Passwords do not match": "Passwords do not match.",
|
"Passwords do not match": "Passwords do not match.",
|
||||||
"Powered by": "Powered by",
|
"Powered by": "Powered by",
|
||||||
|
"Privacy Policy": "Privacy Policy",
|
||||||
"Push Notification": "Push Notification",
|
"Push Notification": "Push Notification",
|
||||||
"Register device": "Register device",
|
"Register device": "Register device",
|
||||||
"Register your first device by clicking on the link below": "Register your first device by clicking on the link below.",
|
"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",
|
"Use OpenID to verify your identity": "Use OpenID to verify your identity",
|
||||||
"Username": "Username",
|
"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 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",
|
"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."
|
"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),
|
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 {
|
if !config.DuoAPI.Disable {
|
||||||
opts.DuoSelfEnrollment = strconv.FormatBool(config.DuoAPI.EnableSelfEnrollment)
|
opts.DuoSelfEnrollment = strconv.FormatBool(config.DuoAPI.EnableSelfEnrollment)
|
||||||
}
|
}
|
||||||
|
@ -226,6 +231,8 @@ type TemplatedFileOptions struct {
|
||||||
RememberMe string
|
RememberMe string
|
||||||
ResetPassword string
|
ResetPassword string
|
||||||
ResetPasswordCustomURL string
|
ResetPasswordCustomURL string
|
||||||
|
PrivacyPolicyURL string
|
||||||
|
PrivacyPolicyAccept string
|
||||||
Session string
|
Session string
|
||||||
Theme string
|
Theme string
|
||||||
|
|
||||||
|
@ -251,6 +258,8 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri
|
||||||
RememberMe: options.RememberMe,
|
RememberMe: options.RememberMe,
|
||||||
ResetPassword: options.ResetPassword,
|
ResetPassword: options.ResetPassword,
|
||||||
ResetPasswordCustomURL: options.ResetPasswordCustomURL,
|
ResetPasswordCustomURL: options.ResetPasswordCustomURL,
|
||||||
|
PrivacyPolicyURL: options.PrivacyPolicyURL,
|
||||||
|
PrivacyPolicyAccept: options.PrivacyPolicyAccept,
|
||||||
Session: options.Session,
|
Session: options.Session,
|
||||||
Theme: options.Theme,
|
Theme: options.Theme,
|
||||||
}
|
}
|
||||||
|
@ -298,6 +307,8 @@ type TemplatedFileCommonData struct {
|
||||||
RememberMe string
|
RememberMe string
|
||||||
ResetPassword string
|
ResetPassword string
|
||||||
ResetPasswordCustomURL string
|
ResetPasswordCustomURL string
|
||||||
|
PrivacyPolicyURL string
|
||||||
|
PrivacyPolicyAccept string
|
||||||
Session string
|
Session string
|
||||||
Theme string
|
Theme string
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,6 @@ VITE_DUO_SELF_ENROLLMENT={{ .DuoSelfEnrollment }}
|
||||||
VITE_REMEMBER_ME={{ .RememberMe }}
|
VITE_REMEMBER_ME={{ .RememberMe }}
|
||||||
VITE_RESET_PASSWORD={{ .ResetPassword }}
|
VITE_RESET_PASSWORD={{ .ResetPassword }}
|
||||||
VITE_RESET_PASSWORD_CUSTOM_URL={{ .ResetPasswordCustomURL }}
|
VITE_RESET_PASSWORD_CUSTOM_URL={{ .ResetPasswordCustomURL }}
|
||||||
|
VITE_PRIVACY_POLICY_URL={{ .PrivacyPolicyURL }}
|
||||||
|
VITE_PRIVACY_POLICY_ACCEPT={{ .PrivacyPolicyAccept }}
|
||||||
VITE_THEME={{ .Theme }}
|
VITE_THEME={{ .Theme }}
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
data-rememberme="%VITE_REMEMBER_ME%"
|
data-rememberme="%VITE_REMEMBER_ME%"
|
||||||
data-resetpassword="%VITE_RESET_PASSWORD%"
|
data-resetpassword="%VITE_RESET_PASSWORD%"
|
||||||
data-resetpasswordcustomurl="%VITE_RESET_PASSWORD_CUSTOM_URL%"
|
data-resetpasswordcustomurl="%VITE_RESET_PASSWORD_CUSTOM_URL%"
|
||||||
|
data-privacypolicyurl="%VITE_PRIVACY_POLICY_URL%"
|
||||||
|
data-privacypolicyaccept="%VITE_PRIVACY_POLICY_ACCEPT%"
|
||||||
data-theme="%VITE_THEME%"
|
data-theme="%VITE_THEME%"
|
||||||
>
|
>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<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 { grey } from "@mui/material/colors";
|
||||||
import makeStyles from "@mui/styles/makeStyles";
|
import makeStyles from "@mui/styles/makeStyles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { ReactComponent as UserSvg } from "@assets/images/user.svg";
|
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 TypographyWithTooltip from "@components/TypographyWithTootip";
|
||||||
import { getLogoOverride } from "@utils/Configuration";
|
import { getLogoOverride, getPrivacyPolicyEnabled } from "@utils/Configuration";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -23,15 +25,20 @@ const url = "https://www.authelia.com";
|
||||||
|
|
||||||
const LoginLayout = function (props: Props) {
|
const LoginLayout = function (props: Props) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
const logo = getLogoOverride() ? (
|
const logo = getLogoOverride() ? (
|
||||||
<img src="./static/media/logo.png" alt="Logo" className={styles.icon} />
|
<img src="./static/media/logo.png" alt="Logo" className={styles.icon} />
|
||||||
) : (
|
) : (
|
||||||
<UserSvg className={styles.icon} />
|
<UserSvg className={styles.icon} />
|
||||||
);
|
);
|
||||||
const { t: translate } = useTranslation();
|
|
||||||
|
const privacyEnabled = getPrivacyPolicyEnabled();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${translate("Login")} - Authelia`;
|
document.title = `${translate("Login")} - Authelia`;
|
||||||
}, [translate]);
|
}, [translate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid id={props.id} className={styles.root} container spacing={0} alignItems="center" justifyContent="center">
|
<Grid id={props.id} className={styles.root} container spacing={0} alignItems="center" justifyContent="center">
|
||||||
<Container maxWidth="xs" className={styles.rootContainer}>
|
<Container maxWidth="xs" className={styles.rootContainer}>
|
||||||
|
@ -57,14 +64,25 @@ const LoginLayout = function (props: Props) {
|
||||||
{props.children}
|
{props.children}
|
||||||
</Grid>
|
</Grid>
|
||||||
{props.showBrand ? (
|
{props.showBrand ? (
|
||||||
<Grid item xs={12}>
|
<Grid item container xs={12} alignItems="center" justifyContent="center">
|
||||||
<Link href={url} target="_blank" underline="hover" className={styles.poweredBy}>
|
<Grid item xs={4}>
|
||||||
{translate("Powered by")} Authelia
|
<Link href={url} target="_blank" underline="hover" className={styles.footerLinks}>
|
||||||
</Link>
|
{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>
|
</Grid>
|
||||||
) : null}
|
) : null}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
|
<PrivacyPolicyDrawer />
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -92,7 +110,7 @@ const useStyles = makeStyles((theme: Theme) => ({
|
||||||
paddingTop: theme.spacing(),
|
paddingTop: theme.spacing(),
|
||||||
paddingBottom: theme.spacing(),
|
paddingBottom: theme.spacing(),
|
||||||
},
|
},
|
||||||
poweredBy: {
|
footerLinks: {
|
||||||
fontSize: "0.7em",
|
fontSize: "0.7em",
|
||||||
color: grey[500],
|
color: grey[500],
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,4 +5,6 @@ document.body.setAttribute("data-duoselfenrollment", "true");
|
||||||
document.body.setAttribute("data-rememberme", "true");
|
document.body.setAttribute("data-rememberme", "true");
|
||||||
document.body.setAttribute("data-resetpassword", "true");
|
document.body.setAttribute("data-resetpassword", "true");
|
||||||
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
document.body.setAttribute("data-resetpasswordcustomurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyurl", "");
|
||||||
|
document.body.setAttribute("data-privacypolicyaccept", "false");
|
||||||
document.body.setAttribute("data-theme", "light");
|
document.body.setAttribute("data-theme", "light");
|
||||||
|
|
|
@ -27,6 +27,18 @@ export function getResetPasswordCustomURL() {
|
||||||
return getEmbeddedVariable("resetpasswordcustomurl");
|
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() {
|
export function getTheme() {
|
||||||
return getEmbeddedVariable("theme");
|
return getEmbeddedVariable("theme");
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue