Allow administrator to provide a Google Analytics tracking ID.
Providing a GA tracking ID allows administrators to analyze how the portal is used by their users in large environments, i.e., with many users. This will make even more sense when we have users and admins management interfaces.pull/482/head
parent
3faa63e8ed
commit
3d20142292
|
@ -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
|
||||
|
|
|
@ -7,4 +7,11 @@ ARG GROUP_ID
|
|||
|
||||
RUN addgroup --gid ${GROUP_ID} dev && \
|
||||
adduser --uid ${USER_ID} -G dev -D dev
|
||||
USER dev
|
||||
|
||||
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
|
||||
|
|
|
@ -4,8 +4,6 @@ services:
|
|||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- "/tmp/authelia:/tmp/authelia"
|
||||
environment:
|
||||
- ENVIRONMENT=dev
|
||||
restart: always
|
||||
|
|
|
@ -13,7 +13,6 @@ services:
|
|||
- "./example/compose/authelia/resources/:/resources"
|
||||
- ".:/app"
|
||||
- "${GOPATH}:/go"
|
||||
- "/tmp/authelia:/tmp/authelia"
|
||||
environment:
|
||||
- ENVIRONMENT=dev
|
||||
networks:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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) {
|
|
@ -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)))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -22,7 +22,7 @@ session:
|
|||
|
||||
storage:
|
||||
local:
|
||||
path: /tmp/authelia/db.sqlite3
|
||||
path: /var/lib/authelia/db.sqlite3
|
||||
|
||||
totp:
|
||||
issuer: example.com
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -20,7 +20,7 @@ session:
|
|||
|
||||
storage:
|
||||
local:
|
||||
path: /tmp/authelia/db.sqlite
|
||||
path: /var/lib/authelia/db.sqlite
|
||||
|
||||
access_control:
|
||||
default_policy: bypass
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
<NotificationsContext.Provider value={{ notification, setNotification }} >
|
||||
<NotificationBar onClose={() => setNotification(null)} />
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path={ResetPasswordStep1Route} exact>
|
||||
<ResetPasswordStep1 />
|
||||
</Route>
|
||||
<Route path={ResetPasswordStep2Route} exact>
|
||||
<ResetPasswordStep2 />
|
||||
</Route>
|
||||
<Route path={RegisterSecurityKeyRoute} exact>
|
||||
<RegisterSecurityKey />
|
||||
</Route>
|
||||
<Route path={RegisterOneTimePasswordRoute} exact>
|
||||
<RegisterOneTimePassword />
|
||||
</Route>
|
||||
<Route path={LogoutRoute} exact>
|
||||
<SignOut />
|
||||
</Route>
|
||||
<Route path={FirstFactorRoute}>
|
||||
<LoginPortal />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Redirect to={FirstFactorRoute}></Redirect>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Tracker tracker={tracker}>
|
||||
<NotificationBar onClose={() => setNotification(null)} />
|
||||
<Switch>
|
||||
<Route path={ResetPasswordStep1Route} exact>
|
||||
<ResetPasswordStep1 />
|
||||
</Route>
|
||||
<Route path={ResetPasswordStep2Route} exact>
|
||||
<ResetPasswordStep2 />
|
||||
</Route>
|
||||
<Route path={RegisterSecurityKeyRoute} exact>
|
||||
<RegisterSecurityKey />
|
||||
</Route>
|
||||
<Route path={RegisterOneTimePasswordRoute} exact>
|
||||
<RegisterOneTimePassword />
|
||||
</Route>
|
||||
<Route path={LogoutRoute} exact>
|
||||
<SignOut />
|
||||
</Route>
|
||||
<Route path={FirstFactorRoute}>
|
||||
<LoginPortal />
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Redirect to={FirstFactorRoute}></Redirect>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Tracker>
|
||||
</Router>
|
||||
</NotificationsContext.Provider>
|
||||
);
|
||||
|
|
|
@ -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(<Router>{node}</Router>);
|
||||
|
||||
it('renders without crashing', () => {
|
||||
mountWithRouter(<Tracker trackingIDs={[]} />);
|
||||
});
|
|
@ -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 (
|
||||
<Fragment>
|
||||
{props.children}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
|
@ -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, []);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,3 +1,9 @@
|
|||
import { SecondFactorMethod } from "./Methods";
|
||||
|
||||
export type Configuration = Set<SecondFactorMethod>
|
||||
export interface Configuration {
|
||||
ga_tracking_id: string;
|
||||
}
|
||||
|
||||
export interface ExtendedConfiguration {
|
||||
available_methods: Set<SecondFactorMethod>;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<Configuration> {
|
||||
const methods = await Get<Method2FA[]>(Available2FAMethodsPath);
|
||||
return new Set(methods.map(toEnum));
|
||||
export async function getConfiguration(): Promise<Configuration> {
|
||||
return Get<Configuration>(ConfigurationPath);
|
||||
}
|
||||
|
||||
interface ExtendedConfigurationPayload {
|
||||
available_methods: Method2FA[];
|
||||
}
|
||||
|
||||
export async function getExtendedConfiguration(): Promise<ExtendedConfiguration> {
|
||||
const config = await Get<ExtendedConfigurationPayload>(ExtendedConfigurationPath);
|
||||
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
|
||||
}
|
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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>
|
||||
<MethodSelectionDialog
|
||||
open={methodSelectionOpen}
|
||||
methods={props.configuration}
|
||||
methods={props.configuration.available_methods}
|
||||
u2fSupported={u2fSupported}
|
||||
onClose={() => setMethodSelectionOpen(false)}
|
||||
onClick={handleMethodSelected} />
|
||||
|
|
Loading…
Reference in New Issue