diff --git a/config.template.yml b/config.template.yml index 125653bd6..e953ad7fe 100644 --- a/config.template.yml +++ b/config.template.yml @@ -8,6 +8,9 @@ port: 9091 # tls_key: /config/ssl/key.pem # tls_cert: /config/ssl/cert.pem +# The theme to display: light, dark, grey +theme: light + # Configuration options specific to the internal http server server: # Buffers usually should be configured to be the same value. diff --git a/docs/configuration/theme.md b/docs/configuration/theme.md new file mode 100644 index 000000000..3d1e88fc5 --- /dev/null +++ b/docs/configuration/theme.md @@ -0,0 +1,22 @@ +--- +layout: default +title: Theme +parent: Configuration +nav_order: 11 +--- + +# Theme + +The theme section configures the theme and style Authelia uses. + +There are currently 3 available themes for Authelia: +* light (default) +* dark +* grey + +## Configuration + +```yaml +# The theme to display: light, dark, grey +theme: light +``` \ No newline at end of file diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go index 92601a656..040b64a79 100644 --- a/internal/configuration/schema/configuration.go +++ b/internal/configuration/schema/configuration.go @@ -4,6 +4,7 @@ package schema type Configuration struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` + Theme string `mapstructure:"theme"` TLSCert string `mapstructure:"tls_cert"` TLSKey string `mapstructure:"tls_key"` CertificatesDirectory string `mapstructure:"certificates_directory"` diff --git a/internal/configuration/validator/access_control_test.go b/internal/configuration/validator/access_control_test.go index 34bf057ac..c57e46a19 100644 --- a/internal/configuration/validator/access_control_test.go +++ b/internal/configuration/validator/access_control_test.go @@ -29,7 +29,7 @@ func (suite *AccessControl) TestShouldValidateCompleteConfiguration() { } func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() { - suite.configuration.DefaultPolicy = "invalid" + suite.configuration.DefaultPolicy = testInvalidPolicy ValidateAccessControl(suite.configuration, suite.validator) @@ -71,7 +71,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() { suite.configuration.Rules = []schema.ACLRule{ { Domains: []string{"public.example.com"}, - Policy: "invalid", + Policy: testInvalidPolicy, }, } diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go index 90145d515..ea60e42df 100644 --- a/internal/configuration/validator/configuration.go +++ b/internal/configuration/validator/configuration.go @@ -52,6 +52,12 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem } } + if configuration.Theme == "" { + configuration.Theme = "light" + } + + ValidateTheme(configuration, validator) + if configuration.TOTP == nil { configuration.TOTP = &schema.DefaultTOTPConfiguration } diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 88a8c44b0..87242ab95 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -9,6 +9,7 @@ var validKeys = []string{ "log_file_path", "default_redirection_url", "jwt_secret", + "theme", "tls_key", "tls_cert", "certificates_directory", @@ -177,6 +178,7 @@ const schemeLDAP = "ldap" const schemeLDAPS = "ldaps" const testBadTimer = "-1" +const testInvalidPolicy = "invalid" const testJWTSecret = "a_secret" const testLDAPBaseDN = "base_dn" const testLDAPPassword = "password" diff --git a/internal/configuration/validator/theme.go b/internal/configuration/validator/theme.go new file mode 100644 index 000000000..a3f12e014 --- /dev/null +++ b/internal/configuration/validator/theme.go @@ -0,0 +1,16 @@ +package validator + +import ( + "fmt" + "regexp" + + "github.com/authelia/authelia/internal/configuration/schema" +) + +// ValidateTheme validates and update Theme configuration. +func ValidateTheme(configuration *schema.Configuration, validator *schema.StructValidator) { + validThemes := regexp.MustCompile("light|dark|grey") + if !validThemes.MatchString(configuration.Theme) { + validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\" or \"grey\"", configuration.Theme)) + } +} diff --git a/internal/configuration/validator/theme_test.go b/internal/configuration/validator/theme_test.go new file mode 100644 index 000000000..054f61247 --- /dev/null +++ b/internal/configuration/validator/theme_test.go @@ -0,0 +1,44 @@ +package validator + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/authelia/authelia/internal/configuration/schema" +) + +type Theme struct { + suite.Suite + configuration *schema.Configuration + validator *schema.StructValidator +} + +func (suite *Theme) SetupTest() { + suite.validator = schema.NewStructValidator() + suite.configuration = &schema.Configuration{ + Theme: "light", + } +} + +func (suite *Theme) TestShouldValidateCompleteConfiguration() { + ValidateTheme(suite.configuration, suite.validator) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) +} + +func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() { + suite.configuration.Theme = "invalid" + + ValidateTheme(suite.configuration, suite.validator) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Theme: invalid is not valid, valid themes are: \"light\", \"dark\" or \"grey\"") +} + +func TestThemes(t *testing.T) { + suite.Run(t, new(Theme)) +} diff --git a/internal/server/server.go b/internal/server/server.go index 05b0af3a4..660071a41 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -33,9 +33,9 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi rootFiles := []string{"favicon.ico", "manifest.json", "robots.txt"} - serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword) - serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword) - serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.Path, configuration.Session.Name, rememberMe, resetPassword) + serveIndexHandler := ServeTemplatedFile(embeddedAssets, indexFile, configuration.Server.Path, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme) + serveSwaggerHandler := ServeTemplatedFile(swaggerAssets, indexFile, configuration.Server.Path, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme) + serveSwaggerAPIHandler := ServeTemplatedFile(swaggerAssets, apiFile, configuration.Server.Path, rememberMe, resetPassword, configuration.Session.Name, configuration.Theme) r := router.New() r.GET("/", serveIndexHandler) diff --git a/internal/server/template.go b/internal/server/template.go index 6717ebcc2..7153cd8c1 100644 --- a/internal/server/template.go +++ b/internal/server/template.go @@ -19,7 +19,7 @@ var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV // this is utilised to pass information between the backend and frontend // and generate a nonce to support a restrictive CSP while using material-ui. //go:generate broccoli -src ../../public_html -o public_html -func ServeTemplatedFile(publicDir, file, base, session, rememberMe, resetPassword string) fasthttp.RequestHandler { +func ServeTemplatedFile(publicDir, file, base, rememberMe, resetPassword, session, theme string) fasthttp.RequestHandler { logger := logging.Logger() f, err := br.Open(publicDir + file) @@ -56,7 +56,7 @@ func ServeTemplatedFile(publicDir, file, base, session, rememberMe, resetPasswor ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self' ; object-src 'none'; style-src 'self' 'nonce-%s'", nonce)) } - err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, CSPNonce, Session, RememberMe, ResetPassword string }{Base: base, CSPNonce: nonce, Session: session, RememberMe: rememberMe, ResetPassword: resetPassword}) + err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ Base, CSPNonce, RememberMe, ResetPassword, Session, Theme string }{Base: base, CSPNonce: nonce, RememberMe: rememberMe, ResetPassword: resetPassword, Session: session, Theme: theme}) if err != nil { ctx.Error("An error occurred", 503) logger.Errorf("Unable to execute template: %v", err) diff --git a/internal/suites/ActiveDirectory/configuration.yml b/internal/suites/ActiveDirectory/configuration.yml index cfe729102..24fddbdba 100644 --- a/internal/suites/ActiveDirectory/configuration.yml +++ b/internal/suites/ActiveDirectory/configuration.yml @@ -6,6 +6,8 @@ port: 9091 tls_cert: /config/ssl/cert.pem tls_key: /config/ssl/key.pem +theme: grey + log_level: debug default_redirection_url: https://home.example.com:8080/ diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml index c9a9f728c..bda70ba86 100644 --- a/internal/suites/LDAP/configuration.yml +++ b/internal/suites/LDAP/configuration.yml @@ -6,6 +6,8 @@ port: 9091 tls_cert: /config/ssl/cert.pem tls_key: /config/ssl/key.pem +theme: dark + log_level: debug default_redirection_url: https://home.example.com:8080/ diff --git a/web/.env.development b/web/.env.development index dc50b8c79..9dcec55d1 100644 --- a/web/.env.development +++ b/web/.env.development @@ -1,4 +1,5 @@ HOST=authelia-frontend PUBLIC_URL="" REACT_APP_REMEMBER_ME=true -REACT_APP_RESET_PASSWORD=true \ No newline at end of file +REACT_APP_RESET_PASSWORD=true +REACT_APP_THEME=light \ No newline at end of file diff --git a/web/.env.production b/web/.env.production index 1f6321076..2c5e2cd67 100644 --- a/web/.env.production +++ b/web/.env.production @@ -1,3 +1,4 @@ PUBLIC_URL={{.Base}} REACT_APP_REMEMBER_ME={{.RememberMe}} -REACT_APP_RESET_PASSWORD={{.ResetPassword}} \ No newline at end of file +REACT_APP_RESET_PASSWORD={{.ResetPassword}} +REACT_APP_THEME={{.Theme}} \ No newline at end of file diff --git a/web/public/index.html b/web/public/index.html index 16a5d184e..2d1407d6f 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -25,7 +25,7 @@