diff --git a/config.template.yml b/config.template.yml
index 4d15f6096..55d157dd2 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -26,6 +26,11 @@ jwt_secret: a_very_important_secret
# be redirected upon successful authentication.
default_redirection_url: https://home.example.com:8080/
+# Google Analytics Tracking ID to track the usage of the portal
+# using a Google Analytics dashboard.
+#
+## google_analytics: UA-00000-01
+
# TOTP Issuer Name
#
# This will be the issuer name displayed in Google Authenticator
diff --git a/example/compose/authelia/Dockerfile.backend b/example/compose/authelia/Dockerfile.backend
index 5847101b6..bd42297d2 100644
--- a/example/compose/authelia/Dockerfile.backend
+++ b/example/compose/authelia/Dockerfile.backend
@@ -7,4 +7,11 @@ ARG GROUP_ID
RUN addgroup --gid ${GROUP_ID} dev && \
adduser --uid ${USER_ID} -G dev -D dev
-USER dev
\ No newline at end of file
+
+RUN mkdir -p /etc/authelia && chown dev:dev /etc/authelia
+RUN mkdir -p /var/lib/authelia && chown dev:dev /var/lib/authelia
+
+USER dev
+
+VOLUME /etc/authelia
+VOLUME /var/lib/authelia
diff --git a/example/compose/authelia/docker-compose.backend-dist.yml b/example/compose/authelia/docker-compose.backend-dist.yml
index 6b5a3ef64..da6148082 100644
--- a/example/compose/authelia/docker-compose.backend-dist.yml
+++ b/example/compose/authelia/docker-compose.backend-dist.yml
@@ -4,8 +4,6 @@ services:
build:
context: .
dockerfile: Dockerfile
- volumes:
- - "/tmp/authelia:/tmp/authelia"
environment:
- ENVIRONMENT=dev
restart: always
diff --git a/example/compose/authelia/docker-compose.backend.yml b/example/compose/authelia/docker-compose.backend.yml
index 0bab39b6d..2b9bf9ed0 100644
--- a/example/compose/authelia/docker-compose.backend.yml
+++ b/example/compose/authelia/docker-compose.backend.yml
@@ -13,7 +13,6 @@ services:
- "./example/compose/authelia/resources/:/resources"
- ".:/app"
- "${GOPATH}:/go"
- - "/tmp/authelia:/tmp/authelia"
environment:
- ENVIRONMENT=dev
networks:
diff --git a/example/compose/authelia/resources/entrypoint.sh b/example/compose/authelia/resources/entrypoint.sh
index e31d1459f..e5aceee14 100755
--- a/example/compose/authelia/resources/entrypoint.sh
+++ b/example/compose/authelia/resources/entrypoint.sh
@@ -4,9 +4,6 @@ set -x
go get github.com/cespare/reflex
-mkdir -p /var/lib/authelia
-mkdir -p /etc/authelia
-
# Sleep 10 seconds to wait the end of npm install updating web directory
# and making reflex reload multiple times.
sleep 10
diff --git a/example/compose/nginx/portal/docker-compose.yml b/example/compose/nginx/portal/docker-compose.yml
index ac74a1ee6..b387d7f3e 100644
--- a/example/compose/nginx/portal/docker-compose.yml
+++ b/example/compose/nginx/portal/docker-compose.yml
@@ -5,8 +5,6 @@ services:
volumes:
- ./example/compose/nginx/portal/nginx.conf:/etc/nginx/nginx.conf
- ./example/compose/nginx/portal/ssl:/etc/ssl
- ports:
- - "8080:8080"
networks:
authelianet:
aliases:
diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go
index 3035fcc20..31e5119c4 100644
--- a/internal/configuration/schema/configuration.go
+++ b/internal/configuration/schema/configuration.go
@@ -2,10 +2,12 @@ package schema
// Configuration object extracted from YAML configuration file.
type Configuration struct {
- Port int `yaml:"port"`
- LogsLevel string `yaml:"logs_level"`
- JWTSecret string `yaml:"jwt_secret"`
- DefaultRedirectionURL string `yaml:"default_redirection_url"`
+ Port int `yaml:"port"`
+ LogsLevel string `yaml:"logs_level"`
+ JWTSecret string `yaml:"jwt_secret"`
+ DefaultRedirectionURL string `yaml:"default_redirection_url"`
+ GoogleAnalyticsTrackingID string `yaml:"google_analytics"`
+
AuthenticationBackend AuthenticationBackendConfiguration `yaml:"authentication_backend"`
Session SessionConfiguration `yaml:"session"`
diff --git a/internal/handlers/handler_2fa_available_methods.go b/internal/handlers/handler_2fa_available_methods.go
deleted file mode 100644
index 72ee789df..000000000
--- a/internal/handlers/handler_2fa_available_methods.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package handlers
-
-import (
- "github.com/clems4ever/authelia/internal/authentication"
- "github.com/clems4ever/authelia/internal/middlewares"
-)
-
-// SecondFactorAvailableMethodsGet retrieve available 2FA methods.
-// The supported methods are: "totp", "u2f", "duo"
-func SecondFactorAvailableMethodsGet(ctx *middlewares.AutheliaCtx) {
- availableMethods := MethodList{authentication.TOTP, authentication.U2F}
-
- if ctx.Configuration.DuoAPI != nil {
- availableMethods = append(availableMethods, authentication.Push)
- }
-
- ctx.Logger.Debugf("Available methods are %s", availableMethods)
- ctx.SetJSONBody(availableMethods)
-}
diff --git a/internal/handlers/handler_configuration.go b/internal/handlers/handler_configuration.go
new file mode 100644
index 000000000..1a96148e2
--- /dev/null
+++ b/internal/handlers/handler_configuration.go
@@ -0,0 +1,14 @@
+package handlers
+
+import "github.com/clems4ever/authelia/internal/middlewares"
+
+type ConfigurationBody struct {
+ GoogleAnalyticsTrackingID string `json:"ga_tracking_id,omitempty"`
+}
+
+func ConfigurationGet(ctx *middlewares.AutheliaCtx) {
+ body := ConfigurationBody{
+ GoogleAnalyticsTrackingID: ctx.Configuration.GoogleAnalyticsTrackingID,
+ }
+ ctx.SetJSONBody(body)
+}
diff --git a/internal/handlers/handler_configuration_test.go b/internal/handlers/handler_configuration_test.go
new file mode 100644
index 000000000..e7532802f
--- /dev/null
+++ b/internal/handlers/handler_configuration_test.go
@@ -0,0 +1,32 @@
+package handlers
+
+import (
+ "github.com/clems4ever/authelia/internal/mocks"
+ "github.com/stretchr/testify/suite"
+)
+
+type ConfigurationSuite struct {
+ suite.Suite
+
+ mock *mocks.MockAutheliaCtx
+}
+
+func (s *ConfigurationSuite) SetupTest() {
+ s.mock = mocks.NewMockAutheliaCtx(s.T())
+}
+
+func (s *ConfigurationSuite) TearDownTest() {
+ s.mock.Close()
+}
+
+func (s *ConfigurationSuite) TestShouldReturnConfiguredGATrackingID() {
+ GATrackingID := "ABC"
+ s.mock.Ctx.Configuration.GoogleAnalyticsTrackingID = GATrackingID
+
+ expectedBody := ConfigurationBody{
+ GoogleAnalyticsTrackingID: GATrackingID,
+ }
+
+ ConfigurationGet(s.mock.Ctx)
+ s.mock.Assert200OK(s.T(), expectedBody)
+}
diff --git a/internal/handlers/handler_extended_configuration.go b/internal/handlers/handler_extended_configuration.go
new file mode 100644
index 000000000..bd32d6eb9
--- /dev/null
+++ b/internal/handlers/handler_extended_configuration.go
@@ -0,0 +1,23 @@
+package handlers
+
+import (
+ "github.com/clems4ever/authelia/internal/authentication"
+ "github.com/clems4ever/authelia/internal/middlewares"
+)
+
+type ExtendedConfigurationBody struct {
+ AvailableMethods MethodList `json:"available_methods"`
+}
+
+// ExtendedConfigurationGet get the extended configuration accessbile to authenticated users.
+func ExtendedConfigurationGet(ctx *middlewares.AutheliaCtx) {
+ body := ExtendedConfigurationBody{}
+ body.AvailableMethods = MethodList{authentication.TOTP, authentication.U2F}
+
+ if ctx.Configuration.DuoAPI != nil {
+ body.AvailableMethods = append(body.AvailableMethods, authentication.Push)
+ }
+
+ ctx.Logger.Debugf("Available methods are %s", body.AvailableMethods)
+ ctx.SetJSONBody(body)
+}
diff --git a/internal/handlers/handler_2fa_available_methods_test.go b/internal/handlers/handler_extended_configuration_test.go
similarity index 70%
rename from internal/handlers/handler_2fa_available_methods_test.go
rename to internal/handlers/handler_extended_configuration_test.go
index 1552d80fe..c0cd06c12 100644
--- a/internal/handlers/handler_2fa_available_methods_test.go
+++ b/internal/handlers/handler_extended_configuration_test.go
@@ -11,7 +11,6 @@ import (
type SecondFactorAvailableMethodsFixture struct {
suite.Suite
-
mock *mocks.MockAutheliaCtx
}
@@ -24,16 +23,22 @@ func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
- SecondFactorAvailableMethodsGet(s.mock.Ctx)
- s.mock.Assert200OK(s.T(), []string{"totp", "u2f"})
+ expectedBody := ExtendedConfigurationBody{
+ AvailableMethods: []string{"totp", "u2f"},
+ }
+ ExtendedConfigurationGet(s.mock.Ctx)
+ s.mock.Assert200OK(s.T(), expectedBody)
}
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndMobilePush() {
s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: &schema.DuoAPIConfiguration{},
}
- SecondFactorAvailableMethodsGet(s.mock.Ctx)
- s.mock.Assert200OK(s.T(), []string{"totp", "u2f", "mobile_push"})
+ expectedBody := ExtendedConfigurationBody{
+ AvailableMethods: []string{"totp", "u2f", "mobile_push"},
+ }
+ ExtendedConfigurationGet(s.mock.Ctx)
+ s.mock.Assert200OK(s.T(), expectedBody)
}
func TestRunSuite(t *testing.T) {
diff --git a/internal/server/server.go b/internal/server/server.go
index 05def5cc4..7229a621c 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -32,6 +32,10 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.GET("/api/state", autheliaMiddleware(handlers.StateGet))
+ router.GET("/api/configuration", autheliaMiddleware(handlers.ConfigurationGet))
+ router.GET("/api/configuration/extended", autheliaMiddleware(
+ middlewares.RequireFirstFactor(handlers.ExtendedConfigurationGet)))
+
router.GET("/api/verify", autheliaMiddleware(handlers.VerifyGet))
router.POST("/api/firstfactor", autheliaMiddleware(handlers.FirstFactorPost))
@@ -45,10 +49,6 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
router.POST("/api/reset-password", autheliaMiddleware(
handlers.ResetPasswordPost))
- // 2FA preferences and settings related endpoints.
- router.GET("/api/secondfactor/available", autheliaMiddleware(
- middlewares.RequireFirstFactor(handlers.SecondFactorAvailableMethodsGet)))
-
// Information about the user
router.GET("/api/user/info", autheliaMiddleware(
middlewares.RequireFirstFactor(handlers.UserInfoGet)))
diff --git a/internal/suites/BypassAll/configuration.yml b/internal/suites/BypassAll/configuration.yml
index 4fd638e3e..6b04336de 100644
--- a/internal/suites/BypassAll/configuration.yml
+++ b/internal/suites/BypassAll/configuration.yml
@@ -20,7 +20,7 @@ session:
storage:
local:
- path: /tmp/authelia/db.sqlite
+ path: /var/lib/authelia/db.sqlite
# The Duo Push Notification API configuration
duo_api:
diff --git a/internal/suites/Docker/configuration.yml b/internal/suites/Docker/configuration.yml
index e3f1cb636..adbb6982b 100644
--- a/internal/suites/Docker/configuration.yml
+++ b/internal/suites/Docker/configuration.yml
@@ -22,7 +22,7 @@ session:
storage:
local:
- path: /tmp/authelia/db.sqlite3
+ path: /var/lib/authelia/db.sqlite3
totp:
issuer: example.com
diff --git a/internal/suites/DuoPush/configuration.yml b/internal/suites/DuoPush/configuration.yml
index 18debd100..bff5891f7 100644
--- a/internal/suites/DuoPush/configuration.yml
+++ b/internal/suites/DuoPush/configuration.yml
@@ -21,7 +21,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
- path: /tmp/authelia/db.sqlite
+ path: /var/lib/authelia/db.sqlite
# TOTP Issuer Name
#
diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml
index 8d11fb07c..80b988864 100644
--- a/internal/suites/LDAP/configuration.yml
+++ b/internal/suites/LDAP/configuration.yml
@@ -57,7 +57,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
- path: /tmp/authelia/db.sqlite3
+ path: /var/lib/authelia/db.sqlite3
# TOTP Issuer Name
#
diff --git a/internal/suites/NetworkACL/configuration.yml b/internal/suites/NetworkACL/configuration.yml
index 35e67a513..760acc8b1 100644
--- a/internal/suites/NetworkACL/configuration.yml
+++ b/internal/suites/NetworkACL/configuration.yml
@@ -21,7 +21,7 @@ session:
# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
- path: /tmp/authelia/db.sqlite
+ path: /var/lib/authelia/db.sqlite
# Access Control
#
diff --git a/internal/suites/ShortTimeouts/configuration.yml b/internal/suites/ShortTimeouts/configuration.yml
index 1f0e36de7..f88ed2e79 100644
--- a/internal/suites/ShortTimeouts/configuration.yml
+++ b/internal/suites/ShortTimeouts/configuration.yml
@@ -20,24 +20,14 @@ session:
inactivity: 5
expiration: 8
-# Configuration of the storage backend used to store data and secrets. i.e. totp data
storage:
local:
- path: /tmp/authelia/db.sqlite
+ path: /var/lib/authelia/db.sqlite
-# TOTP Issuer Name
-#
-# This will be the issuer name displayed in Google Authenticator
-# See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format for more info on issuer names
totp:
issuer: example.com
-# Access Control
-#
-# Access control is a set of rules you can use to restrict user access to certain
-# resources.
access_control:
- # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
default_policy: deny
rules:
@@ -70,37 +60,12 @@ access_control:
subject: "user:bob"
policy: two_factor
-# Configuration of the authentication regulation mechanism.
regulation:
- # Set it to 0 to disable max_retries.
max_retries: 3
-
- # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
find_time: 3
-
- # The length of time before a banned user can login again.
ban_time: 5
-# Default redirection URL
-#
-# Note: this parameter is optional. If not provided, user won't
-# be redirected upon successful authentication.
-#default_redirection_url: https://authelia.example.domain
-
notifier:
- # For testing purpose, notifications can be sent in a file
- # filesystem:
- # filename: /tmp/authelia/notification.txt
-
- # Use your email account to send the notifications. You can use an app password.
- # List of valid services can be found here: https://nodemailer.com/smtp/well-known/
- ## email:
- ## username: user@example.com
- ## password: yourpassword
- ## sender: admin@example.com
- ## service: gmail
-
- # Use a SMTP server for sending notifications
smtp:
host: smtp
port: 1025
diff --git a/internal/suites/Standalone/docker-compose.yml b/internal/suites/Standalone/docker-compose.yml
index 9231bb69f..305ddfc42 100644
--- a/internal/suites/Standalone/docker-compose.yml
+++ b/internal/suites/Standalone/docker-compose.yml
@@ -4,3 +4,4 @@ services:
volumes:
- "./internal/suites/Standalone/configuration.yml:/etc/authelia/configuration.yml:ro"
- "./internal/suites/Standalone/users.yml:/var/lib/authelia/users.yml"
+ - "/tmp/authelia:/tmp/authelia"
diff --git a/internal/suites/Traefik/configuration.yml b/internal/suites/Traefik/configuration.yml
index fa6930ea5..201d95459 100644
--- a/internal/suites/Traefik/configuration.yml
+++ b/internal/suites/Traefik/configuration.yml
@@ -20,7 +20,7 @@ session:
storage:
local:
- path: /tmp/authelia/db.sqlite
+ path: /var/lib/authelia/db.sqlite
access_control:
default_policy: bypass
diff --git a/internal/suites/scenario_backend_protection_test.go b/internal/suites/scenario_backend_protection_test.go
index d990c4615..52d969e79 100644
--- a/internal/suites/scenario_backend_protection_test.go
+++ b/internal/suites/scenario_backend_protection_test.go
@@ -45,7 +45,10 @@ func (s *BackendProtectionScenario) TestProtectionOfBackendEndpoints() {
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/user/info/2fa_method", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/user/info", AutheliaBaseURL), 403)
- s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/secondfactor/available", AutheliaBaseURL), 403)
+ s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration/extended", AutheliaBaseURL), 403)
+
+ // This is the global configuration, it's safe to let it open.
+ s.AssertRequestStatusCode("GET", fmt.Sprintf("%s/api/configuration", AutheliaBaseURL), 200)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/start", AutheliaBaseURL), 403)
s.AssertRequestStatusCode("POST", fmt.Sprintf("%s/api/secondfactor/u2f/identity/finish", AutheliaBaseURL), 403)
diff --git a/internal/suites/scenario_two_factor_test.go b/internal/suites/scenario_two_factor_test.go
index 900aab575..e01aefe68 100644
--- a/internal/suites/scenario_two_factor_test.go
+++ b/internal/suites/scenario_two_factor_test.go
@@ -7,8 +7,6 @@ import (
"testing"
"time"
- "github.com/clems4ever/authelia/internal/storage"
- "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@@ -49,35 +47,6 @@ func (s *TwoFactorSuite) SetupTest() {
s.verifyIsHome(ctx, s.T())
}
-func (s *TwoFactorSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
- ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
- defer cancel()
-
- username := "john"
- password := "password"
-
- // Clean up any TOTP secret already in DB
- provider := storage.NewSQLiteProvider("/tmp/authelia/db.sqlite3")
- require.NoError(s.T(), provider.DeleteTOTPSecret(username))
-
- // Login one factor
- s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
-
- // Check the user is asked to register a new device
- s.WaitElementLocatedByClassName(ctx, s.T(), "state-not-registered")
-
- // Then register the TOTP factor
- s.doRegisterTOTP(ctx, s.T())
- // And logout
- s.doLogout(ctx, s.T())
-
- // Login one factor again
- s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
-
- // now the user should be asked to perform 2FA
- s.WaitElementLocatedByClassName(ctx, s.T(), "state-method")
-}
-
func (s *TwoFactorSuite) TestShouldAuthorizeSecretAfterTwoFactor() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
diff --git a/internal/suites/suite_high_availability_test.go b/internal/suites/suite_high_availability_test.go
index f19084bb6..478dc9a6e 100644
--- a/internal/suites/suite_high_availability_test.go
+++ b/internal/suites/suite_high_availability_test.go
@@ -49,15 +49,15 @@ func (s *HighAvailabilityWebDriverSuite) SetupTest() {
}
func (s *HighAvailabilityWebDriverSuite) TestShouldKeepUserDataInDB() {
- ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Second)
defer cancel()
secret := s.doRegisterThenLogout(ctx, s.T(), "john", "password")
err := haDockerEnvironment.Restart("mariadb")
- s.Assert().NoError(err)
+ s.Require().NoError(err)
- time.Sleep(2 * time.Second)
+ time.Sleep(20 * time.Second)
s.doLoginTwoFactor(ctx, s.T(), "john", "password", false, secret, "")
s.verifyIsSecondFactorPage(ctx, s.T())
@@ -229,6 +229,10 @@ func (s *HighAvailabilitySuite) TestHighAvailabilityWebDriverSuite() {
suite.Run(s.T(), NewHighAvailabilityWebDriverSuite())
}
+func TestHighAvailabilityWebDriverSuite(t *testing.T) {
+ suite.Run(t, NewHighAvailabilityWebDriverSuite())
+}
+
func TestHighAvailabilitySuite(t *testing.T) {
suite.Run(t, NewHighAvailabilitySuite())
}
diff --git a/internal/suites/suite_standalone.go b/internal/suites/suite_standalone.go
index 8e18b29e7..0306ebe65 100644
--- a/internal/suites/suite_standalone.go
+++ b/internal/suites/suite_standalone.go
@@ -53,7 +53,7 @@ func init() {
SetUpTimeout: 5 * time.Minute,
OnSetupTimeout: onSetupTimeout,
TearDown: teardown,
- TestTimeout: 2 * time.Minute,
+ TestTimeout: 3 * time.Minute,
TearDownTimeout: 2 * time.Minute,
Description: `This suite is used to test Authelia in a standalone
configuration with in-memory sessions and a local sqlite db stored on disk`,
diff --git a/internal/suites/suite_standalone_test.go b/internal/suites/suite_standalone_test.go
index 8265844a8..afe08122c 100644
--- a/internal/suites/suite_standalone_test.go
+++ b/internal/suites/suite_standalone_test.go
@@ -9,6 +9,8 @@ import (
"testing"
"time"
+ "github.com/clems4ever/authelia/internal/storage"
+ "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@@ -65,6 +67,35 @@ func (s *StandaloneWebDriverSuite) TestShouldLetUserKnowHeIsAlreadyAuthenticated
s.WaitElementLocatedByClassName(ctx, s.T(), "success-icon")
}
+func (s *StandaloneWebDriverSuite) TestShouldCheckUserIsAskedToRegisterDevice() {
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer cancel()
+
+ username := "john"
+ password := "password"
+
+ // Clean up any TOTP secret already in DB
+ provider := storage.NewSQLiteProvider("/tmp/authelia/db.sqlite3")
+ require.NoError(s.T(), provider.DeleteTOTPSecret(username))
+
+ // Login one factor
+ s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
+
+ // Check the user is asked to register a new device
+ s.WaitElementLocatedByClassName(ctx, s.T(), "state-not-registered")
+
+ // Then register the TOTP factor
+ s.doRegisterTOTP(ctx, s.T())
+ // And logout
+ s.doLogout(ctx, s.T())
+
+ // Login one factor again
+ s.doLoginOneFactor(ctx, s.T(), username, password, false, "")
+
+ // now the user should be asked to perform 2FA
+ s.WaitElementLocatedByClassName(ctx, s.T(), "state-method")
+}
+
type StandaloneSuite struct {
suite.Suite
}
diff --git a/web/package-lock.json b/web/package-lock.json
index 5b2609dac..96c623c06 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -1566,6 +1566,14 @@
"@types/react": "*"
}
},
+ "@types/react-ga": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react-ga/-/react-ga-2.3.0.tgz",
+ "integrity": "sha512-7Vkv6wH1Kem4vkjuJxRYxDgLfokm0shugDk0W5p9C28POrsPAXezLbgP5C2tyFZ7lKARdyrCxwkdRTC1UV0dHg==",
+ "requires": {
+ "react-ga": "*"
+ }
+ },
"@types/react-router": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.3.tgz",
@@ -11311,6 +11319,11 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.3.tgz",
"integrity": "sha512-bOUvMWFQVk5oz8Ded9Xb7WVdEi3QGLC8tH7HmYP0Fdp4Bn3qw0tRFmr5TW6mvahzvmrK4a6bqWGfCevBflP+Xw=="
},
+ "react-ga": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/react-ga/-/react-ga-2.7.0.tgz",
+ "integrity": "sha512-AjC7UOZMvygrWTc2hKxTDvlMXEtbmA0IgJjmkhgmQQ3RkXrWR11xEagLGFGaNyaPnmg24oaIiaNPnEoftUhfXA=="
+ },
"react-is": {
"version": "16.12.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
diff --git a/web/package.json b/web/package.json
index 45189a7b8..8f480edc3 100644
--- a/web/package.json
+++ b/web/package.json
@@ -18,6 +18,7 @@
"@types/query-string": "^6.3.0",
"@types/react": "16.9.12",
"@types/react-dom": "16.9.4",
+ "@types/react-ga": "^2.3.0",
"@types/react-router-dom": "^5.1.2",
"axios": "^0.19.0",
"chai": "^4.2.0",
@@ -28,6 +29,7 @@
"query-string": "^6.9.0",
"react": "^16.12.0",
"react-dom": "^16.12.0",
+ "react-ga": "^2.7.0",
"react-loading": "^2.0.3",
"react-otp-input": "^1.0.1",
"react-router-dom": "^5.1.2",
diff --git a/web/src/App.tsx b/web/src/App.tsx
index b9d78862d..83afe3db1 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect } from 'react';
import {
BrowserRouter as Router, Route, Switch, Redirect
} from "react-router-dom";
@@ -17,36 +17,52 @@ import NotificationsContext from './hooks/NotificationsContext';
import { Notification } from './models/Notifications';
import NotificationBar from './components/NotificationBar';
import SignOut from './views/LoginPortal/SignOut/SignOut';
+import { useConfiguration } from './hooks/Configuration';
+import Tracker from "./components/Tracker";
+import { useTracking } from "./hooks/Tracking";
const App: React.FC = () => {
const [notification, setNotification] = useState(null as Notification | null);
+ const [configuration, fetchConfig, , fetchConfigError] = useConfiguration();
+ const tracker = useTracking(configuration);
+
+ useEffect(() => {
+ if (fetchConfigError) {
+ console.error(fetchConfigError);
+ }
+ }, [fetchConfigError]);
+
+ useEffect(() => { fetchConfig() }, [fetchConfig]);
+
return (
- setNotification(null)} />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ setNotification(null)} />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/web/src/components/Tracker.test.tsx b/web/src/components/Tracker.test.tsx
new file mode 100644
index 000000000..0ba960d7a
--- /dev/null
+++ b/web/src/components/Tracker.test.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import { mount } from "enzyme";
+import Tracker from "./Tracker";
+
+import { MemoryRouter as Router } from 'react-router-dom';
+
+const mountWithRouter = node => mount({node});
+
+it('renders without crashing', () => {
+ mountWithRouter();
+});
\ No newline at end of file
diff --git a/web/src/components/Tracker.tsx b/web/src/components/Tracker.tsx
new file mode 100644
index 000000000..a19840e1c
--- /dev/null
+++ b/web/src/components/Tracker.tsx
@@ -0,0 +1,35 @@
+import React, { useEffect, useCallback, Fragment, ReactNode } from "react";
+import { useLocation } from "react-router";
+import ReactGA, { Tracker } from "react-ga";
+
+export interface Props {
+ tracker: Tracker | undefined;
+ children?: ReactNode;
+}
+
+export default function (props: Props) {
+ const location = useLocation();
+
+ const trackPage = useCallback((page: string) => {
+ if (props.tracker) {
+ ReactGA.set({ page });
+ ReactGA.pageview(page);
+ }
+ }, [props.tracker]);
+
+ useEffect(() => {
+ if (props.tracker) {
+ ReactGA.initialize([props.tracker]);
+ }
+ }, [props.tracker]);
+
+ useEffect(() => {
+ trackPage(location.pathname + location.search);
+ }, [trackPage, location.pathname, location.search]);
+
+ return (
+
+ {props.children}
+
+ )
+}
\ No newline at end of file
diff --git a/web/src/hooks/Configuration.ts b/web/src/hooks/Configuration.ts
index 44a16e0ad..18f407596 100644
--- a/web/src/hooks/Configuration.ts
+++ b/web/src/hooks/Configuration.ts
@@ -1,6 +1,10 @@
import { useRemoteCall } from "./RemoteCall";
-import { getAvailable2FAMethods } from "../services/Configuration";
+import { getConfiguration, getExtendedConfiguration } from "../services/Configuration";
-export function useAutheliaConfiguration() {
- return useRemoteCall(getAvailable2FAMethods, []);
+export function useConfiguration() {
+ return useRemoteCall(getConfiguration, []);
+}
+
+export function useExtendedConfiguration() {
+ return useRemoteCall(getExtendedConfiguration, []);
}
\ No newline at end of file
diff --git a/web/src/hooks/Tracking.tsx b/web/src/hooks/Tracking.tsx
new file mode 100644
index 000000000..c6fe2815b
--- /dev/null
+++ b/web/src/hooks/Tracking.tsx
@@ -0,0 +1,15 @@
+import { useState, useEffect } from "react";
+import { Configuration } from "../models/Configuration";
+import { Tracker } from "react-ga";
+
+export function useTracking(configuration: Configuration | undefined) {
+ const [trackingIds, setTrackingIds] = useState(undefined as Tracker | undefined);
+
+ useEffect(() => {
+ if (configuration && configuration.ga_tracking_id) {
+ setTrackingIds({ trackingId: configuration.ga_tracking_id });
+ }
+ }, [configuration]);
+
+ return trackingIds;
+}
\ No newline at end of file
diff --git a/web/src/models/Configuration.ts b/web/src/models/Configuration.ts
index 2784d9b37..f40766fd2 100644
--- a/web/src/models/Configuration.ts
+++ b/web/src/models/Configuration.ts
@@ -1,3 +1,9 @@
import { SecondFactorMethod } from "./Methods";
-export type Configuration = Set
\ No newline at end of file
+export interface Configuration {
+ ga_tracking_id: string;
+}
+
+export interface ExtendedConfiguration {
+ available_methods: Set;
+}
\ No newline at end of file
diff --git a/web/src/services/Api.ts b/web/src/services/Api.ts
index 22850d084..3df7ddf33 100644
--- a/web/src/services/Api.ts
+++ b/web/src/services/Api.ts
@@ -25,6 +25,9 @@ export const UserInfoPath = "/api/user/info";
export const UserInfo2FAMethodPath = "/api/user/info/2fa_method";
export const Available2FAMethodsPath = "/api/secondfactor/available";
+export const ConfigurationPath = "/api/configuration";
+export const ExtendedConfigurationPath = "/api/configuration/extended";
+
export interface ErrorResponse {
status: "KO";
message: string;
diff --git a/web/src/services/Configuration.ts b/web/src/services/Configuration.ts
index 47eb0425b..e77c610e3 100644
--- a/web/src/services/Configuration.ts
+++ b/web/src/services/Configuration.ts
@@ -1,9 +1,17 @@
import { Get } from "./Client";
-import { Available2FAMethodsPath } from "./Api";
-import { Method2FA, toEnum } from "./UserPreferences";
-import { Configuration } from "../models/Configuration";
+import { ExtendedConfigurationPath, ConfigurationPath } from "./Api";
+import { toEnum, Method2FA } from "./UserPreferences";
+import { Configuration, ExtendedConfiguration } from "../models/Configuration";
-export async function getAvailable2FAMethods(): Promise {
- const methods = await Get(Available2FAMethodsPath);
- return new Set(methods.map(toEnum));
+export async function getConfiguration(): Promise {
+ return Get(ConfigurationPath);
+}
+
+interface ExtendedConfigurationPayload {
+ available_methods: Method2FA[];
+}
+
+export async function getExtendedConfiguration(): Promise {
+ const config = await Get(ExtendedConfigurationPath);
+ return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
}
\ No newline at end of file
diff --git a/web/src/views/LoginPortal/LoginPortal.tsx b/web/src/views/LoginPortal/LoginPortal.tsx
index 39aded588..dfff8f25f 100644
--- a/web/src/views/LoginPortal/LoginPortal.tsx
+++ b/web/src/views/LoginPortal/LoginPortal.tsx
@@ -13,7 +13,7 @@ import { useNotifications } from "../../hooks/NotificationsContext";
import { useRedirectionURL } from "../../hooks/RedirectionURL";
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
import { SecondFactorMethod } from "../../models/Methods";
-import { useAutheliaConfiguration } from "../../hooks/Configuration";
+import { useExtendedConfiguration } from "../../hooks/Configuration";
export default function () {
const history = useHistory();
@@ -24,7 +24,7 @@ export default function () {
const [state, fetchState, , fetchStateError] = useAutheliaState();
const [userInfo, fetchUserInfo, , fetchUserInfoError] = userUserInfo();
- const [configuration, fetchConfiguration, , fetchConfigurationError] = useAutheliaConfiguration();
+ const [configuration, fetchConfiguration, , fetchConfigurationError] = useExtendedConfiguration();
const redirect = useCallback((url: string) => history.push(url), [history]);
diff --git a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx
index 3497a2438..b6472bb0e 100644
--- a/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx
+++ b/web/src/views/LoginPortal/SecondFactor/SecondFactorForm.tsx
@@ -18,7 +18,7 @@ import {
} from "../../../Routes";
import { setPrefered2FAMethod } from "../../../services/UserPreferences";
import { UserInfo } from "../../../models/UserInfo";
-import { Configuration } from "../../../models/Configuration";
+import { ExtendedConfiguration } from "../../../models/Configuration";
import u2fApi from "u2f-api";
import { AuthenticationLevel } from "../../../services/State";
@@ -29,7 +29,7 @@ export interface Props {
authenticationLevel: AuthenticationLevel;
userInfo: UserInfo;
- configuration: Configuration;
+ configuration: ExtendedConfiguration;
onMethodChanged: (method: SecondFactorMethod) => void;
onAuthenticationSuccess: (redirectURL: string | undefined) => void;
@@ -89,7 +89,7 @@ export default function (props: Props) {
showBrand>
setMethodSelectionOpen(false)}
onClick={handleMethodSelected} />