feat(tokenizer|sso): add tokenizer for session management and oidc sso support (#9183)
## 📄 Summary
- Instead of relying on JWT for session management, we are adding another token system: opaque. This gives the benefits of expiration and revocation.
- We are now ensuring that emails are regex checked throughout the backend.
- Support has been added for OIDC protocol
This commit is contained in:
17
.github/workflows/integrationci.yaml
vendored
17
.github/workflows/integrationci.yaml
vendored
@@ -15,7 +15,8 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
src:
|
src:
|
||||||
- bootstrap
|
- bootstrap
|
||||||
- auth
|
- passwordauthn
|
||||||
|
- callbackauthn
|
||||||
- querier
|
- querier
|
||||||
- ttl
|
- ttl
|
||||||
sqlstore-provider:
|
sqlstore-provider:
|
||||||
@@ -43,6 +44,20 @@ jobs:
|
|||||||
python -m pip install poetry==2.1.2
|
python -m pip install poetry==2.1.2
|
||||||
python -m poetry config virtualenvs.in-project true
|
python -m poetry config virtualenvs.in-project true
|
||||||
cd tests/integration && poetry install --no-root
|
cd tests/integration && poetry install --no-root
|
||||||
|
- name: webdriver
|
||||||
|
run: |
|
||||||
|
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||||
|
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee -a /etc/apt/sources.list.d/google-chrome.list
|
||||||
|
sudo apt-get update -qqy
|
||||||
|
sudo apt-get -qqy install google-chrome-stable
|
||||||
|
CHROME_VERSION=$(google-chrome-stable --version)
|
||||||
|
CHROME_FULL_VERSION=${CHROME_VERSION%%.*}
|
||||||
|
CHROME_MAJOR_VERSION=${CHROME_FULL_VERSION//[!0-9]}
|
||||||
|
sudo rm /etc/apt/sources.list.d/google-chrome.list
|
||||||
|
export CHROMEDRIVER_VERSION=`curl -s https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_${CHROME_MAJOR_VERSION%%.*}`
|
||||||
|
curl -L -O "https://storage.googleapis.com/chrome-for-testing-public/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip"
|
||||||
|
unzip chromedriver-linux64.zip && chmod +x chromedriver && sudo mv chromedriver /usr/local/bin
|
||||||
|
chromedriver -version
|
||||||
- name: run
|
- name: run
|
||||||
run: |
|
run: |
|
||||||
cd tests/integration && \
|
cd tests/integration && \
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/cmd"
|
"github.com/SigNoz/signoz/cmd"
|
||||||
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/analytics"
|
"github.com/SigNoz/signoz/pkg/analytics"
|
||||||
|
"github.com/SigNoz/signoz/pkg/authn"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
|
||||||
@@ -56,12 +56,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
|
|
||||||
|
|
||||||
signoz, err := signoz.New(
|
signoz, err := signoz.New(
|
||||||
ctx,
|
ctx,
|
||||||
config,
|
config,
|
||||||
jwt,
|
|
||||||
zeus.Config{},
|
zeus.Config{},
|
||||||
noopzeus.NewProviderFactory(),
|
noopzeus.NewProviderFactory(),
|
||||||
licensing.Config{},
|
licensing.Config{},
|
||||||
@@ -76,13 +73,16 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
|||||||
},
|
},
|
||||||
signoz.NewSQLStoreProviderFactories(),
|
signoz.NewSQLStoreProviderFactories(),
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
|
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||||
|
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := app.NewServer(config, signoz, jwt)
|
server, err := app.NewServer(config, signoz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to create server", "error", err)
|
logger.ErrorContext(ctx, "failed to create server", "error", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/config"
|
"github.com/SigNoz/signoz/pkg/config"
|
||||||
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
"github.com/SigNoz/signoz/pkg/config/envprovider"
|
||||||
@@ -30,12 +29,3 @@ func NewSigNozConfig(ctx context.Context, logger *slog.Logger, flags signoz.Depr
|
|||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJWTSecret(ctx context.Context, logger *slog.Logger) string {
|
|
||||||
jwtSecret := os.Getenv("SIGNOZ_JWT_SECRET")
|
|
||||||
if len(jwtSecret) == 0 {
|
|
||||||
logger.ErrorContext(ctx, "🚨 CRITICAL SECURITY ISSUE: No JWT secret key specified!", "error", "SIGNOZ_JWT_SECRET environment variable is not set. This has dire consequences for the security of the application. Without a JWT secret, user sessions are vulnerable to tampering and unauthorized access. Please set the SIGNOZ_JWT_SECRET environment variable immediately. For more information, please refer to https://github.com/SigNoz/signoz/issues/8400.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwtSecret
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/cmd"
|
"github.com/SigNoz/signoz/cmd"
|
||||||
|
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
|
||||||
|
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
|
||||||
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
|
||||||
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
|
||||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
||||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||||
"github.com/SigNoz/signoz/pkg/analytics"
|
"github.com/SigNoz/signoz/pkg/analytics"
|
||||||
|
"github.com/SigNoz/signoz/pkg/authn"
|
||||||
"github.com/SigNoz/signoz/pkg/factory"
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
"github.com/SigNoz/signoz/pkg/licensing"
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||||
@@ -54,17 +57,14 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
|||||||
|
|
||||||
// add enterprise sqlstore factories to the community sqlstore factories
|
// add enterprise sqlstore factories to the community sqlstore factories
|
||||||
sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
|
sqlstoreFactories := signoz.NewSQLStoreProviderFactories()
|
||||||
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory())); err != nil {
|
if err := sqlstoreFactories.Add(postgressqlstore.NewFactory(sqlstorehook.NewLoggingFactory(), sqlstorehook.NewInstrumentationFactory())); err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
|
logger.ErrorContext(ctx, "failed to add postgressqlstore factory", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
jwt := authtypes.NewJWT(cmd.NewJWTSecret(ctx, logger), 30*time.Minute, 30*24*time.Hour)
|
|
||||||
|
|
||||||
signoz, err := signoz.New(
|
signoz, err := signoz.New(
|
||||||
ctx,
|
ctx,
|
||||||
config,
|
config,
|
||||||
jwt,
|
|
||||||
enterprisezeus.Config(),
|
enterprisezeus.Config(),
|
||||||
httpzeus.NewProviderFactory(),
|
httpzeus.NewProviderFactory(),
|
||||||
enterpriselicensing.Config(24*time.Hour, 3),
|
enterpriselicensing.Config(24*time.Hour, 3),
|
||||||
@@ -84,13 +84,34 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
|||||||
},
|
},
|
||||||
sqlstoreFactories,
|
sqlstoreFactories,
|
||||||
signoz.NewTelemetryStoreProviderFactories(),
|
signoz.NewTelemetryStoreProviderFactories(),
|
||||||
|
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
|
||||||
|
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcCallbackAuthN, err := oidccallbackauthn.New(store, licensing, providerSettings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authNs[authtypes.AuthNProviderSAML] = samlCallbackAuthN
|
||||||
|
authNs[authtypes.AuthNProviderOIDC] = oidcCallbackAuthN
|
||||||
|
|
||||||
|
return authNs, nil
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
logger.ErrorContext(ctx, "failed to create signoz", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
server, err := enterpriseapp.NewServer(config, signoz, jwt)
|
server, err := enterpriseapp.NewServer(config, signoz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to create server", "error", err)
|
logger.ErrorContext(ctx, "failed to create server", "error", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -243,3 +243,28 @@ statsreporter:
|
|||||||
gateway:
|
gateway:
|
||||||
# The URL of the gateway's api.
|
# The URL of the gateway's api.
|
||||||
url: http://localhost:8080
|
url: http://localhost:8080
|
||||||
|
|
||||||
|
##################### Tokenizer #####################
|
||||||
|
tokenizer:
|
||||||
|
# Specifies the tokenizer provider to use.
|
||||||
|
provider: jwt
|
||||||
|
lifetime:
|
||||||
|
# The duration for which a user can be idle before being required to authenticate.
|
||||||
|
idle: 168h
|
||||||
|
# The duration for which a user can remain logged in before being asked to login.
|
||||||
|
max: 720h
|
||||||
|
rotation:
|
||||||
|
# The interval to rotate tokens in.
|
||||||
|
interval: 30m
|
||||||
|
# The duration for which the previous token pair remains valid after a token pair is rotated.
|
||||||
|
duration: 60s
|
||||||
|
jwt:
|
||||||
|
# The secret to sign the JWT tokens.
|
||||||
|
secret: secret
|
||||||
|
opaque:
|
||||||
|
gc:
|
||||||
|
# The interval to perform garbage collection.
|
||||||
|
interval: 1h
|
||||||
|
token:
|
||||||
|
# The maximum number of tokens a user can have. This limits the number of concurrent sessions a user can have.
|
||||||
|
max_per_user: 5
|
||||||
|
|||||||
191
ee/authn/callbackauthn/oidccallbackauthn/authn.go
Normal file
191
ee/authn/callbackauthn/oidccallbackauthn/authn.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package oidccallbackauthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/authn"
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/factory"
|
||||||
|
"github.com/SigNoz/signoz/pkg/http/client"
|
||||||
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
redirectPath string = "/api/v1/complete/oidc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
scopes []string = []string{"email", oidc.ScopeOpenID}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ authn.CallbackAuthN = (*AuthN)(nil)
|
||||||
|
|
||||||
|
type AuthN struct {
|
||||||
|
store authtypes.AuthNStore
|
||||||
|
licensing licensing.Licensing
|
||||||
|
httpClient *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(store authtypes.AuthNStore, licensing licensing.Licensing, providerSettings factory.ProviderSettings) (*AuthN, error) {
|
||||||
|
httpClient, err := client.New(providerSettings.Logger, providerSettings.TracerProvider, providerSettings.MeterProvider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AuthN{
|
||||||
|
store: store,
|
||||||
|
licensing: licensing,
|
||||||
|
httpClient: httpClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (string, error) {
|
||||||
|
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderOIDC {
|
||||||
|
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "domain type is not oidc")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, oauth2Config, err := a.oidcProviderAndoauth2Config(ctx, siteURL, authDomain)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return oauth2Config.AuthCodeURL(authtypes.NewState(siteURL, authDomain.StorableAuthDomain().ID).URL.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) HandleCallback(ctx context.Context, query url.Values) (*authtypes.CallbackIdentity, error) {
|
||||||
|
if err := query.Get("error"); err != "" {
|
||||||
|
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "oidc: error while authenticating").WithAdditional(query.Get("error_description"))
|
||||||
|
}
|
||||||
|
|
||||||
|
state, err := authtypes.NewStateFromString(query.Get("state"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Newf(errors.TypeInvalidInput, authtypes.ErrCodeInvalidState, "oidc: invalid state").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
authDomain, err := a.store.GetAuthDomainFromID(ctx, state.DomainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = a.licensing.GetActive(ctx, authDomain.StorableAuthDomain().OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcProvider, oauth2Config, err := a.oidcProviderAndoauth2Config(ctx, state.URL, authDomain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, a.httpClient.Client())
|
||||||
|
token, err := oauth2Config.Exchange(ctx, query.Get("code"))
|
||||||
|
if err != nil {
|
||||||
|
var retrieveError *oauth2.RetrieveError
|
||||||
|
if errors.As(err, &retrieveError) {
|
||||||
|
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "oidc: failed to get token").WithAdditional(retrieveError.ErrorDescription).WithAdditional(string(retrieveError.Body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "oidc: failed to get token").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := a.claimsFromIDToken(ctx, authDomain, oidcProvider, token)
|
||||||
|
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims == nil && authDomain.AuthDomainConfig().OIDC.GetUserInfo {
|
||||||
|
claims, err = a.claimsFromUserInfo(ctx, oidcProvider, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emailClaim, ok := claims[authDomain.AuthDomainConfig().OIDC.ClaimMapping.Email].(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email in claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err := valuer.NewEmail(emailClaim)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to parse email").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authDomain.AuthDomainConfig().OIDC.InsecureSkipEmailVerified {
|
||||||
|
emailVerifiedClaim, ok := claims["email_verified"].(bool)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: missing email_verified in claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !emailVerifiedClaim {
|
||||||
|
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "oidc: email is not verified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) oidcProviderAndoauth2Config(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (*oidc.Provider, *oauth2.Config, error) {
|
||||||
|
if authDomain.AuthDomainConfig().OIDC.IssuerAlias != "" {
|
||||||
|
ctx = oidc.InsecureIssuerURLContext(ctx, authDomain.AuthDomainConfig().OIDC.IssuerAlias)
|
||||||
|
}
|
||||||
|
|
||||||
|
oidcProvider, err := oidc.NewProvider(ctx, authDomain.AuthDomainConfig().OIDC.Issuer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return oidcProvider, &oauth2.Config{
|
||||||
|
ClientID: authDomain.AuthDomainConfig().OIDC.ClientID,
|
||||||
|
ClientSecret: authDomain.AuthDomainConfig().OIDC.ClientSecret,
|
||||||
|
Endpoint: oidcProvider.Endpoint(),
|
||||||
|
Scopes: scopes,
|
||||||
|
RedirectURL: (&url.URL{
|
||||||
|
Scheme: siteURL.Scheme,
|
||||||
|
Host: siteURL.Host,
|
||||||
|
Path: redirectPath,
|
||||||
|
}).String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) claimsFromIDToken(ctx context.Context, authDomain *authtypes.AuthDomain, provider *oidc.Provider, token *oauth2.Token) (map[string]any, error) {
|
||||||
|
rawIDToken, ok := token.Extra("id_token").(string)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, "oidc: no id_token in token response")
|
||||||
|
}
|
||||||
|
|
||||||
|
verifier := provider.Verifier(&oidc.Config{ClientID: authDomain.AuthDomainConfig().OIDC.ClientID})
|
||||||
|
idToken, err := verifier.Verify(ctx, rawIDToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Newf(errors.TypeForbidden, errors.CodeForbidden, "oidc: failed to verify token").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims map[string]any
|
||||||
|
if err := idToken.Claims(&claims); err != nil {
|
||||||
|
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to decode claims").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) claimsFromUserInfo(ctx context.Context, provider *oidc.Provider, token *oauth2.Token) (map[string]any, error) {
|
||||||
|
var claims map[string]any
|
||||||
|
|
||||||
|
userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(&oauth2.Token{
|
||||||
|
AccessToken: token.AccessToken,
|
||||||
|
TokenType: "Bearer", // The UserInfo endpoint requires a bearer token as per RFC6750
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "oidc: failed to get user info").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := userInfo.Claims(&claims); err != nil {
|
||||||
|
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "oidc: failed to decode claims").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
155
ee/authn/callbackauthn/samlcallbackauthn/authn.go
Normal file
155
ee/authn/callbackauthn/samlcallbackauthn/authn.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package samlcallbackauthn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/SigNoz/signoz/pkg/authn"
|
||||||
|
"github.com/SigNoz/signoz/pkg/errors"
|
||||||
|
"github.com/SigNoz/signoz/pkg/licensing"
|
||||||
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||||
|
"github.com/SigNoz/signoz/pkg/valuer"
|
||||||
|
saml2 "github.com/russellhaering/gosaml2"
|
||||||
|
dsig "github.com/russellhaering/goxmldsig"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
redirectPath string = "/api/v1/complete/saml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ authn.CallbackAuthN = (*AuthN)(nil)
|
||||||
|
|
||||||
|
type AuthN struct {
|
||||||
|
store authtypes.AuthNStore
|
||||||
|
licensing licensing.Licensing
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(ctx context.Context, store authtypes.AuthNStore, licensing licensing.Licensing) (*AuthN, error) {
|
||||||
|
return &AuthN{
|
||||||
|
store: store,
|
||||||
|
licensing: licensing,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) LoginURL(ctx context.Context, siteURL *url.URL, authDomain *authtypes.AuthDomain) (string, error) {
|
||||||
|
if authDomain.AuthDomainConfig().AuthNProvider != authtypes.AuthNProviderSAML {
|
||||||
|
return "", errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthDomainMismatch, "saml: domain type is not saml")
|
||||||
|
}
|
||||||
|
|
||||||
|
sp, err := a.serviceProvider(siteURL, authDomain)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := sp.BuildAuthURL(authtypes.NewState(siteURL, authDomain.StorableAuthDomain().ID).URL.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) HandleCallback(ctx context.Context, formValues url.Values) (*authtypes.CallbackIdentity, error) {
|
||||||
|
state, err := authtypes.NewStateFromString(formValues.Get("RelayState"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, authtypes.ErrCodeInvalidState, "saml: invalid state").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
authDomain, err := a.store.GetAuthDomainFromID(ctx, state.DomainID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = a.licensing.GetActive(ctx, authDomain.StorableAuthDomain().OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
sp, err := a.serviceProvider(state.URL, authDomain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
assertionInfo, err := sp.RetrieveAssertionInfo(formValues.Get("SAMLResponse"))
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, &saml2.ErrVerification{}) {
|
||||||
|
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.As(err, &saml2.ErrMissingElement{}) {
|
||||||
|
return nil, errors.New(errors.TypeNotFound, errors.CodeNotFound, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if assertionInfo.WarningInfo.InvalidTime {
|
||||||
|
return nil, errors.New(errors.TypeForbidden, errors.CodeForbidden, "saml: expired saml response")
|
||||||
|
}
|
||||||
|
|
||||||
|
email, err := valuer.NewEmail(assertionInfo.NameID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "saml: invalid email").WithAdditional("The nameID assertion is used to retrieve the email address, please check your IDP configuration and try again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return authtypes.NewCallbackIdentity("", email, authDomain.StorableAuthDomain().OrgID, state), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) serviceProvider(siteURL *url.URL, authDomain *authtypes.AuthDomain) (*saml2.SAMLServiceProvider, error) {
|
||||||
|
certStore, err := a.getCertificateStore(authDomain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acsURL := &url.URL{Scheme: siteURL.Scheme, Host: siteURL.Host, Path: redirectPath}
|
||||||
|
|
||||||
|
// Note:
|
||||||
|
// The ServiceProviderIssuer is the client id in case of keycloak. Since we set it to the host here, we need to set the client id == host in keycloak.
|
||||||
|
// For AWSSSO, this is the value of Application SAML audience.
|
||||||
|
return &saml2.SAMLServiceProvider{
|
||||||
|
IdentityProviderSSOURL: authDomain.AuthDomainConfig().SAML.SamlIdp,
|
||||||
|
IdentityProviderIssuer: authDomain.AuthDomainConfig().SAML.SamlEntity,
|
||||||
|
ServiceProviderIssuer: siteURL.Host,
|
||||||
|
AssertionConsumerServiceURL: acsURL.String(),
|
||||||
|
SignAuthnRequests: !authDomain.AuthDomainConfig().SAML.InsecureSkipAuthNRequestsSigned,
|
||||||
|
AllowMissingAttributes: true,
|
||||||
|
IDPCertificateStore: certStore,
|
||||||
|
SPKeyStore: dsig.RandomKeyStoreForTest(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthN) getCertificateStore(authDomain *authtypes.AuthDomain) (dsig.X509CertificateStore, error) {
|
||||||
|
certStore := &dsig.MemoryX509CertificateStore{
|
||||||
|
Roots: []*x509.Certificate{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var certBytes []byte
|
||||||
|
if strings.Contains(authDomain.AuthDomainConfig().SAML.SamlCert, "-----BEGIN CERTIFICATE-----") {
|
||||||
|
block, _ := pem.Decode([]byte(authDomain.AuthDomainConfig().SAML.SamlCert))
|
||||||
|
if block == nil {
|
||||||
|
return certStore, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "no valid pem cert found")
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes = block.Bytes
|
||||||
|
} else {
|
||||||
|
certData, err := base64.StdEncoding.DecodeString(authDomain.AuthDomainConfig().SAML.SamlCert)
|
||||||
|
if err != nil {
|
||||||
|
return certStore, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to read certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
certBytes = certData
|
||||||
|
}
|
||||||
|
|
||||||
|
idpCert, err := x509.ParseCertificate(certBytes)
|
||||||
|
if err != nil {
|
||||||
|
return certStore, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to prepare saml request, invalid cert: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
certStore.Roots = append(certStore.Roots, idpCert)
|
||||||
|
|
||||||
|
return certStore, nil
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
|
||||||
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
|
||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/version"
|
"github.com/SigNoz/signoz/pkg/version"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
@@ -35,10 +34,7 @@ type APIHandlerOptions struct {
|
|||||||
Gateway *httputil.ReverseProxy
|
Gateway *httputil.ReverseProxy
|
||||||
GatewayUrl string
|
GatewayUrl string
|
||||||
// Querier Influx Interval
|
// Querier Influx Interval
|
||||||
FluxInterval time.Duration
|
FluxInterval time.Duration
|
||||||
UseLogsNewSchema bool
|
|
||||||
UseTraceNewSchema bool
|
|
||||||
JWT *authtypes.JWT
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIHandler struct {
|
type APIHandler struct {
|
||||||
@@ -93,7 +89,8 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
|
|||||||
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
|
||||||
|
|
||||||
// paid plans specific routes
|
// paid plans specific routes
|
||||||
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.receiveSAML)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.Signoz.Handlers.Session.CreateSessionBySAMLCallback)).Methods(http.MethodPost)
|
||||||
|
router.HandleFunc("/api/v1/complete/oidc", am.OpenAccess(ah.Signoz.Handlers.Session.CreateSessionByOIDCCallback)).Methods(http.MethodGet)
|
||||||
|
|
||||||
// base overrides
|
// base overrides
|
||||||
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/query-service/constants"
|
|
||||||
"github.com/SigNoz/signoz/pkg/valuer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) {
|
|
||||||
ssoError := []byte("Login failed. Please contact your system administrator")
|
|
||||||
dst := make([]byte, base64.StdEncoding.EncodedLen(len(ssoError)))
|
|
||||||
base64.StdEncoding.Encode(dst, ssoError)
|
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectURL, string(dst)), http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
|
|
||||||
// receiveSAML completes a SAML request and gets user logged in
|
|
||||||
func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// this is the source url that initiated the login request
|
|
||||||
redirectUri := constants.GetDefaultSiteURL()
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// the relay state is sent when a login request is submitted to
|
|
||||||
// Idp.
|
|
||||||
relayState := r.FormValue("RelayState")
|
|
||||||
zap.L().Debug("[receiveML] relay state", zap.String("relayState", relayState))
|
|
||||||
|
|
||||||
parsedState, err := url.Parse(relayState)
|
|
||||||
if err != nil || relayState == "" {
|
|
||||||
zap.L().Error("[receiveSAML] failed to process response - invalid response from IDP", zap.Error(err), zap.Any("request", r))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// upgrade redirect url from the relay state for better accuracy
|
|
||||||
redirectUri = fmt.Sprintf("%s://%s%s", parsedState.Scheme, parsedState.Host, "/login")
|
|
||||||
|
|
||||||
// fetch domain by parsing relay state.
|
|
||||||
domain, err := ah.Signoz.Modules.User.GetDomainFromSsoResponse(ctx, parsedState)
|
|
||||||
if err != nil {
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
orgID, err := valuer.NewUUID(domain.OrgID)
|
|
||||||
if err != nil {
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = ah.Signoz.Licensing.GetActive(ctx, orgID)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveSAML] sso requested but feature unavailable in org domain")
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sp, err := domain.PrepareSamlRequest(parsedState)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveSAML] failed to prepare saml request for domain", zap.String("domain", domain.String()), zap.Error(err))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assertionInfo, err := sp.RetrieveAssertionInfo(r.FormValue("SAMLResponse"))
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveSAML] failed to retrieve assertion info from saml response", zap.String("domain", domain.String()), zap.Error(err))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if assertionInfo.WarningInfo.InvalidTime {
|
|
||||||
zap.L().Error("[receiveSAML] expired saml response", zap.String("domain", domain.String()), zap.Error(err))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
email := assertionInfo.NameID
|
|
||||||
if email == "" {
|
|
||||||
zap.L().Error("[receiveSAML] invalid email in the SSO response", zap.String("domain", domain.String()))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
nextPage, err := ah.Signoz.Modules.User.PrepareSsoRedirect(ctx, redirectUri, email)
|
|
||||||
if err != nil {
|
|
||||||
zap.L().Error("[receiveSAML] failed to generate redirect URI after successful login ", zap.String("domain", domain.String()), zap.Error(err))
|
|
||||||
handleSsoError(w, r, redirectUri)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, nextPage, http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
@@ -168,38 +168,22 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
|
|||||||
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
|
||||||
ctx context.Context, orgId string, cloudProvider string,
|
ctx context.Context, orgId string, cloudProvider string,
|
||||||
) (*types.User, *basemodel.ApiError) {
|
) (*types.User, *basemodel.ApiError) {
|
||||||
cloudIntegrationUser := fmt.Sprintf("%s-integration", cloudProvider)
|
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
|
||||||
email := fmt.Sprintf("%s@signoz.io", cloudIntegrationUser)
|
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
|
||||||
|
|
||||||
integrationUserResult, err := ah.Signoz.Modules.User.GetUserByEmailInOrg(ctx, orgId, email)
|
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId))
|
||||||
if err != nil && !errors.Ast(err, errors.TypeNotFound) {
|
|
||||||
return nil, basemodel.NotFoundError(fmt.Errorf("couldn't look for integration user: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if integrationUserResult != nil {
|
|
||||||
return &integrationUserResult.User, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
zap.L().Info(
|
|
||||||
"cloud integration user not found. Attempting to create the user",
|
|
||||||
zap.String("cloudProvider", cloudProvider),
|
|
||||||
)
|
|
||||||
|
|
||||||
newUser, err := types.NewUser(cloudIntegrationUser, email, types.RoleViewer.String(), orgId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, basemodel.InternalError(fmt.Errorf(
|
|
||||||
"couldn't create cloud integration user: %w", err,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
password := types.MustGenerateFactorPassword(newUser.ID.StringValue())
|
|
||||||
|
|
||||||
err = ah.Signoz.Modules.User.CreateUser(ctx, newUser, user.WithFactorPassword(password))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUser, nil
|
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
|
||||||
|
|
||||||
|
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
|
||||||
|
if err != nil {
|
||||||
|
return nil, basemodel.InternalError(fmt.Errorf("couldn't look for integration user: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloudIntegrationUser, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
func getIngestionUrlAndSigNozAPIUrl(ctx context.Context, licenseKey string) (
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof" // http profiler
|
_ "net/http/pprof" // http profiler
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
|
||||||
|
"go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
|
|
||||||
@@ -25,7 +28,6 @@ import (
|
|||||||
"github.com/SigNoz/signoz/pkg/signoz"
|
"github.com/SigNoz/signoz/pkg/signoz"
|
||||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
||||||
"github.com/SigNoz/signoz/pkg/web"
|
"github.com/SigNoz/signoz/pkg/web"
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
"github.com/soheilhy/cmux"
|
"github.com/soheilhy/cmux"
|
||||||
@@ -50,7 +52,6 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
config signoz.Config
|
config signoz.Config
|
||||||
signoz *signoz.SigNoz
|
signoz *signoz.SigNoz
|
||||||
jwt *authtypes.JWT
|
|
||||||
ruleManager *baserules.Manager
|
ruleManager *baserules.Manager
|
||||||
|
|
||||||
// public http router
|
// public http router
|
||||||
@@ -67,7 +68,7 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates and initializes Server
|
// NewServer creates and initializes Server
|
||||||
func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT) (*Server, error) {
|
func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||||
gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
|
gatewayProxy, err := gateway.NewProxy(config.Gateway.URL.String(), gateway.RoutePrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -153,7 +154,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
|||||||
FluxInterval: config.Querier.FluxInterval,
|
FluxInterval: config.Querier.FluxInterval,
|
||||||
Gateway: gatewayProxy,
|
Gateway: gatewayProxy,
|
||||||
GatewayUrl: config.Gateway.URL.String(),
|
GatewayUrl: config.Gateway.URL.String(),
|
||||||
JWT: jwt,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
|
apiHandler, err := api.NewAPIHandler(apiOpts, signoz)
|
||||||
@@ -164,7 +164,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz, jwt *authtypes.JWT)
|
|||||||
s := &Server{
|
s := &Server{
|
||||||
config: config,
|
config: config,
|
||||||
signoz: signoz,
|
signoz: signoz,
|
||||||
jwt: jwt,
|
|
||||||
ruleManager: rm,
|
ruleManager: rm,
|
||||||
httpHostPort: baseconst.HTTPHostPort,
|
httpHostPort: baseconst.HTTPHostPort,
|
||||||
unavailableChannel: make(chan healthcheck.Status),
|
unavailableChannel: make(chan healthcheck.Status),
|
||||||
@@ -195,7 +194,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
|
|||||||
r := baseapp.NewRouter()
|
r := baseapp.NewRouter()
|
||||||
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
|
am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger())
|
||||||
|
|
||||||
r.Use(middleware.NewAuth(s.jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Instrumentation.Logger()).Wrap)
|
r.Use(otelmux.Middleware(
|
||||||
|
"apiserver",
|
||||||
|
otelmux.WithMeterProvider(s.signoz.Instrumentation.MeterProvider()),
|
||||||
|
otelmux.WithTracerProvider(s.signoz.Instrumentation.TracerProvider()),
|
||||||
|
otelmux.WithPropagators(propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})),
|
||||||
|
otelmux.WithFilter(func(r *http.Request) bool {
|
||||||
|
return !slices.Contains([]string{"/api/v1/health"}, r.URL.Path)
|
||||||
|
}),
|
||||||
|
otelmux.WithPublicEndpoint(),
|
||||||
|
))
|
||||||
|
r.Use(middleware.NewAuthN([]string{"Authorization", "Sec-WebSocket-Protocol"}, s.signoz.Sharder, s.signoz.Tokenizer, s.signoz.Instrumentation.Logger()).Wrap)
|
||||||
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
r.Use(middleware.NewAPIKey(s.signoz.SQLStore, []string{"SIGNOZ-API-KEY"}, s.signoz.Instrumentation.Logger(), s.signoz.Sharder).Wrap)
|
||||||
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
r.Use(middleware.NewTimeout(s.signoz.Instrumentation.Logger(),
|
||||||
s.config.APIServer.Timeout.ExcludedRoutes,
|
s.config.APIServer.Timeout.ExcludedRoutes,
|
||||||
|
|||||||
@@ -4,10 +4,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultSiteURL = "https://localhost:8080"
|
|
||||||
)
|
|
||||||
|
|
||||||
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
var LicenseSignozIo = "https://license.signoz.io/api/v1"
|
||||||
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
|
||||||
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
|
||||||
@@ -27,13 +23,6 @@ func GetOrDefaultEnv(key string, fallback string) string {
|
|||||||
|
|
||||||
// constant functions that override env vars
|
// constant functions that override env vars
|
||||||
|
|
||||||
// GetDefaultSiteURL returns default site url, primarily
|
|
||||||
// used to send saml request and allowing backend to
|
|
||||||
// handle http redirect
|
|
||||||
func GetDefaultSiteURL() string {
|
|
||||||
return GetOrDefaultEnv("SIGNOZ_SITE_URL", DefaultSiteURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
|
||||||
|
|
||||||
var IsDotMetricsEnabled = false
|
var IsDotMetricsEnabled = false
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
|||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
|
||||||
const afterLogin = (
|
const afterLogin = (
|
||||||
userId: string,
|
|
||||||
authToken: string,
|
authToken: string,
|
||||||
refreshToken: string,
|
refreshToken: string,
|
||||||
interceptorRejected?: boolean,
|
interceptorRejected?: boolean,
|
||||||
): void => {
|
): void => {
|
||||||
setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, authToken);
|
setLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN, authToken);
|
||||||
setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, refreshToken);
|
setLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN, refreshToken);
|
||||||
setLocalStorageApi(LOCALSTORAGE.USER_ID, userId);
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
setLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN, 'true');
|
||||||
|
|
||||||
if (!interceptorRejected) {
|
if (!interceptorRejected) {
|
||||||
@@ -18,7 +16,6 @@ const afterLogin = (
|
|||||||
detail: {
|
detail: {
|
||||||
accessJWT: authToken,
|
accessJWT: authToken,
|
||||||
refreshJWT: refreshToken,
|
refreshJWT: refreshToken,
|
||||||
id: userId,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
|
||||||
import { isEmpty } from 'lodash-es';
|
|
||||||
|
|
||||||
export interface WsDataEvent {
|
|
||||||
read_rows: number;
|
|
||||||
read_bytes: number;
|
|
||||||
elapsed_ms: number;
|
|
||||||
}
|
|
||||||
interface GetQueryStatsProps {
|
|
||||||
queryId: string;
|
|
||||||
setData: React.Dispatch<React.SetStateAction<WsDataEvent | undefined>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getURL(baseURL: string, queryId: string): URL | string {
|
|
||||||
if (baseURL && !isEmpty(baseURL)) {
|
|
||||||
return `${baseURL}/ws/query_progress?q=${queryId}`;
|
|
||||||
}
|
|
||||||
const url = new URL(`/ws/query_progress?q=${queryId}`, window.location.href);
|
|
||||||
|
|
||||||
if (window.location.protocol === 'http:') {
|
|
||||||
url.protocol = 'ws';
|
|
||||||
} else {
|
|
||||||
url.protocol = 'wss';
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getQueryStats(props: GetQueryStatsProps): void {
|
|
||||||
const { queryId, setData } = props;
|
|
||||||
|
|
||||||
const token = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN) || '';
|
|
||||||
|
|
||||||
// https://github.com/whatwg/websockets/issues/20 reason for not using the relative URLs
|
|
||||||
const url = getURL(ENVIRONMENT.wsURL, queryId);
|
|
||||||
|
|
||||||
const socket = new WebSocket(url, token);
|
|
||||||
|
|
||||||
socket.addEventListener('message', (event) => {
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(event?.data);
|
|
||||||
setData(parsedData);
|
|
||||||
} catch {
|
|
||||||
setData(event?.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener('error', (event) => {
|
|
||||||
console.error(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener('close', (event) => {
|
|
||||||
// 1000 is a normal closure status code
|
|
||||||
if (event.code !== 1000) {
|
|
||||||
console.error('WebSocket closed with error:', event);
|
|
||||||
} else {
|
|
||||||
console.error('WebSocket closed normally.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import loginApi from 'api/v1/login/login';
|
import post from 'api/v2/sessions/rotate/post';
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import axios, {
|
import axios, {
|
||||||
AxiosError,
|
AxiosError,
|
||||||
@@ -12,6 +12,7 @@ import axios, {
|
|||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
import { Events } from 'constants/events';
|
import { Events } from 'constants/events';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
|
import { QueryClient } from 'react-query';
|
||||||
import { eventEmitter } from 'utils/getEventEmitter';
|
import { eventEmitter } from 'utils/getEventEmitter';
|
||||||
|
|
||||||
import apiV1, {
|
import apiV1, {
|
||||||
@@ -26,6 +27,14 @@ import apiV1, {
|
|||||||
import { Logout } from './utils';
|
import { Logout } from './utils';
|
||||||
|
|
||||||
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
|
const RESPONSE_TIMEOUT_THRESHOLD = 5000; // 5 seconds
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const interceptorsResponse = (
|
const interceptorsResponse = (
|
||||||
value: AxiosResponse<any>,
|
value: AxiosResponse<any>,
|
||||||
@@ -74,19 +83,24 @@ const interceptorRejected = async (
|
|||||||
try {
|
try {
|
||||||
if (axios.isAxiosError(value) && value.response) {
|
if (axios.isAxiosError(value) && value.response) {
|
||||||
const { response } = value;
|
const { response } = value;
|
||||||
// reject the refresh token error
|
|
||||||
if (response.status === 401 && response.config.url !== '/login') {
|
if (
|
||||||
|
response.status === 401 &&
|
||||||
|
// if the session rotate call errors out with 401 or the delete sessions call returns 401 then we do not retry!
|
||||||
|
response.config.url !== '/sessions/rotate' &&
|
||||||
|
!(
|
||||||
|
response.config.url === '/sessions' && response.config.method === 'delete'
|
||||||
|
)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const response = await loginApi({
|
const accessToken = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN);
|
||||||
refreshToken: getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN) || '',
|
const refreshToken = getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN);
|
||||||
|
const response = await queryClient.fetchQuery({
|
||||||
|
queryFn: () => post({ refreshToken: refreshToken || '' }),
|
||||||
|
queryKey: ['/api/v2/sessions/rotate', accessToken, refreshToken],
|
||||||
});
|
});
|
||||||
|
|
||||||
afterLogin(
|
afterLogin(response.data.accessToken, response.data.refreshToken, true);
|
||||||
response.data.userId,
|
|
||||||
response.data.accessJwt,
|
|
||||||
response.data.refreshJwt,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const reResponse = await axios(
|
const reResponse = await axios(
|
||||||
@@ -95,7 +109,7 @@ const interceptorRejected = async (
|
|||||||
method: value.config.method,
|
method: value.config.method,
|
||||||
headers: {
|
headers: {
|
||||||
...value.config.headers,
|
...value.config.headers,
|
||||||
Authorization: `Bearer ${response.data.accessJwt}`,
|
Authorization: `Bearer ${response.data.accessToken}`,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
...JSON.parse(value.config.data || '{}'),
|
...JSON.parse(value.config.data || '{}'),
|
||||||
@@ -113,8 +127,8 @@ const interceptorRejected = async (
|
|||||||
Logout();
|
Logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// when refresh token is expired
|
|
||||||
if (response.status === 401 && response.config.url === '/login') {
|
if (response.status === 401 && response.config.url === '/sessions/rotate') {
|
||||||
Logout();
|
Logout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,15 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
|
|
||||||
export const Logout = (): void => {
|
import deleteSession from './v2/sessions/delete';
|
||||||
|
|
||||||
|
export const Logout = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await deleteSession();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN);
|
deleteLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN);
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.IS_LOGGED_IN);
|
deleteLocalStorageKey(LOCALSTORAGE.IS_LOGGED_IN);
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
deleteLocalStorageKey(LOCALSTORAGE.IS_IDENTIFIED_USER);
|
||||||
@@ -14,7 +22,6 @@ export const Logout = (): void => {
|
|||||||
deleteLocalStorageKey(LOCALSTORAGE.USER_ID);
|
deleteLocalStorageKey(LOCALSTORAGE.USER_ID);
|
||||||
deleteLocalStorageKey(LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT);
|
deleteLocalStorageKey(LOCALSTORAGE.QUICK_FILTERS_SETTINGS_ANNOUNCEMENT);
|
||||||
window.dispatchEvent(new CustomEvent('LOGOUT'));
|
window.dispatchEvent(new CustomEvent('LOGOUT'));
|
||||||
|
|
||||||
history.push(ROUTES.LOGIN);
|
history.push(ROUTES.LOGIN);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import axios from 'api';
|
|||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
import { PayloadProps, Props } from 'types/api/SAML/deleteDomain';
|
|
||||||
|
|
||||||
const deleteDomain = async (props: Props): Promise<SuccessResponseV2<null>> => {
|
const deleteDomain = async (id: string): Promise<SuccessResponseV2<null>> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.delete<PayloadProps>(`/domains/${props.id}`);
|
const response = await axios.delete<null>(`/domains/${id}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
httpStatusCode: response.status,
|
||||||
25
frontend/src/api/v1/domains/id/put.ts
Normal file
25
frontend/src/api/v1/domains/id/put.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { UpdatableAuthDomain } from 'types/api/v1/domains/put';
|
||||||
|
|
||||||
|
const put = async (
|
||||||
|
props: UpdatableAuthDomain,
|
||||||
|
): Promise<SuccessResponseV2<null>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put<RawSuccessResponse<null>>(
|
||||||
|
`/domains/${props.id}`,
|
||||||
|
{ config: props.config },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default put;
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
import { AuthDomain, PayloadProps } from 'types/api/SAML/listDomain';
|
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||||
|
|
||||||
const listAllDomain = async (): Promise<SuccessResponseV2<AuthDomain[]>> => {
|
const listAllDomain = async (): Promise<
|
||||||
|
SuccessResponseV2<GettableAuthDomain[]>
|
||||||
|
> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get<PayloadProps>(`/domains`);
|
const response = await axios.get<RawSuccessResponse<GettableAuthDomain[]>>(
|
||||||
|
`/domains`,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
httpStatusCode: response.status,
|
||||||
|
|||||||
26
frontend/src/api/v1/domains/post.ts
Normal file
26
frontend/src/api/v1/domains/post.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||||
|
import { PostableAuthDomain } from 'types/api/v1/domains/post';
|
||||||
|
|
||||||
|
const post = async (
|
||||||
|
props: PostableAuthDomain,
|
||||||
|
): Promise<SuccessResponseV2<GettableAuthDomain>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<RawSuccessResponse<GettableAuthDomain>>(
|
||||||
|
`/domains`,
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default post;
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { AuthDomain } from 'types/api/SAML/listDomain';
|
|
||||||
import { PayloadProps, Props } from 'types/api/SAML/updateDomain';
|
|
||||||
|
|
||||||
const updateDomain = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponseV2<AuthDomain>> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.put<PayloadProps>(`/domains/${props.id}`, props);
|
|
||||||
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default updateDomain;
|
|
||||||
@@ -2,15 +2,12 @@ import axios from 'api';
|
|||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
import {
|
import { PayloadProps, Props } from 'types/api/user/accept';
|
||||||
LoginPrecheckResponse,
|
import { UserResponse } from 'types/api/user/getUser';
|
||||||
PayloadProps,
|
|
||||||
Props,
|
|
||||||
} from 'types/api/user/accept';
|
|
||||||
|
|
||||||
const accept = async (
|
const accept = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponseV2<LoginPrecheckResponse>> => {
|
): Promise<SuccessResponseV2<UserResponse>> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<PayloadProps>(`/invite/accept`, props);
|
const response = await axios.post<PayloadProps>(`/invite/accept`, props);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { Props, Signup as PayloadProps } from 'types/api/user/loginPrecheck';
|
|
||||||
|
|
||||||
const loginPrecheck = async (
|
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`/loginPrecheck?email=${encodeURIComponent(
|
|
||||||
props.email,
|
|
||||||
)}&ref=${encodeURIComponent(window.location.href)}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.statusText,
|
|
||||||
payload: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default loginPrecheck;
|
|
||||||
27
frontend/src/api/v1/register/post.ts
Normal file
27
frontend/src/api/v1/register/post.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { Props } from 'types/api/user/signup';
|
||||||
|
import { SignupResponse } from 'types/api/v1/register/post';
|
||||||
|
|
||||||
|
const post = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponseV2<SignupResponse>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<RawSuccessResponse<SignupResponse>>(
|
||||||
|
`/register`,
|
||||||
|
{
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default post;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
|
||||||
import { PayloadProps, Signup } from 'types/api/user/loginPrecheck';
|
|
||||||
import { Props } from 'types/api/user/signup';
|
|
||||||
|
|
||||||
const signup = async (props: Props): Promise<SuccessResponseV2<Signup>> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post<PayloadProps>(`/register`, {
|
|
||||||
...props,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
httpStatusCode: response.status,
|
|
||||||
data: response.data.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default signup;
|
|
||||||
@@ -2,15 +2,11 @@ import axios from 'api';
|
|||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
import { PayloadProps, Props, UserLoginResponse } from 'types/api/user/login';
|
import { PayloadProps, UserResponse } from 'types/api/user/getUser';
|
||||||
|
|
||||||
const login = async (
|
const get = async (): Promise<SuccessResponseV2<UserResponse>> => {
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponseV2<UserLoginResponse>> => {
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<PayloadProps>(`/login`, {
|
const response = await axios.get<PayloadProps>(`/user/me`);
|
||||||
...props,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
httpStatusCode: response.status,
|
||||||
@@ -21,4 +17,4 @@ const login = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default login;
|
export default get;
|
||||||
@@ -2,20 +2,19 @@ import axios from 'api';
|
|||||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||||
import { AuthDomain } from 'types/api/SAML/listDomain';
|
import { Info } from 'types/api/v1/version/get';
|
||||||
import { PayloadProps, Props } from 'types/api/SAML/postDomain';
|
|
||||||
|
|
||||||
const create = async (props: Props): Promise<SuccessResponseV2<AuthDomain>> => {
|
const get = async (): Promise<SuccessResponseV2<Info>> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post<PayloadProps>(`/domains`, props);
|
const response = await axios.get<Info>(`/version`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpStatusCode: response.status,
|
httpStatusCode: response.status,
|
||||||
data: response.data.data,
|
data: response.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default create;
|
export default get;
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import axios from 'api';
|
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { getVersion } from 'constants/api';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps } from 'types/api/user/getVersion';
|
|
||||||
|
|
||||||
const getVersionApi = async (): Promise<
|
|
||||||
SuccessResponse<PayloadProps> | ErrorResponse
|
|
||||||
> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`/${getVersion}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getVersionApi;
|
|
||||||
27
frontend/src/api/v2/sessions/context/get.ts
Normal file
27
frontend/src/api/v2/sessions/context/get.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { Props, SessionsContext } from 'types/api/v2/sessions/context/get';
|
||||||
|
|
||||||
|
const get = async (
|
||||||
|
props: Props,
|
||||||
|
): Promise<SuccessResponseV2<SessionsContext>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<RawSuccessResponse<SessionsContext>>(
|
||||||
|
'/sessions/context',
|
||||||
|
{
|
||||||
|
params: props,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default get;
|
||||||
19
frontend/src/api/v2/sessions/delete.ts
Normal file
19
frontend/src/api/v2/sessions/delete.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
|
|
||||||
|
const deleteSession = async (): Promise<SuccessResponseV2<null>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete<RawSuccessResponse<null>>('/sessions');
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default deleteSession;
|
||||||
23
frontend/src/api/v2/sessions/email_password/post.ts
Normal file
23
frontend/src/api/v2/sessions/email_password/post.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { Props, Token } from 'types/api/v2/sessions/email_password/post';
|
||||||
|
|
||||||
|
const post = async (props: Props): Promise<SuccessResponseV2<Token>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<RawSuccessResponse<Token>>(
|
||||||
|
'/sessions/email_password',
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default post;
|
||||||
23
frontend/src/api/v2/sessions/rotate/post.ts
Normal file
23
frontend/src/api/v2/sessions/rotate/post.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ApiV2Instance as axios } from 'api';
|
||||||
|
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorV2Resp, RawSuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||||
|
import { Props, Token } from 'types/api/v2/sessions/rotate/post';
|
||||||
|
|
||||||
|
const post = async (props: Props): Promise<SuccessResponseV2<Token>> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post<RawSuccessResponse<Token>>(
|
||||||
|
'/sessions/rotate',
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpStatusCode: response.status,
|
||||||
|
data: response.data.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default post;
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
const SOMETHING_WENT_WRONG = 'Something went wrong';
|
const SOMETHING_WENT_WRONG = 'Something went wrong';
|
||||||
|
|
||||||
const getVersion = 'version';
|
export { SOMETHING_WENT_WRONG };
|
||||||
|
|
||||||
export { getVersion, SOMETHING_WENT_WRONG };
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import EndPointDetails from '../Explorer/Domains/DomainDetails/EndPointDetails';
|
|||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock('react-query', () => ({
|
jest.mock('react-query', () => ({
|
||||||
|
...jest.requireActual('react-query'),
|
||||||
useQueries: jest.fn(),
|
useQueries: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ jest.mock(
|
|||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
jest.mock('react-query', () => ({
|
jest.mock('react-query', () => ({
|
||||||
|
...jest.requireActual('react-query'),
|
||||||
useQueries: jest.fn(),
|
useQueries: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import manageCreditCardApi from 'api/v1/portal/create';
|
import manageCreditCardApi from 'api/v1/portal/create';
|
||||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||||
|
import getUserVersion from 'api/v1/version/get';
|
||||||
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||||
import getUserVersion from 'api/v1/version/getVersion';
|
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
||||||
@@ -317,14 +317,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getUserVersionResponse.isFetched &&
|
getUserVersionResponse.isFetched &&
|
||||||
getUserVersionResponse.isSuccess &&
|
getUserVersionResponse.isSuccess &&
|
||||||
getUserVersionResponse.data &&
|
getUserVersionResponse.data &&
|
||||||
getUserVersionResponse.data.payload
|
getUserVersionResponse.data.data
|
||||||
) {
|
) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UPDATE_CURRENT_VERSION,
|
type: UPDATE_CURRENT_VERSION,
|
||||||
payload: {
|
payload: {
|
||||||
currentVersion: getUserVersionResponse.data.payload.version,
|
currentVersion: getUserVersionResponse.data.data.version,
|
||||||
ee: getUserVersionResponse.data.payload.ee,
|
ee: getUserVersionResponse.data.data.ee,
|
||||||
setupCompleted: getUserVersionResponse.data.payload.setupCompleted,
|
setupCompleted: getUserVersionResponse.data.data.setupCompleted,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
|||||||
|
|
||||||
const mockUseQuery = jest.fn();
|
const mockUseQuery = jest.fn();
|
||||||
jest.mock('react-query', () => ({
|
jest.mock('react-query', () => ({
|
||||||
|
...jest.requireActual('react-query'),
|
||||||
useQuery: (queryKey: any, queryFn: any, options: any): any =>
|
useQuery: (queryKey: any, queryFn: any, options: any): any =>
|
||||||
mockUseQuery(queryKey, queryFn, options),
|
mockUseQuery(queryKey, queryFn, options),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ jest.mock('container/InfraMonitoringK8s/commonUtils', () => ({
|
|||||||
const mockUseQueries = jest.fn();
|
const mockUseQueries = jest.fn();
|
||||||
const mockUseQuery = jest.fn();
|
const mockUseQuery = jest.fn();
|
||||||
jest.mock('react-query', () => ({
|
jest.mock('react-query', () => ({
|
||||||
|
...jest.requireActual('react-query'),
|
||||||
useQueries: (queryConfigs: any[]): any[] => mockUseQueries(queryConfigs),
|
useQueries: (queryConfigs: any[]): any[] => mockUseQueries(queryConfigs),
|
||||||
useQuery: (config: any): any => mockUseQuery(config),
|
useQuery: (config: any): any => mockUseQuery(config),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
|||||||
|
|
||||||
const mockUseQuery = jest.fn();
|
const mockUseQuery = jest.fn();
|
||||||
jest.mock('react-query', () => ({
|
jest.mock('react-query', () => ({
|
||||||
|
...jest.requireActual('react-query'),
|
||||||
useQuery: (queryKey: any, queryFn: any, options: any): any =>
|
useQuery: (queryKey: any, queryFn: any, options: any): any =>
|
||||||
mockUseQuery(queryKey, queryFn, options),
|
mockUseQuery(queryKey, queryFn, options),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,118 +1,830 @@
|
|||||||
import Login from 'container/Login';
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
import ROUTES from 'constants/routes';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { rest, server } from 'mocks-server/server';
|
||||||
|
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||||
|
import { ErrorV2 } from 'types/api';
|
||||||
|
import { Info } from 'types/api/v1/version/get';
|
||||||
|
import { SessionsContext } from 'types/api/v2/sessions/context/get';
|
||||||
|
import { Token } from 'types/api/v2/sessions/email_password/post';
|
||||||
|
|
||||||
const errorNotification = jest.fn();
|
import Login from '../index';
|
||||||
jest.mock('hooks/useNotifications', () => ({
|
|
||||||
|
const VERSION_ENDPOINT = '*/api/v1/version';
|
||||||
|
const SESSIONS_CONTEXT_ENDPOINT = '*/api/v2/sessions/context';
|
||||||
|
const CALLBACK_AUTHN_ORG = 'callback_authn_org';
|
||||||
|
const CALLBACK_AUTHN_URL = 'https://sso.example.com/auth';
|
||||||
|
const PASSWORD_AUTHN_ORG = 'password_authn_org';
|
||||||
|
const PASSWORD_AUTHN_EMAIL = 'jest.test@signoz.io';
|
||||||
|
|
||||||
|
jest.mock('lib/history', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
useNotifications: jest.fn(() => ({
|
default: {
|
||||||
notifications: {
|
push: jest.fn(),
|
||||||
error: errorNotification,
|
location: {
|
||||||
|
search: '',
|
||||||
},
|
},
|
||||||
})),
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('Login Flow', () => {
|
const mockHistoryPush = history.push as jest.MockedFunction<
|
||||||
test('Login form is rendered correctly', async () => {
|
typeof history.push
|
||||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
>;
|
||||||
|
|
||||||
// Check for the main description
|
// Mock data
|
||||||
expect(
|
const mockVersionSetupCompleted: Info = {
|
||||||
screen.getByText(
|
setupCompleted: true,
|
||||||
'Sign in to monitor, trace, and troubleshoot your applications effortlessly.',
|
ee: 'Y',
|
||||||
),
|
version: '0.25.0',
|
||||||
).toBeInTheDocument();
|
};
|
||||||
|
|
||||||
// Email input
|
const mockVersionSetupIncomplete: Info = {
|
||||||
const emailInput = screen.getByTestId('email');
|
setupCompleted: false,
|
||||||
expect(emailInput).toBeInTheDocument();
|
ee: 'Y',
|
||||||
expect(emailInput).toHaveAttribute('type', 'email');
|
version: '0.25.0',
|
||||||
|
};
|
||||||
|
|
||||||
// Next button
|
const mockSingleOrgPasswordAuth: SessionsContext = {
|
||||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
exists: true,
|
||||||
expect(nextButton).toBeInTheDocument();
|
orgs: [
|
||||||
|
{
|
||||||
|
id: 'org-1',
|
||||||
|
name: 'Test Organization',
|
||||||
|
authNSupport: {
|
||||||
|
password: [{ provider: 'email_password' }],
|
||||||
|
callback: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// No account prompt (default: canSelfRegister is false)
|
const mockSingleOrgCallbackAuth: SessionsContext = {
|
||||||
expect(
|
exists: true,
|
||||||
screen.getByText(
|
orgs: [
|
||||||
"Don't have an account? Contact your admin to send you an invite link.",
|
{
|
||||||
),
|
id: 'org-1',
|
||||||
).toBeInTheDocument();
|
name: 'Test Organization',
|
||||||
});
|
authNSupport: {
|
||||||
|
password: [],
|
||||||
|
callback: [{ provider: 'google', url: CALLBACK_AUTHN_URL }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
test('Display error if email is not provided', async () => {
|
const mockMultiOrgMixedAuth: SessionsContext = {
|
||||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
exists: true,
|
||||||
|
orgs: [
|
||||||
|
{
|
||||||
|
id: 'org-1',
|
||||||
|
name: PASSWORD_AUTHN_ORG,
|
||||||
|
authNSupport: {
|
||||||
|
password: [{ provider: 'email_password' }],
|
||||||
|
callback: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'org-2',
|
||||||
|
name: CALLBACK_AUTHN_ORG,
|
||||||
|
authNSupport: {
|
||||||
|
password: [],
|
||||||
|
callback: [{ provider: 'google', url: CALLBACK_AUTHN_URL }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
const mockOrgWithWarning: SessionsContext = {
|
||||||
fireEvent.click(nextButton);
|
exists: true,
|
||||||
|
orgs: [
|
||||||
|
{
|
||||||
|
id: 'org-1',
|
||||||
|
name: 'Warning Organization',
|
||||||
|
authNSupport: {
|
||||||
|
password: [{ provider: 'email_password' }],
|
||||||
|
callback: [],
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
code: 'ORG_WARNING',
|
||||||
|
message: 'Organization has limited access',
|
||||||
|
url: 'https://example.com/warning',
|
||||||
|
errors: [{ message: 'Contact admin for full access' }],
|
||||||
|
} as ErrorV2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
await waitFor(() =>
|
const mockEmailPasswordResponse: Token = {
|
||||||
expect(errorNotification).toHaveBeenCalledWith({
|
accessToken: 'mock-access-token',
|
||||||
message: 'Please enter a valid email address',
|
refreshToken: 'mock-refresh-token',
|
||||||
}),
|
};
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Display error if invalid email is provided and next clicked', async () => {
|
describe('Login Component', () => {
|
||||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
const emailInput = screen.getByTestId('email');
|
server.use(
|
||||||
fireEvent.change(emailInput, {
|
rest.get(VERSION_ENDPOINT, (_, res, ctx) =>
|
||||||
target: { value: 'failEmail@signoz.io' },
|
res(
|
||||||
});
|
ctx.status(200),
|
||||||
|
ctx.json({ data: mockVersionSetupCompleted, status: 'success' }),
|
||||||
const nextButton = screen.getByRole('button', { name: /next/i });
|
|
||||||
fireEvent.click(nextButton);
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(errorNotification).toHaveBeenCalledWith({
|
|
||||||
message:
|
|
||||||
'Invalid configuration detected, please contact your administrator',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('providing shaheer@signoz.io as email and pressing next, should make the Login with SSO button visible', async () => {
|
|
||||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="" />);
|
|
||||||
act(() => {
|
|
||||||
fireEvent.change(screen.getByTestId('email'), {
|
|
||||||
target: { value: 'shaheer@signoz.io' },
|
|
||||||
});
|
|
||||||
fireEvent.click(screen.getByTestId('initiate_login'));
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(screen.getByText(/login with sso/i)).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Display email, password, forgot password if password=Y', () => {
|
|
||||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="Y" />);
|
|
||||||
|
|
||||||
const emailInput = screen.getByTestId('email');
|
|
||||||
expect(emailInput).toBeInTheDocument();
|
|
||||||
|
|
||||||
const passwordInput = screen.getByTestId('password');
|
|
||||||
expect(passwordInput).toBeInTheDocument();
|
|
||||||
|
|
||||||
const forgotPasswordLink = screen.getByText('Forgot password?');
|
|
||||||
expect(forgotPasswordLink).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Display tooltip with correct message if forgot password is hovered while password=Y', async () => {
|
|
||||||
render(<Login ssoerror="" jwt="" refreshjwt="" userId="" withPassword="Y" />);
|
|
||||||
const forgotPasswordLink = screen.getByText('Forgot password?');
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
fireEvent.mouseOver(forgotPasswordLink);
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
// Tooltip text is static in the new UI
|
|
||||||
expect(
|
|
||||||
screen.getByText(
|
|
||||||
'Ask your admin to reset your password and send you a new invite link',
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
server.resetHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Initial Render', () => {
|
||||||
|
it('renders login form with email input and next button', () => {
|
||||||
|
const { getByTestId, getByPlaceholderText } = render(<Login />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(/sign in to monitor, trace, and troubleshoot/i),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
expect(getByTestId('email')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('initiate_login')).toBeInTheDocument();
|
||||||
|
expect(getByPlaceholderText('name@yourcompany.com')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows loading state when version data is being fetched', () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(VERSION_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.delay(100),
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ data: mockVersionSetupCompleted, status: 'success' }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Login />);
|
||||||
|
|
||||||
|
expect(getByTestId('initiate_login')).toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Setup Check', () => {
|
||||||
|
it('redirects to signup when setup is not completed', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(VERSION_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ data: mockVersionSetupIncomplete, status: 'success' }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockHistoryPush).toHaveBeenCalledWith(ROUTES.SIGN_UP);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stays on login page when setup is completed', async () => {
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockHistoryPush).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles version API error gracefully', async () => {
|
||||||
|
server.use(
|
||||||
|
rest.get(VERSION_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(ctx.status(500), ctx.json({ error: 'Server error' })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockHistoryPush).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Session Context Fetching', () => {
|
||||||
|
it('fetches session context on next button click and enables password', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('password')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles session context API errors', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(500),
|
||||||
|
ctx.json({
|
||||||
|
error: {
|
||||||
|
code: 'internal_server',
|
||||||
|
message: 'couldnt fetch the sessions context',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId, getByText } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('couldnt fetch the sessions context')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto-selects organization when only one exists', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should show password field directly (no org selection needed)
|
||||||
|
expect(getByTestId('password')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(/organization name/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Organization Selection', () => {
|
||||||
|
it('shows organization dropdown when multiple orgs exist', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockMultiOrgMixedAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId, getByText } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('Organization Name')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on the dropdown to reveal the options
|
||||||
|
await user.click(screen.getByRole('combobox'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(PASSWORD_AUTHN_ORG)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(CALLBACK_AUTHN_ORG)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates selected organization on dropdown change', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json({ data: mockMultiOrgMixedAuth })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByTestId('email');
|
||||||
|
const nextButton = screen.getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select CALLBACK_AUTHN_ORG
|
||||||
|
await user.click(screen.getByRole('combobox'));
|
||||||
|
await user.click(screen.getByText(CALLBACK_AUTHN_ORG));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', { name: /login with callback/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Password Authentication', () => {
|
||||||
|
it('shows password field when password auth is supported', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId, getByText } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('password')).toBeInTheDocument();
|
||||||
|
expect(getByText(/forgot password/i)).toBeInTheDocument();
|
||||||
|
expect(getByTestId('password_authn_submit')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables password auth when URL parameter password=Y', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgCallbackAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Login />, undefined, {
|
||||||
|
initialRoute: '/login?password=Y',
|
||||||
|
});
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should show password field even for SSO org due to password=Y override
|
||||||
|
expect(getByTestId('password')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Callback Authentication', () => {
|
||||||
|
it('shows callback login button when callback auth is supported', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgCallbackAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId, queryByTestId } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('callback_authn_submit')).toBeInTheDocument();
|
||||||
|
expect(queryByTestId('password')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to callback URL on button click', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
// Mock window.location.href
|
||||||
|
const mockLocation = {
|
||||||
|
href: 'http://localhost/',
|
||||||
|
};
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: mockLocation,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgCallbackAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId, queryByTestId } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('callback_authn_submit')).toBeInTheDocument();
|
||||||
|
expect(queryByTestId('password')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const callbackButton = getByTestId('callback_authn_submit');
|
||||||
|
await user.click(callbackButton);
|
||||||
|
|
||||||
|
// Check that window.location.href was set to the callback URL
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(window.location.href).toBe(CALLBACK_AUTHN_URL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Password Authentication Execution', () => {
|
||||||
|
it('calls email/password API with correct parameters', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
rest.post('*/api/v2/sessions/email_password', async (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockEmailPasswordResponse }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('password')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordInput = getByTestId('password');
|
||||||
|
const loginButton = getByTestId('password_authn_submit');
|
||||||
|
|
||||||
|
await user.type(passwordInput, 'testpassword');
|
||||||
|
await user.click(loginButton);
|
||||||
|
|
||||||
|
// do not test for the request paramters here. Reference: https://mswjs.io/docs/best-practices/avoid-request-assertions
|
||||||
|
// rather test for the effects of the request
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(localStorage.getItem('AUTH_TOKEN')).toBe('mock-access-token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error modal on authentication failure', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockSingleOrgPasswordAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
rest.post('*/api/v2/sessions/email_password', (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(401),
|
||||||
|
ctx.json({
|
||||||
|
error: {
|
||||||
|
code: 'invalid_input',
|
||||||
|
message: 'invalid password',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId, getByText } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('password')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordInput = getByTestId('password');
|
||||||
|
const loginButton = getByTestId('password_authn_submit');
|
||||||
|
|
||||||
|
await user.type(passwordInput, 'wrongpassword');
|
||||||
|
await user.click(loginButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('invalid password')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('URL Parameter Handling', () => {
|
||||||
|
it('calls afterLogin when accessToken and refreshToken are in URL', async () => {
|
||||||
|
render(<Login />, undefined, {
|
||||||
|
initialRoute: '/login?accessToken=test-token&refreshToken=test-refresh',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(localStorage.getItem('AUTH_TOKEN')).toBe('test-token');
|
||||||
|
expect(localStorage.getItem('REFRESH_AUTH_TOKEN')).toBe('test-refresh');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error modal when callbackauthnerr parameter exists', async () => {
|
||||||
|
const { getByText } = render(<Login />, undefined, {
|
||||||
|
initialRoute:
|
||||||
|
'/login?callbackauthnerr=true&code=AUTH_ERROR&message=Authentication failed&url=https://example.com/error&errors=[{"message":"Invalid token"}]',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByText('AUTH_ERROR')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles malformed error JSON gracefully', async () => {
|
||||||
|
const { queryByText, getByText } = render(<Login />, undefined, {
|
||||||
|
initialRoute:
|
||||||
|
'/login?callbackauthnerr=true&code=AUTH_ERROR&message=Authentication failed&errors=invalid-json',
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByText('invalid-json')).not.toBeInTheDocument();
|
||||||
|
expect(getByText('AUTH_ERROR')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Session Organization Warnings', () => {
|
||||||
|
it('shows warning modal when org has warning', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockOrgWithWarning }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByTestId('email');
|
||||||
|
const nextButton = screen.getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/organization has limited access/i),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows warning modal when a warning org is selected among multiple orgs', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
// Mock multiple orgs including one with a warning
|
||||||
|
const mockMultiOrgWithWarning = {
|
||||||
|
orgs: [
|
||||||
|
{ id: 'org1', name: 'Org 1' },
|
||||||
|
{
|
||||||
|
id: 'org2',
|
||||||
|
name: 'Org 2',
|
||||||
|
warning: {
|
||||||
|
code: 'ORG_WARNING',
|
||||||
|
message: 'Organization has limited access',
|
||||||
|
url: 'https://example.com/warning',
|
||||||
|
errors: [{ message: 'Contact admin for full access' }],
|
||||||
|
} as ErrorV2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (_, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ status: 'success', data: mockMultiOrgWithWarning }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { getByTestId } = render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = getByTestId('email');
|
||||||
|
const nextButton = getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select the organization with a warning
|
||||||
|
await user.click(screen.getByRole('combobox'));
|
||||||
|
await user.click(screen.getByText('Org 2'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText(/organization has limited access/i),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Form State Management', () => {
|
||||||
|
it('disables form fields during loading states', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(
|
||||||
|
ctx.delay(100),
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({ data: mockSingleOrgPasswordAuth }),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByTestId('email');
|
||||||
|
const nextButton = screen.getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
// Button should be disabled during API call
|
||||||
|
expect(nextButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows correct button text for each auth method', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json({ data: mockSingleOrgPasswordAuth })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
// Initially shows "Next" button
|
||||||
|
expect(screen.getByTestId('initiate_login')).toBeInTheDocument();
|
||||||
|
|
||||||
|
const emailInput = screen.getByTestId('email');
|
||||||
|
const nextButton = screen.getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should show "Login" button for password auth
|
||||||
|
expect(screen.getByTestId('password_authn_submit')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId('initiate_login')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge Cases', () => {
|
||||||
|
it('handles user with no organizations', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
const mockNoOrgs: SessionsContext = {
|
||||||
|
exists: false,
|
||||||
|
orgs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json({ data: mockNoOrgs })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByTestId('email');
|
||||||
|
const nextButton = screen.getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should not show any auth method buttons
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('password_authn_submit'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('callback_authn_submit'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles organization with no auth support', async () => {
|
||||||
|
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||||
|
|
||||||
|
const mockNoAuthSupport: SessionsContext = {
|
||||||
|
exists: true,
|
||||||
|
orgs: [
|
||||||
|
{
|
||||||
|
id: 'org-1',
|
||||||
|
name: 'No Auth Organization',
|
||||||
|
authNSupport: {
|
||||||
|
password: [],
|
||||||
|
callback: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
server.use(
|
||||||
|
rest.get(SESSIONS_CONTEXT_ENDPOINT, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.json({ data: mockNoAuthSupport })),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
render(<Login />);
|
||||||
|
|
||||||
|
const emailInput = screen.getByTestId('email');
|
||||||
|
const nextButton = screen.getByTestId('initiate_login');
|
||||||
|
|
||||||
|
await user.type(emailInput, PASSWORD_AUTHN_EMAIL);
|
||||||
|
await user.click(nextButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should not show any auth method buttons
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('password_authn_submit'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('callback_authn_submit'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,213 +1,255 @@
|
|||||||
import './Login.styles.scss';
|
import './Login.styles.scss';
|
||||||
|
|
||||||
import { Button, Form, Input, Space, Tooltip, Typography } from 'antd';
|
import { Button, Form, Input, Select, Space, Tooltip, Typography } from 'antd';
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getVersion from 'api/v1/version/get';
|
||||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
import get from 'api/v2/sessions/context/get';
|
||||||
import loginApi from 'api/v1/login/login';
|
import post from 'api/v2/sessions/email_password/post';
|
||||||
import loginPrecheckApi from 'api/v1/login/loginPrecheck';
|
|
||||||
import getUserVersion from 'api/v1/version/getVersion';
|
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { ArrowRight } from 'lucide-react';
|
import { ArrowRight } from 'lucide-react';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
import { ErrorV2 } from 'types/api';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { Signup as PrecheckResultType } from 'types/api/user/loginPrecheck';
|
import { SessionsContext } from 'types/api/v2/sessions/context/get';
|
||||||
|
|
||||||
import { FormContainer, Label, ParentContainer } from './styles';
|
import { FormContainer, Label, ParentContainer } from './styles';
|
||||||
|
|
||||||
interface LoginProps {
|
function parseErrors(errors: string): { message: string }[] {
|
||||||
jwt: string;
|
try {
|
||||||
refreshjwt: string;
|
const parsedErrors = JSON.parse(errors);
|
||||||
userId: string;
|
return parsedErrors.map((error: { message: string }) => ({
|
||||||
ssoerror: string;
|
message: error.message,
|
||||||
withPassword: string;
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse errors:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormValues = { email: string; password: string };
|
type FormValues = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
orgId: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
function Login({
|
function Login(): JSX.Element {
|
||||||
jwt,
|
const urlQueryParams = useUrlQuery();
|
||||||
refreshjwt,
|
// override for callbackAuthN in case of some misconfiguration
|
||||||
userId,
|
const isPasswordAuthNEnabled = (urlQueryParams.get('password') || 'N') === 'Y';
|
||||||
ssoerror = '',
|
|
||||||
withPassword = '0',
|
|
||||||
}: LoginProps): JSX.Element {
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const { user } = useAppContext();
|
|
||||||
|
|
||||||
const [precheckResult, setPrecheckResult] = useState<PrecheckResultType>({
|
// callbackAuthN handling
|
||||||
sso: false,
|
const accessToken = urlQueryParams.get('accessToken') || '';
|
||||||
ssoUrl: '',
|
const refreshToken = urlQueryParams.get('refreshToken') || '';
|
||||||
canSelfRegister: false,
|
|
||||||
isUser: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [precheckInProcess, setPrecheckInProcess] = useState(false);
|
// callbackAuthN error handling
|
||||||
const [precheckComplete, setPrecheckComplete] = useState(false);
|
const callbackAuthError = urlQueryParams.get('callbackauthnerr') || '';
|
||||||
|
const callbackAuthErrorCode = urlQueryParams.get('code') || '';
|
||||||
|
const callbackAuthErrorMessage = urlQueryParams.get('message') || '';
|
||||||
|
const callbackAuthErrorURL = urlQueryParams.get('url') || '';
|
||||||
|
const callbackAuthErrorAdditional = urlQueryParams.get('errors') || '';
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const [sessionsContext, setSessionsContext] = useState<SessionsContext>();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
|
const [sessionsOrgId, setSessionsOrgId] = useState<string>('');
|
||||||
|
const [
|
||||||
|
sessionsContextLoading,
|
||||||
|
setIsLoadingSessionsContext,
|
||||||
|
] = useState<boolean>(false);
|
||||||
|
const [form] = Form.useForm<FormValues>();
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
const getUserVersionResponse = useQuery({
|
// setupCompleted information to route to signup page in case setup is incomplete
|
||||||
queryFn: getUserVersion,
|
const {
|
||||||
queryKey: ['getUserVersion', user?.accessJwt],
|
data: versionData,
|
||||||
|
isLoading: versionLoading,
|
||||||
|
error: versionError,
|
||||||
|
} = useQuery({
|
||||||
|
queryFn: getVersion,
|
||||||
|
queryKey: ['api/v1/version/get'],
|
||||||
enabled: true,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// in case of error do not route to signup page as it may lead to double registration
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
getUserVersionResponse.isFetched &&
|
versionData &&
|
||||||
getUserVersionResponse.data &&
|
!versionLoading &&
|
||||||
getUserVersionResponse.data.payload
|
!versionError &&
|
||||||
|
!versionData.data.setupCompleted
|
||||||
) {
|
) {
|
||||||
const { setupCompleted } = getUserVersionResponse.data.payload;
|
history.push(ROUTES.SIGN_UP);
|
||||||
if (!setupCompleted) {
|
|
||||||
// no org account registered yet, re-route user to sign up first
|
|
||||||
history.push(ROUTES.SIGN_UP);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [getUserVersionResponse]);
|
}, [versionData, versionLoading, versionError]);
|
||||||
|
|
||||||
const [form] = Form.useForm<FormValues>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (withPassword === 'Y') {
|
|
||||||
setPrecheckComplete(true);
|
|
||||||
}
|
|
||||||
}, [withPassword]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function processJwt(): Promise<void> {
|
|
||||||
if (jwt && jwt !== '') {
|
|
||||||
setIsLoading(true);
|
|
||||||
await afterLogin(userId, jwt, refreshjwt);
|
|
||||||
setIsLoading(false);
|
|
||||||
const fromPathname = getLocalStorageApi(
|
|
||||||
LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT,
|
|
||||||
);
|
|
||||||
if (fromPathname) {
|
|
||||||
history.push(fromPathname);
|
|
||||||
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, '');
|
|
||||||
} else {
|
|
||||||
history.push(ROUTES.APPLICATION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processJwt();
|
|
||||||
}, [jwt, refreshjwt, userId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ssoerror !== '') {
|
|
||||||
notifications.error({
|
|
||||||
message: 'sorry, failed to login',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [ssoerror, notifications]);
|
|
||||||
|
|
||||||
|
// fetch the sessions context post user entering the email
|
||||||
const onNextHandler = async (): Promise<void> => {
|
const onNextHandler = async (): Promise<void> => {
|
||||||
const email = form.getFieldValue('email');
|
const email = form.getFieldValue('email');
|
||||||
if (!email) {
|
setIsLoadingSessionsContext(true);
|
||||||
notifications.error({
|
|
||||||
message: 'Please enter a valid email address',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPrecheckInProcess(true);
|
|
||||||
try {
|
try {
|
||||||
const response = await loginPrecheckApi({
|
const sessionsContextResponse = await get({
|
||||||
email,
|
email,
|
||||||
|
ref: window.location.href,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode === 200) {
|
setSessionsContext(sessionsContextResponse.data);
|
||||||
setPrecheckResult({ ...precheckResult, ...response.payload });
|
if (sessionsContextResponse.data.orgs.length === 1) {
|
||||||
|
setSessionsOrgId(sessionsContextResponse.data.orgs[0].id);
|
||||||
const { isUser } = response.payload;
|
|
||||||
if (isUser) {
|
|
||||||
setPrecheckComplete(true);
|
|
||||||
} else {
|
|
||||||
notifications.error({
|
|
||||||
message:
|
|
||||||
'This account does not exist. To create a new account, contact your admin to get an invite link',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notifications.error({
|
|
||||||
message:
|
|
||||||
'Invalid configuration detected, please contact your administrator',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.log('failed to call precheck Api', e);
|
|
||||||
notifications.error({ message: 'Sorry, something went wrong' });
|
|
||||||
}
|
|
||||||
setPrecheckInProcess(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { sso, canSelfRegister } = precheckResult;
|
|
||||||
|
|
||||||
const onSubmitHandler: () => Promise<void> = async () => {
|
|
||||||
try {
|
|
||||||
const { email, password } = form.getFieldsValue();
|
|
||||||
if (!precheckComplete) {
|
|
||||||
onNextHandler();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (precheckComplete && sso) {
|
|
||||||
window.location.href = precheckResult.ssoUrl || '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const response = await loginApi({
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
|
|
||||||
afterLogin(
|
|
||||||
response.data.userId,
|
|
||||||
response.data.accessJwt,
|
|
||||||
response.data.refreshJwt,
|
|
||||||
);
|
|
||||||
setIsLoading(false);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsLoading(false);
|
showErrorModal(error as APIError);
|
||||||
notifications.error({
|
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
setIsLoadingSessionsContext(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSAMLAction = (): JSX.Element => (
|
// post selection of email and session org decide on the authN mechanism to use
|
||||||
<Button
|
const isPasswordAuthN = useMemo((): boolean => {
|
||||||
type="primary"
|
if (!sessionsContext) {
|
||||||
loading={isLoading}
|
return false;
|
||||||
disabled={isLoading}
|
}
|
||||||
href={precheckResult.ssoUrl}
|
|
||||||
>
|
|
||||||
Login with SSO
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderOnSsoError = (): JSX.Element | null => {
|
if (!sessionsOrgId) {
|
||||||
if (!ssoerror) {
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isPasswordAuthN = false;
|
||||||
|
sessionsContext.orgs.forEach((orgSession) => {
|
||||||
|
if (
|
||||||
|
orgSession.id === sessionsOrgId &&
|
||||||
|
orgSession.authNSupport?.password?.length > 0
|
||||||
|
) {
|
||||||
|
isPasswordAuthN = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isPasswordAuthN || isPasswordAuthNEnabled;
|
||||||
|
}, [sessionsContext, sessionsOrgId, isPasswordAuthNEnabled]);
|
||||||
|
|
||||||
|
const isCallbackAuthN = useMemo((): boolean => {
|
||||||
|
if (!sessionsContext) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sessionsOrgId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isCallbackAuthN = false;
|
||||||
|
sessionsContext.orgs.forEach((orgSession) => {
|
||||||
|
if (
|
||||||
|
orgSession.id === sessionsOrgId &&
|
||||||
|
orgSession.authNSupport?.callback?.length > 0
|
||||||
|
) {
|
||||||
|
isCallbackAuthN = true;
|
||||||
|
form.setFieldValue('url', orgSession.authNSupport.callback[0].url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return isCallbackAuthN && !isPasswordAuthNEnabled;
|
||||||
|
}, [sessionsContext, sessionsOrgId, isPasswordAuthNEnabled, form]);
|
||||||
|
|
||||||
|
const sessionsOrgWarning = useMemo((): ErrorV2 | null => {
|
||||||
|
if (!sessionsContext) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (!sessionsOrgId) {
|
||||||
<Typography.Paragraph italic style={{ color: '#ACACAC' }}>
|
return null;
|
||||||
Are you trying to resolve SSO configuration issue?{' '}
|
}
|
||||||
<a href="/login?password=Y">Login with password</a>.
|
|
||||||
</Typography.Paragraph>
|
let sessionsOrgWarning;
|
||||||
);
|
sessionsContext.orgs.forEach((orgSession) => {
|
||||||
|
if (orgSession.id === sessionsOrgId && orgSession.warning) {
|
||||||
|
sessionsOrgWarning = orgSession.warning;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return sessionsOrgWarning || null;
|
||||||
|
}, [sessionsContext, sessionsOrgId]);
|
||||||
|
|
||||||
|
// once the callback authN redirects to the login screen with access_token and refresh_token navigate them to homepage
|
||||||
|
useEffect(() => {
|
||||||
|
if (accessToken && refreshToken) {
|
||||||
|
afterLogin(accessToken, refreshToken);
|
||||||
|
}
|
||||||
|
}, [accessToken, refreshToken]);
|
||||||
|
|
||||||
|
const onSubmitHandler: () => Promise<void> = async () => {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isPasswordAuthN) {
|
||||||
|
const email = form.getFieldValue('email');
|
||||||
|
|
||||||
|
const password = form.getFieldValue('password');
|
||||||
|
|
||||||
|
const createSessionEmailPasswordResponse = await post({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
orgId: sessionsOrgId,
|
||||||
|
});
|
||||||
|
|
||||||
|
afterLogin(
|
||||||
|
createSessionEmailPasswordResponse.data.accessToken,
|
||||||
|
createSessionEmailPasswordResponse.data.refreshToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isCallbackAuthN) {
|
||||||
|
const url = form.getFieldValue('url');
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (callbackAuthError) {
|
||||||
|
showErrorModal(
|
||||||
|
new APIError({
|
||||||
|
httpStatusCode: 500,
|
||||||
|
error: {
|
||||||
|
code: callbackAuthErrorCode,
|
||||||
|
message: callbackAuthErrorMessage,
|
||||||
|
url: callbackAuthErrorURL,
|
||||||
|
errors: parseErrors(callbackAuthErrorAdditional),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
callbackAuthError,
|
||||||
|
callbackAuthErrorAdditional,
|
||||||
|
callbackAuthErrorCode,
|
||||||
|
callbackAuthErrorMessage,
|
||||||
|
callbackAuthErrorURL,
|
||||||
|
showErrorModal,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessionsOrgWarning) {
|
||||||
|
showErrorModal(
|
||||||
|
new APIError({
|
||||||
|
error: {
|
||||||
|
code: sessionsOrgWarning.code,
|
||||||
|
message: sessionsOrgWarning.message,
|
||||||
|
url: sessionsOrgWarning.url,
|
||||||
|
errors: sessionsOrgWarning.errors,
|
||||||
|
},
|
||||||
|
httpStatusCode: 400,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [sessionsOrgWarning, showErrorModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-form-container">
|
<div className="login-form-container">
|
||||||
<FormContainer form={form} onFinish={onSubmitHandler}>
|
<FormContainer form={form} onFinish={onSubmitHandler}>
|
||||||
@@ -225,17 +267,39 @@ function Login({
|
|||||||
<FormContainer.Item name="email">
|
<FormContainer.Item name="email">
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
id="loginEmail"
|
id="email"
|
||||||
data-testid="email"
|
data-testid="email"
|
||||||
required
|
required
|
||||||
placeholder="name@yourcompany.com"
|
placeholder="name@yourcompany.com"
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={isLoading}
|
disabled={versionLoading}
|
||||||
className="login-form-input"
|
className="login-form-input"
|
||||||
/>
|
/>
|
||||||
</FormContainer.Item>
|
</FormContainer.Item>
|
||||||
</ParentContainer>
|
</ParentContainer>
|
||||||
{precheckComplete && !sso && (
|
|
||||||
|
{sessionsContext && sessionsContext.orgs.length > 1 && (
|
||||||
|
<ParentContainer>
|
||||||
|
<Label htmlFor="orgId">Organization Name</Label>
|
||||||
|
<FormContainer.Item name="orgId">
|
||||||
|
<Select
|
||||||
|
id="orgId"
|
||||||
|
data-testid="orgId"
|
||||||
|
className="login-form-input"
|
||||||
|
placeholder="Select your organization"
|
||||||
|
options={sessionsContext.orgs.map((org) => ({
|
||||||
|
value: org.id,
|
||||||
|
label: org.name || 'default',
|
||||||
|
}))}
|
||||||
|
onChange={(value: string): void => {
|
||||||
|
setSessionsOrgId(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormContainer.Item>
|
||||||
|
</ParentContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessionsContext && isPasswordAuthN && (
|
||||||
<ParentContainer>
|
<ParentContainer>
|
||||||
<Label htmlFor="Password">Password</Label>
|
<Label htmlFor="Password">Password</Label>
|
||||||
<FormContainer.Item name="password">
|
<FormContainer.Item name="password">
|
||||||
@@ -243,7 +307,7 @@ function Login({
|
|||||||
required
|
required
|
||||||
id="currentPassword"
|
id="currentPassword"
|
||||||
data-testid="password"
|
data-testid="password"
|
||||||
disabled={isLoading}
|
disabled={isSubmitting}
|
||||||
className="login-form-input"
|
className="login-form-input"
|
||||||
/>
|
/>
|
||||||
</FormContainer.Item>
|
</FormContainer.Item>
|
||||||
@@ -255,16 +319,16 @@ function Login({
|
|||||||
</div>
|
</div>
|
||||||
</ParentContainer>
|
</ParentContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: 16 }}
|
||||||
align="start"
|
align="start"
|
||||||
direction="vertical"
|
direction="vertical"
|
||||||
size={20}
|
size={20}
|
||||||
>
|
>
|
||||||
{!precheckComplete && (
|
{!sessionsContext && (
|
||||||
<Button
|
<Button
|
||||||
disabled={precheckInProcess}
|
disabled={versionLoading || sessionsContextLoading}
|
||||||
loading={precheckInProcess}
|
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={onNextHandler}
|
onClick={onNextHandler}
|
||||||
data-testid="initiate_login"
|
data-testid="initiate_login"
|
||||||
@@ -274,12 +338,27 @@ function Login({
|
|||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{precheckComplete && !sso && (
|
|
||||||
|
{sessionsContext && isCallbackAuthN && (
|
||||||
<Button
|
<Button
|
||||||
disabled={isLoading}
|
disabled={isSubmitting}
|
||||||
loading={isLoading}
|
|
||||||
type="primary"
|
type="primary"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
|
data-testid="callback_authn_submit"
|
||||||
|
data-attr="signup"
|
||||||
|
className="periscope-btn primary next-btn"
|
||||||
|
icon={<ArrowRight size={12} />}
|
||||||
|
>
|
||||||
|
Login With Callback
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sessionsContext && isPasswordAuthN && (
|
||||||
|
<Button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
type="primary"
|
||||||
|
data-testid="password_authn_submit"
|
||||||
|
htmlType="submit"
|
||||||
data-attr="signup"
|
data-attr="signup"
|
||||||
className="periscope-btn primary next-btn"
|
className="periscope-btn primary next-btn"
|
||||||
icon={<ArrowRight size={12} />}
|
icon={<ArrowRight size={12} />}
|
||||||
@@ -287,30 +366,6 @@ function Login({
|
|||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{precheckComplete && sso && renderSAMLAction()}
|
|
||||||
{!precheckComplete && ssoerror && renderOnSsoError()}
|
|
||||||
|
|
||||||
{!canSelfRegister && (
|
|
||||||
<Typography.Paragraph className="no-acccount">
|
|
||||||
Don't have an account? Contact your admin to send you an invite
|
|
||||||
link.
|
|
||||||
</Typography.Paragraph>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{canSelfRegister && (
|
|
||||||
<Typography.Paragraph italic style={{ color: '#ACACAC' }}>
|
|
||||||
If you are admin,{' '}
|
|
||||||
<Typography.Link
|
|
||||||
onClick={(): void => {
|
|
||||||
history.push(ROUTES.SIGN_UP);
|
|
||||||
}}
|
|
||||||
style={{ fontWeight: 700 }}
|
|
||||||
>
|
|
||||||
Create an account
|
|
||||||
</Typography.Link>
|
|
||||||
</Typography.Paragraph>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { Switch, Typography } from 'antd';
|
import { Switch, Typography } from 'antd';
|
||||||
import { WsDataEvent } from 'api/common/getQueryStats';
|
|
||||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
|
||||||
import LogsDownloadOptionsMenu from 'components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu';
|
import LogsDownloadOptionsMenu from 'components/LogsDownloadOptionsMenu/LogsDownloadOptionsMenu';
|
||||||
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
import LogsFormatOptionsMenu from 'components/LogsFormatOptionsMenu/LogsFormatOptionsMenu';
|
||||||
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||||
@@ -14,7 +12,6 @@ import QueryStatus from './QueryStatus';
|
|||||||
|
|
||||||
function LogsActionsContainer({
|
function LogsActionsContainer({
|
||||||
listQuery,
|
listQuery,
|
||||||
queryStats,
|
|
||||||
selectedPanelType,
|
selectedPanelType,
|
||||||
showFrequencyChart,
|
showFrequencyChart,
|
||||||
handleToggleFrequencyChart,
|
handleToggleFrequencyChart,
|
||||||
@@ -37,7 +34,6 @@ function LogsActionsContainer({
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
isSuccess: boolean;
|
isSuccess: boolean;
|
||||||
queryStats: WsDataEvent | undefined;
|
|
||||||
minTime: number;
|
minTime: number;
|
||||||
maxTime: number;
|
maxTime: number;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
@@ -126,22 +122,6 @@ function LogsActionsContainer({
|
|||||||
error={isError}
|
error={isError}
|
||||||
success={isSuccess}
|
success={isSuccess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{queryStats?.read_rows && (
|
|
||||||
<Typography.Text className="rows">
|
|
||||||
{getYAxisFormattedValue(queryStats.read_rows?.toString(), 'short')}{' '}
|
|
||||||
rows
|
|
||||||
</Typography.Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{queryStats?.elapsed_ms && (
|
|
||||||
<>
|
|
||||||
<div className="divider" />
|
|
||||||
<Typography.Text className="time">
|
|
||||||
{getYAxisFormattedValue(queryStats?.elapsed_ms?.toString(), 'ms')}
|
|
||||||
</Typography.Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import './LogsExplorerViews.styles.scss';
|
|||||||
|
|
||||||
import getFromLocalstorage from 'api/browser/localstorage/get';
|
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||||
import setToLocalstorage from 'api/browser/localstorage/set';
|
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||||
import { getQueryStats, WsDataEvent } from 'api/common/getQueryStats';
|
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
@@ -132,7 +131,6 @@ function LogsExplorerViewsContainer({
|
|||||||
const [logs, setLogs] = useState<ILog[]>([]);
|
const [logs, setLogs] = useState<ILog[]>([]);
|
||||||
const [requestData, setRequestData] = useState<Query | null>(null);
|
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||||
const [queryId, setQueryId] = useState<string>(v4());
|
const [queryId, setQueryId] = useState<string>(v4());
|
||||||
const [queryStats, setQueryStats] = useState<WsDataEvent>();
|
|
||||||
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
const [listChartQuery, setListChartQuery] = useState<Query | null>(null);
|
||||||
|
|
||||||
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
|
||||||
@@ -409,19 +407,6 @@ function LogsExplorerViewsContainer({
|
|||||||
setQueryId(v4());
|
setQueryId(v4());
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!isEmpty(queryId) &&
|
|
||||||
(isLoading || isFetching) &&
|
|
||||||
selectedPanelType !== PANEL_TYPES.LIST
|
|
||||||
) {
|
|
||||||
setQueryStats(undefined);
|
|
||||||
setTimeout(() => {
|
|
||||||
getQueryStats({ queryId, setData: setQueryStats });
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
}, [queryId, isLoading, isFetching, selectedPanelType]);
|
|
||||||
|
|
||||||
const logEventCalledRef = useRef(false);
|
const logEventCalledRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||||
@@ -632,7 +617,6 @@ function LogsExplorerViewsContainer({
|
|||||||
{!showLiveLogs && (
|
{!showLiveLogs && (
|
||||||
<LogsActionsContainer
|
<LogsActionsContainer
|
||||||
listQuery={listQuery}
|
listQuery={listQuery}
|
||||||
queryStats={queryStats}
|
|
||||||
selectedPanelType={selectedPanelType}
|
selectedPanelType={selectedPanelType}
|
||||||
showFrequencyChart={showFrequencyChart}
|
showFrequencyChart={showFrequencyChart}
|
||||||
handleToggleFrequencyChart={handleToggleFrequencyChart}
|
handleToggleFrequencyChart={handleToggleFrequencyChart}
|
||||||
|
|||||||
@@ -52,10 +52,6 @@ jest.mock(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.mock('api/common/getQueryStats', () => ({
|
|
||||||
getQueryStats: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('constants/panelTypes', () => ({
|
jest.mock('constants/panelTypes', () => ({
|
||||||
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
|
AVAILABLE_EXPORT_PANEL_TYPES: ['graph', 'table'],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
import { Button, Card, Space, Typography } from 'antd';
|
|
||||||
import changeMyPassword from 'api/v1/factor_password/changeMyPassword';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { Save } from 'lucide-react';
|
|
||||||
import { isPasswordNotValidMessage, isPasswordValid } from 'pages/SignUp/utils';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import APIError from 'types/api/error';
|
|
||||||
|
|
||||||
import { Password } from '../styles';
|
|
||||||
|
|
||||||
function PasswordContainer(): JSX.Element {
|
|
||||||
const [currentPassword, setCurrentPassword] = useState<string>('');
|
|
||||||
const [updatePassword, setUpdatePassword] = useState<string>('');
|
|
||||||
const { t } = useTranslation(['routes', 'settings', 'common']);
|
|
||||||
const { user } = useAppContext();
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const [isPasswordPolicyError, setIsPasswordPolicyError] = useState<boolean>(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultPlaceHolder = '*************';
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentPassword && !isPasswordValid(currentPassword)) {
|
|
||||||
setIsPasswordPolicyError(true);
|
|
||||||
} else {
|
|
||||||
setIsPasswordPolicyError(false);
|
|
||||||
}
|
|
||||||
}, [currentPassword]);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChangePasswordClickHandler = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
if (!isPasswordValid(currentPassword)) {
|
|
||||||
setIsPasswordPolicyError(true);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await changeMyPassword({
|
|
||||||
newPassword: updatePassword,
|
|
||||||
oldPassword: currentPassword,
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
notifications.success({
|
|
||||||
message: t('success', {
|
|
||||||
ns: 'common',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
|
||||||
} catch (error) {
|
|
||||||
setIsLoading(false);
|
|
||||||
notifications.error({
|
|
||||||
message: (error as APIError).error.error.code,
|
|
||||||
description: (error as APIError).error.error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isDisabled =
|
|
||||||
isLoading ||
|
|
||||||
currentPassword.length === 0 ||
|
|
||||||
updatePassword.length === 0 ||
|
|
||||||
isPasswordPolicyError ||
|
|
||||||
currentPassword === updatePassword;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="reset-password-card">
|
|
||||||
<Space direction="vertical" size="small">
|
|
||||||
<Typography.Title
|
|
||||||
level={4}
|
|
||||||
style={{ marginTop: 0 }}
|
|
||||||
data-testid="change-password-header"
|
|
||||||
>
|
|
||||||
{t('change_password', {
|
|
||||||
ns: 'settings',
|
|
||||||
})}
|
|
||||||
</Typography.Title>
|
|
||||||
<Space direction="vertical">
|
|
||||||
<Typography data-testid="current-password-label">
|
|
||||||
{t('current_password', {
|
|
||||||
ns: 'settings',
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
<Password
|
|
||||||
data-testid="current-password-textbox"
|
|
||||||
disabled={isLoading}
|
|
||||||
placeholder={defaultPlaceHolder}
|
|
||||||
onChange={(event): void => {
|
|
||||||
setCurrentPassword(event.target.value);
|
|
||||||
}}
|
|
||||||
value={currentPassword}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<Space direction="vertical">
|
|
||||||
<Typography data-testid="new-password-label">
|
|
||||||
{t('new_password', {
|
|
||||||
ns: 'settings',
|
|
||||||
})}
|
|
||||||
</Typography>
|
|
||||||
<Password
|
|
||||||
data-testid="new-password-textbox"
|
|
||||||
disabled={isLoading}
|
|
||||||
placeholder={defaultPlaceHolder}
|
|
||||||
onChange={(event): void => {
|
|
||||||
const updatedValue = event.target.value;
|
|
||||||
setUpdatePassword(updatedValue);
|
|
||||||
}}
|
|
||||||
value={updatePassword}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
{isPasswordPolicyError && (
|
|
||||||
<Typography.Paragraph
|
|
||||||
data-testid="validation-message"
|
|
||||||
style={{
|
|
||||||
color: '#D89614',
|
|
||||||
marginTop: '0.50rem',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isPasswordNotValidMessage}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
<Button
|
|
||||||
disabled={isDisabled}
|
|
||||||
loading={isLoading}
|
|
||||||
onClick={onChangePasswordClickHandler}
|
|
||||||
type="primary"
|
|
||||||
data-testid="update-password-button"
|
|
||||||
>
|
|
||||||
<Save
|
|
||||||
size={12}
|
|
||||||
style={{ marginRight: '8px' }}
|
|
||||||
data-testid="update-password-icon"
|
|
||||||
/>{' '}
|
|
||||||
{t('change_password', {
|
|
||||||
ns: 'settings',
|
|
||||||
})}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PasswordContainer;
|
|
||||||
@@ -7,9 +7,8 @@ import changeMyPassword from 'api/v1/factor_password/changeMyPassword';
|
|||||||
import editUser from 'api/v1/user/id/update';
|
import editUser from 'api/v1/user/id/update';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { Check, FileTerminal, MailIcon, UserIcon } from 'lucide-react';
|
import { Check, FileTerminal, MailIcon, UserIcon } from 'lucide-react';
|
||||||
import { isPasswordValid } from 'pages/SignUp/utils';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
@@ -22,9 +21,6 @@ function UserInfo(): JSX.Element {
|
|||||||
const [currentPassword, setCurrentPassword] = useState<string>('');
|
const [currentPassword, setCurrentPassword] = useState<string>('');
|
||||||
const [updatePassword, setUpdatePassword] = useState<string>('');
|
const [updatePassword, setUpdatePassword] = useState<string>('');
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const [isPasswordPolicyError, setIsPasswordPolicyError] = useState<boolean>(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [changedName, setChangedName] = useState<string>(
|
const [changedName, setChangedName] = useState<string>(
|
||||||
user?.displayName || '',
|
user?.displayName || '',
|
||||||
@@ -40,14 +36,6 @@ function UserInfo(): JSX.Element {
|
|||||||
|
|
||||||
const defaultPlaceHolder = '*************';
|
const defaultPlaceHolder = '*************';
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentPassword && !isPasswordValid(currentPassword)) {
|
|
||||||
setIsPasswordPolicyError(true);
|
|
||||||
} else {
|
|
||||||
setIsPasswordPolicyError(false);
|
|
||||||
}
|
|
||||||
}, [currentPassword]);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
@@ -64,11 +52,6 @@ function UserInfo(): JSX.Element {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (!isPasswordValid(currentPassword)) {
|
|
||||||
setIsPasswordPolicyError(true);
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await changeMyPassword({
|
await changeMyPassword({
|
||||||
newPassword: updatePassword,
|
newPassword: updatePassword,
|
||||||
oldPassword: currentPassword,
|
oldPassword: currentPassword,
|
||||||
@@ -94,7 +77,6 @@ function UserInfo(): JSX.Element {
|
|||||||
isLoading ||
|
isLoading ||
|
||||||
currentPassword.length === 0 ||
|
currentPassword.length === 0 ||
|
||||||
updatePassword.length === 0 ||
|
updatePassword.length === 0 ||
|
||||||
isPasswordPolicyError ||
|
|
||||||
currentPassword === updatePassword;
|
currentPassword === updatePassword;
|
||||||
|
|
||||||
const onSaveHandler = async (): Promise<void> => {
|
const onSaveHandler = async (): Promise<void> => {
|
||||||
|
|||||||
@@ -408,6 +408,7 @@ export default function Onboarding(): JSX.Element {
|
|||||||
form={form}
|
form={form}
|
||||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
|
onClose={(): void => {}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
.auth-domain {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.auth-domain-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-domain-list {
|
||||||
|
.auth-domain-list-column-action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import './CreateEdit.styles.scss';
|
||||||
|
|
||||||
|
import { GoogleSquareFilled, KeyOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
|
||||||
|
interface AuthNProvider {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthNProviders(samlEnabled: boolean): AuthNProvider[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'google_auth',
|
||||||
|
title: 'Google Apps Authentication',
|
||||||
|
description: 'Let members sign-in with a Google workspace account',
|
||||||
|
icon: <GoogleSquareFilled style={{ fontSize: '37px' }} />,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'saml',
|
||||||
|
title: 'SAML Authentication',
|
||||||
|
description:
|
||||||
|
'Azure, Active Directory, Okta or your custom SAML 2.0 solution',
|
||||||
|
icon: <KeyOutlined style={{ fontSize: '37px' }} />,
|
||||||
|
enabled: samlEnabled,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: 'oidc',
|
||||||
|
title: 'OIDC Authentication',
|
||||||
|
description:
|
||||||
|
'Authenticate using OpenID Connect providers like Azure, Active Directory, Okta, or other OIDC compliant solutions',
|
||||||
|
icon: <KeyOutlined style={{ fontSize: '37px' }} />,
|
||||||
|
enabled: samlEnabled,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthnProviderSelector({
|
||||||
|
setAuthnProvider,
|
||||||
|
samlEnabled,
|
||||||
|
}: {
|
||||||
|
setAuthnProvider: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
samlEnabled: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
const authnProviders = getAuthNProviders(samlEnabled);
|
||||||
|
return (
|
||||||
|
<div className="authn-provider-selector">
|
||||||
|
<section className="header">
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
Configure Authentication Method
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph italic>
|
||||||
|
SigNoz supports the following single sign-on services (SSO). Get started
|
||||||
|
with setting your project’s SSO below
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</section>
|
||||||
|
<section className="selector">
|
||||||
|
{authnProviders.map((provider) => {
|
||||||
|
if (provider.enabled) {
|
||||||
|
return (
|
||||||
|
<section key={provider.key} className="provider">
|
||||||
|
<span className="icon">{provider.icon}</span>
|
||||||
|
<div className="title-description">
|
||||||
|
<Typography.Text className="title">{provider.title}</Typography.Text>
|
||||||
|
<Typography.Paragraph className="description">
|
||||||
|
{provider.description}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={(): void => setAuthnProvider(provider.key)}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div key={provider.key} />;
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthnProviderSelector;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
.authn-provider-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.provider {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.title-description {
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
.auth-domain-configure {
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import './CreateEdit.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Form, Modal } from 'antd';
|
||||||
|
import put from 'api/v1/domains/id/put';
|
||||||
|
import post from 'api/v1/domains/post';
|
||||||
|
import { FeatureKeys } from 'constants/features';
|
||||||
|
import { defaultTo } from 'lodash-es';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||||
|
import { PostableAuthDomain } from 'types/api/v1/domains/post';
|
||||||
|
|
||||||
|
import AuthnProviderSelector from './AuthnProviderSelector';
|
||||||
|
import ConfigureGoogleAuthAuthnProvider from './Providers/AuthnGoogleAuth';
|
||||||
|
import ConfigureOIDCAuthnProvider from './Providers/AuthnOIDC';
|
||||||
|
import ConfigureSAMLAuthnProvider from './Providers/AuthnSAML';
|
||||||
|
|
||||||
|
interface CreateOrEditProps {
|
||||||
|
isCreate: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
record?: GettableAuthDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureAuthnProvider(
|
||||||
|
authnProvider: string,
|
||||||
|
isCreate: boolean,
|
||||||
|
): JSX.Element {
|
||||||
|
switch (authnProvider) {
|
||||||
|
case 'saml':
|
||||||
|
return <ConfigureSAMLAuthnProvider isCreate={isCreate} />;
|
||||||
|
case 'google_auth':
|
||||||
|
return <ConfigureGoogleAuthAuthnProvider isCreate={isCreate} />;
|
||||||
|
case 'oidc':
|
||||||
|
return <ConfigureOIDCAuthnProvider isCreate={isCreate} />;
|
||||||
|
default:
|
||||||
|
return <ConfigureGoogleAuthAuthnProvider isCreate={isCreate} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateOrEdit(props: CreateOrEditProps): JSX.Element {
|
||||||
|
const { isCreate, record, onClose } = props;
|
||||||
|
const [form] = Form.useForm<PostableAuthDomain>();
|
||||||
|
const [authnProvider, setAuthnProvider] = useState<string>(
|
||||||
|
record?.ssoType || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
const { featureFlags } = useAppContext();
|
||||||
|
const samlEnabled =
|
||||||
|
featureFlags?.find((flag) => flag.name === FeatureKeys.SSO)?.active || false;
|
||||||
|
|
||||||
|
const onSubmitHandler = async (): Promise<void> => {
|
||||||
|
const name = form.getFieldValue('name');
|
||||||
|
const googleAuthConfig = form.getFieldValue('googleAuthConfig');
|
||||||
|
const samlConfig = form.getFieldValue('samlConfig');
|
||||||
|
const oidcConfig = form.getFieldValue('oidcConfig');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isCreate) {
|
||||||
|
await post({
|
||||||
|
name,
|
||||||
|
config: {
|
||||||
|
ssoEnabled: true,
|
||||||
|
ssoType: authnProvider,
|
||||||
|
googleAuthConfig,
|
||||||
|
samlConfig,
|
||||||
|
oidcConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await put({
|
||||||
|
id: record?.id || '',
|
||||||
|
config: {
|
||||||
|
ssoEnabled: form.getFieldValue('ssoEnabled'),
|
||||||
|
ssoType: authnProvider,
|
||||||
|
googleAuthConfig,
|
||||||
|
samlConfig,
|
||||||
|
oidcConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBackHandler = (): void => {
|
||||||
|
setAuthnProvider('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open footer={null} onCancel={onClose}>
|
||||||
|
<Form
|
||||||
|
name="auth-domain"
|
||||||
|
initialValues={defaultTo(record, {
|
||||||
|
name: '',
|
||||||
|
ssoEnabled: false,
|
||||||
|
ssoType: '',
|
||||||
|
})}
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
>
|
||||||
|
{isCreate && authnProvider === '' && (
|
||||||
|
<AuthnProviderSelector
|
||||||
|
setAuthnProvider={setAuthnProvider}
|
||||||
|
samlEnabled={samlEnabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{authnProvider !== '' && (
|
||||||
|
<div className="auth-domain-configure">
|
||||||
|
{configureAuthnProvider(authnProvider, isCreate)}
|
||||||
|
<section className="action-buttons">
|
||||||
|
{isCreate && <Button onClick={onBackHandler}>Back</Button>}
|
||||||
|
{!isCreate && <Button onClick={onClose}>Cancel</Button>}
|
||||||
|
<Button onClick={onSubmitHandler} type="primary">
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateOrEdit.defaultProps = {
|
||||||
|
record: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateOrEdit;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import './Providers.styles.scss';
|
||||||
|
|
||||||
|
import { Callout } from '@signozhq/callout';
|
||||||
|
import { Form, Input, Typography } from 'antd';
|
||||||
|
|
||||||
|
function ConfigureGoogleAuthAuthnProvider({
|
||||||
|
isCreate,
|
||||||
|
}: {
|
||||||
|
isCreate: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="google-auth">
|
||||||
|
<section className="header">
|
||||||
|
<Typography.Text className="title">
|
||||||
|
Edit Google Authentication
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Paragraph className="description">
|
||||||
|
Enter OAuth 2.0 credentials obtained from the Google API Console below.
|
||||||
|
Read the{' '}
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs/userguide/sso-authentication"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
docs
|
||||||
|
</a>{' '}
|
||||||
|
for more information.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Form.Item label="Domain" name="name" className="field">
|
||||||
|
<Input disabled={!isCreate} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Client ID"
|
||||||
|
name={['googleAuthConfig', 'clientId']}
|
||||||
|
className="field"
|
||||||
|
tooltip={{
|
||||||
|
title: `ClientID is the application's ID. For example, 292085223830.apps.googleusercontent.com.`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Client Secret"
|
||||||
|
name={['googleAuthConfig', 'clientSecret']}
|
||||||
|
className="field"
|
||||||
|
tooltip={{
|
||||||
|
title: `It is the application's secret.`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Callout
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
showIcon
|
||||||
|
description="Google OAuth2 won’t be enabled unless you enter all the attributes above"
|
||||||
|
className="callout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfigureGoogleAuthAuthnProvider;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import './Providers.styles.scss';
|
||||||
|
|
||||||
|
import { Callout } from '@signozhq/callout';
|
||||||
|
import { Checkbox, Form, Input, Typography } from 'antd';
|
||||||
|
|
||||||
|
function ConfigureOIDCAuthnProvider({
|
||||||
|
isCreate,
|
||||||
|
}: {
|
||||||
|
isCreate: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="saml">
|
||||||
|
<section className="header">
|
||||||
|
<Typography.Text className="title">
|
||||||
|
Edit OIDC Authentication
|
||||||
|
</Typography.Text>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Form.Item label="Domain" name="name">
|
||||||
|
<Input disabled={!isCreate} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Issuer URL"
|
||||||
|
name={['oidcConfig', 'issuer']}
|
||||||
|
tooltip={{
|
||||||
|
title: `It is the URL identifier for the service. For example: "https://accounts.google.com" or "https://login.salesforce.com".`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Issuer Alias"
|
||||||
|
name={['oidcConfig', 'issuerAlias']}
|
||||||
|
tooltip={{
|
||||||
|
title: `Some offspec providers like Azure, Oracle IDCS have oidc discovery url different from issuer url which causes issuerValidation to fail.
|
||||||
|
This provides a way to override the Issuer url from the .well-known/openid-configuration issuer`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Client ID"
|
||||||
|
name={['oidcConfig', 'clientId']}
|
||||||
|
tooltip={{ title: `It is the application's ID.` }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Client Secret"
|
||||||
|
name={['oidcConfig', 'clientSecret']}
|
||||||
|
tooltip={{ title: `It is the application's secret.` }}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Email Claim Mapping"
|
||||||
|
name={['oidcConfig', 'claimMapping', 'email']}
|
||||||
|
tooltip={{
|
||||||
|
title: `Mapping of email claims to the corresponding email field in the token.`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Skip Email Verification"
|
||||||
|
name={['oidcConfig', 'insecureSkipEmailVerified']}
|
||||||
|
valuePropName="checked"
|
||||||
|
className="field"
|
||||||
|
tooltip={{
|
||||||
|
title: `Whether to skip email verification. Defaults to "false"`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Get User Info"
|
||||||
|
name={['oidcConfig', 'getUserInfo']}
|
||||||
|
valuePropName="checked"
|
||||||
|
className="field"
|
||||||
|
tooltip={{
|
||||||
|
title: `Uses the userinfo endpoint to get additional claims for the token. This is especially useful where upstreams return "thin" id tokens`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Callout
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
showIcon
|
||||||
|
description="OIDC won’t be enabled unless you enter all the attributes above"
|
||||||
|
className="callout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfigureOIDCAuthnProvider;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import './Providers.styles.scss';
|
||||||
|
|
||||||
|
import { Callout } from '@signozhq/callout';
|
||||||
|
import { Checkbox, Form, Input, Typography } from 'antd';
|
||||||
|
|
||||||
|
function ConfigureSAMLAuthnProvider({
|
||||||
|
isCreate,
|
||||||
|
}: {
|
||||||
|
isCreate: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="saml">
|
||||||
|
<section className="header">
|
||||||
|
<Typography.Text className="title">
|
||||||
|
Edit SAML Authentication
|
||||||
|
</Typography.Text>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Form.Item label="Domain" name="name">
|
||||||
|
<Input disabled={!isCreate} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="SAML ACS URL"
|
||||||
|
name={['samlConfig', 'samlIdp']}
|
||||||
|
tooltip={{
|
||||||
|
title: `The entityID of the SAML identity provider. It can typically be found in the EntityID attribute of the EntityDescriptor element in the SAML metadata of the identity provider. Example: <md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="{samlEntity}">`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="SAML Entity ID"
|
||||||
|
name={['samlConfig', 'samlEntity']}
|
||||||
|
tooltip={{
|
||||||
|
title: `The SSO endpoint of the SAML identity provider. It can typically be found in the SingleSignOnService element in the SAML metadata of the identity provider. Example: <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="{samlIdp}"/>`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="SAML X.509 Certificate"
|
||||||
|
name={['samlConfig', 'samlCert']}
|
||||||
|
tooltip={{
|
||||||
|
title: `The certificate of the SAML identity provider. It can typically be found in the X509Certificate element in the SAML metadata of the identity provider. Example: <ds:X509Certificate><ds:X509Certificate>{samlCert}</ds:X509Certificate></ds:X509Certificate>`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={4} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Skip Signing AuthN Requests"
|
||||||
|
name={['samlConfig', 'insecureSkipAuthNRequestsSigned']}
|
||||||
|
valuePropName="checked"
|
||||||
|
className="field"
|
||||||
|
tooltip={{
|
||||||
|
title: `Whether to skip signing the SAML requests. It can typically be found in the WantAuthnRequestsSigned attribute of the IDPSSODescriptor element in the SAML metadata of the identity provider. Example: <md:IDPSSODescriptor WantAuthnRequestsSigned="false" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
|
||||||
|
For providers like jumpcloud, this should be set to true.Note: This is the reverse of WantAuthnRequestsSigned. If WantAuthnRequestsSigned is false, then InsecureSkipAuthNRequestsSigned should be true.`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Callout
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
showIcon
|
||||||
|
description="SAML won’t be enabled unless you enter all the attributes above"
|
||||||
|
className="callout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfigureSAMLAuthnProvider;
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
.google-auth {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.saml {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
.ant-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-flow: nowrap;
|
||||||
|
|
||||||
|
.ant-col {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Switch } from 'antd';
|
||||||
|
import put from 'api/v1/domains/id/put';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
import { GettableAuthDomain } from 'types/api/v1/domains/list';
|
||||||
|
|
||||||
|
function Toggle({ isDefaultChecked, record }: ToggleProps): JSX.Element {
|
||||||
|
const [isChecked, setIsChecked] = useState<boolean>(isDefaultChecked);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
|
const onChangeHandler = async (checked: boolean): Promise<void> => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await put({
|
||||||
|
id: record.id,
|
||||||
|
config: {
|
||||||
|
ssoEnabled: checked,
|
||||||
|
ssoType: record.ssoType,
|
||||||
|
googleAuthConfig: record.googleAuthConfig,
|
||||||
|
oidcConfig: record.oidcConfig,
|
||||||
|
samlConfig: record.samlConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIsChecked(checked);
|
||||||
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch loading={isLoading} checked={isChecked} onChange={onChangeHandler} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToggleProps {
|
||||||
|
isDefaultChecked: boolean;
|
||||||
|
record: GettableAuthDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Toggle;
|
||||||
148
frontend/src/container/OrganizationSettings/AuthDomain/index.tsx
Normal file
148
frontend/src/container/OrganizationSettings/AuthDomain/index.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import './AuthDomain.styles.scss';
|
||||||
|
|
||||||
|
import { PlusOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Table, Typography } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
|
import deleteDomain from 'api/v1/domains/id/delete';
|
||||||
|
import listAllDomain from 'api/v1/domains/list';
|
||||||
|
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||||
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
import { GettableAuthDomain, SSOType } from 'types/api/v1/domains/list';
|
||||||
|
|
||||||
|
import CreateEdit from './CreateEdit/CreateEdit';
|
||||||
|
import Toggle from './Toggle';
|
||||||
|
|
||||||
|
const columns: ColumnsType<GettableAuthDomain> = [
|
||||||
|
{
|
||||||
|
title: 'Domain',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 100,
|
||||||
|
render: (val): JSX.Element => <Typography.Text>{val}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Enforce SSO',
|
||||||
|
dataIndex: 'ssoEnabled',
|
||||||
|
key: 'ssoEnabled',
|
||||||
|
width: 80,
|
||||||
|
render: (value: boolean, record: GettableAuthDomain): JSX.Element => (
|
||||||
|
<Toggle isDefaultChecked={value} record={record} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
dataIndex: 'action',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record: GettableAuthDomain): JSX.Element => (
|
||||||
|
<section className="auth-domain-list-column-action">
|
||||||
|
<Typography.Link data-column-action="configure">
|
||||||
|
Configure {SSOType.get(record.ssoType)}
|
||||||
|
</Typography.Link>
|
||||||
|
<Typography.Link type="danger" data-column-action="delete">
|
||||||
|
Delete
|
||||||
|
</Typography.Link>
|
||||||
|
</section>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function deleteDomainById(
|
||||||
|
id: string,
|
||||||
|
showErrorModal: (error: APIError) => void,
|
||||||
|
refetchAuthDomainListResponse: () => void,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await deleteDomain(id);
|
||||||
|
refetchAuthDomainListResponse();
|
||||||
|
} catch (error) {
|
||||||
|
showErrorModal(error as APIError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthDomain(): JSX.Element {
|
||||||
|
const [record, setRecord] = useState<GettableAuthDomain>();
|
||||||
|
const [addDomain, setAddDomain] = useState<boolean>(false);
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
const {
|
||||||
|
data: authDomainListResponse,
|
||||||
|
isLoading: isLoadingAuthDomainListResponse,
|
||||||
|
isFetching: isFetchingAuthDomainListResponse,
|
||||||
|
error: errorFetchingAuthDomainListResponse,
|
||||||
|
refetch: refetchAuthDomainListResponse,
|
||||||
|
} = useQuery({
|
||||||
|
queryFn: listAllDomain,
|
||||||
|
queryKey: ['/api/v1/domains', 'list'],
|
||||||
|
enabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="auth-domain">
|
||||||
|
<section className="auth-domain-header">
|
||||||
|
<Typography.Title level={3}>Authenticated Domains</Typography.Title>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={(): void => {
|
||||||
|
setAddDomain(true);
|
||||||
|
}}
|
||||||
|
className="button"
|
||||||
|
>
|
||||||
|
Add Domain
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
{(errorFetchingAuthDomainListResponse as APIError) && (
|
||||||
|
<ErrorContent error={errorFetchingAuthDomainListResponse as APIError} />
|
||||||
|
)}
|
||||||
|
{!(errorFetchingAuthDomainListResponse as APIError) && (
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={authDomainListResponse?.data}
|
||||||
|
onRow={(record): any => ({
|
||||||
|
onClick: (
|
||||||
|
event: React.SyntheticEvent<HTMLLinkElement, MouseEvent>,
|
||||||
|
): void => {
|
||||||
|
const target = event.target as HTMLLinkElement;
|
||||||
|
const { columnAction } = target.dataset;
|
||||||
|
switch (columnAction) {
|
||||||
|
case 'configure':
|
||||||
|
setRecord(record);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
deleteDomainById(
|
||||||
|
record.id,
|
||||||
|
showErrorModal,
|
||||||
|
refetchAuthDomainListResponse,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.error('Unknown action:', columnAction);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
loading={
|
||||||
|
isLoadingAuthDomainListResponse || isFetchingAuthDomainListResponse
|
||||||
|
}
|
||||||
|
className="auth-domain-list"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(addDomain || record) && (
|
||||||
|
<CreateEdit
|
||||||
|
isCreate={!record}
|
||||||
|
record={record}
|
||||||
|
onClose={(): void => {
|
||||||
|
setAddDomain(false);
|
||||||
|
setRecord(undefined);
|
||||||
|
refetchAuthDomainListResponse();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthDomain;
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
/* eslint-disable prefer-regex-literals */
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { Button, Form, Input, Modal, Typography } from 'antd';
|
|
||||||
import { useForm } from 'antd/es/form/Form';
|
|
||||||
import createDomainApi from 'api/v1/domains/create';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import APIError from 'types/api/error';
|
|
||||||
|
|
||||||
function AddDomain({ refetch }: Props): JSX.Element {
|
|
||||||
const { t } = useTranslation(['common', 'organizationsettings']);
|
|
||||||
const [isAddDomains, setIsDomain] = useState(false);
|
|
||||||
const [form] = useForm<FormProps>();
|
|
||||||
const { org } = useAppContext();
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const onCreateHandler = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await createDomainApi({
|
|
||||||
name: form.getFieldValue('domain'),
|
|
||||||
orgId: (org || [])[0].id,
|
|
||||||
});
|
|
||||||
|
|
||||||
notifications.success({
|
|
||||||
message: 'Your domain has been added successfully.',
|
|
||||||
duration: 15,
|
|
||||||
});
|
|
||||||
setIsDomain(false);
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error({
|
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="auth-domains-title-container">
|
|
||||||
<Typography.Title level={3}>
|
|
||||||
{t('authenticated_domains', {
|
|
||||||
ns: 'organizationsettings',
|
|
||||||
})}
|
|
||||||
</Typography.Title>
|
|
||||||
<Button
|
|
||||||
onClick={(): void => setIsDomain(true)}
|
|
||||||
type="primary"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
>
|
|
||||||
{t('add_domain', { ns: 'organizationsettings' })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Modal
|
|
||||||
centered
|
|
||||||
title="Add Domain"
|
|
||||||
className="add-domain-modal"
|
|
||||||
footer={null}
|
|
||||||
open={isAddDomains}
|
|
||||||
destroyOnClose
|
|
||||||
onCancel={(): void => setIsDomain(false)}
|
|
||||||
>
|
|
||||||
<Form form={form} onFinish={onCreateHandler} requiredMark>
|
|
||||||
<Form.Item
|
|
||||||
required
|
|
||||||
name={['domain']}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
message: 'Please enter a valid domain',
|
|
||||||
required: true,
|
|
||||||
pattern: new RegExp(
|
|
||||||
'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input placeholder="signoz.io" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Add Domain
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FormProps {
|
|
||||||
domain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
refetch: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddDomain;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { Button, Space, Typography } from 'antd';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { IconContainer, TitleContainer, TitleText } from './styles';
|
|
||||||
|
|
||||||
function Row({
|
|
||||||
onClickHandler,
|
|
||||||
Icon,
|
|
||||||
buttonText,
|
|
||||||
subTitle,
|
|
||||||
title,
|
|
||||||
isDisabled,
|
|
||||||
}: RowProps): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
|
||||||
<IconContainer>{Icon}</IconContainer>
|
|
||||||
|
|
||||||
<TitleContainer>
|
|
||||||
<TitleText>{title}</TitleText>
|
|
||||||
<Typography.Text>{subTitle}</Typography.Text>
|
|
||||||
</TitleContainer>
|
|
||||||
|
|
||||||
<Button disabled={isDisabled} onClick={onClickHandler} type="primary">
|
|
||||||
{buttonText}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RowProps {
|
|
||||||
onClickHandler: VoidFunction;
|
|
||||||
Icon: ReactNode;
|
|
||||||
title: string;
|
|
||||||
subTitle: ReactNode;
|
|
||||||
buttonText: string;
|
|
||||||
isDisabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Row;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Typography } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const TitleContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IconContainer = styled.div`
|
|
||||||
min-width: 70px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const TitleText = styled(Typography)`
|
|
||||||
font-weight: bold;
|
|
||||||
`;
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { GoogleSquareFilled, KeyOutlined } from '@ant-design/icons';
|
|
||||||
import { Typography } from 'antd';
|
|
||||||
import { FeatureKeys } from 'constants/features';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
import { AuthDomain, GOOGLE_AUTH, SAML } from 'types/api/SAML/listDomain';
|
|
||||||
|
|
||||||
import Row, { RowProps } from './Row';
|
|
||||||
import { RowContainer, RowSpace } from './styles';
|
|
||||||
|
|
||||||
function Create({
|
|
||||||
ssoMethod,
|
|
||||||
assignSsoMethod,
|
|
||||||
setIsSettingsOpen,
|
|
||||||
setIsEditModalOpen,
|
|
||||||
}: CreateProps): JSX.Element {
|
|
||||||
const { featureFlags } = useAppContext();
|
|
||||||
const SSOFlag =
|
|
||||||
featureFlags?.find((flag) => flag.name === FeatureKeys.SSO)?.active || false;
|
|
||||||
|
|
||||||
const onGoogleAuthClickHandler = useCallback(() => {
|
|
||||||
assignSsoMethod(GOOGLE_AUTH);
|
|
||||||
setIsSettingsOpen(false);
|
|
||||||
setIsEditModalOpen(true);
|
|
||||||
}, [assignSsoMethod, setIsSettingsOpen, setIsEditModalOpen]);
|
|
||||||
|
|
||||||
const onEditSAMLHandler = useCallback(() => {
|
|
||||||
assignSsoMethod(SAML);
|
|
||||||
setIsSettingsOpen(false);
|
|
||||||
setIsEditModalOpen(true);
|
|
||||||
}, [assignSsoMethod, setIsSettingsOpen, setIsEditModalOpen]);
|
|
||||||
|
|
||||||
const ConfigureButtonText = useMemo(() => {
|
|
||||||
switch (ssoMethod) {
|
|
||||||
case GOOGLE_AUTH:
|
|
||||||
return 'Edit Google Auth';
|
|
||||||
case SAML:
|
|
||||||
return 'Edit SAML';
|
|
||||||
default:
|
|
||||||
return 'Get Started';
|
|
||||||
}
|
|
||||||
}, [ssoMethod]);
|
|
||||||
|
|
||||||
const data: RowProps[] = SSOFlag
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
buttonText: ConfigureButtonText,
|
|
||||||
Icon: <GoogleSquareFilled style={{ fontSize: '37px' }} />,
|
|
||||||
title: 'Google Apps Authentication',
|
|
||||||
subTitle: 'Let members sign-in with a Google workspace account',
|
|
||||||
onClickHandler: onGoogleAuthClickHandler,
|
|
||||||
isDisabled: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
buttonText: ConfigureButtonText,
|
|
||||||
Icon: <KeyOutlined style={{ fontSize: '37px' }} />,
|
|
||||||
onClickHandler: onEditSAMLHandler,
|
|
||||||
subTitle: (
|
|
||||||
<>
|
|
||||||
Azure, Active Directory, Okta or your custom SAML 2.0 solution{' '}
|
|
||||||
<a
|
|
||||||
href="https://github.com/SigNoz/signoz/issues/8647"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
(Unsupported SAMLs)
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
title: 'SAML Authentication',
|
|
||||||
isDisabled: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
buttonText: ConfigureButtonText,
|
|
||||||
Icon: <GoogleSquareFilled style={{ fontSize: '37px' }} />,
|
|
||||||
title: 'Google Apps Authentication',
|
|
||||||
subTitle: 'Let members sign-in with a Google account',
|
|
||||||
onClickHandler: onGoogleAuthClickHandler,
|
|
||||||
isDisabled: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Typography.Text italic>
|
|
||||||
SigNoz supports the following single sign-on services (SSO). Get started
|
|
||||||
with setting your project’s SSO below
|
|
||||||
</Typography.Text>
|
|
||||||
|
|
||||||
<RowContainer>
|
|
||||||
<RowSpace direction="vertical">
|
|
||||||
{data.map((rowData) => (
|
|
||||||
<Row
|
|
||||||
Icon={rowData.Icon}
|
|
||||||
buttonText={rowData.buttonText}
|
|
||||||
onClickHandler={rowData.onClickHandler}
|
|
||||||
subTitle={rowData.subTitle}
|
|
||||||
title={rowData.title}
|
|
||||||
key={rowData.title}
|
|
||||||
isDisabled={rowData.isDisabled}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</RowSpace>
|
|
||||||
</RowContainer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateProps {
|
|
||||||
ssoMethod: AuthDomain['ssoType'];
|
|
||||||
assignSsoMethod: (value: AuthDomain['ssoType']) => void;
|
|
||||||
setIsSettingsOpen: (value: boolean) => void;
|
|
||||||
setIsEditModalOpen: (value: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Create;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Space } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const RowContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-top: 1rem;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const RowSpace = styled(Space)`
|
|
||||||
&&& {
|
|
||||||
row-gap: 1.5rem !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { InfoCircleFilled } from '@ant-design/icons';
|
|
||||||
import { Card, Form, Input, Space, Typography } from 'antd';
|
|
||||||
|
|
||||||
function EditGoogleAuth(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Typography.Paragraph>
|
|
||||||
Enter OAuth 2.0 credentials obtained from the Google API Console below. Read
|
|
||||||
the{' '}
|
|
||||||
<a
|
|
||||||
href="https://signoz.io/docs/userguide/sso-authentication"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
docs
|
|
||||||
</a>{' '}
|
|
||||||
for more information.
|
|
||||||
</Typography.Paragraph>
|
|
||||||
<Form.Item
|
|
||||||
label="Client ID"
|
|
||||||
name={['googleAuthConfig', 'clientId']}
|
|
||||||
rules={[{ required: true, message: 'Please input Google Auth Client ID!' }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="Client Secret"
|
|
||||||
name={['googleAuthConfig', 'clientSecret']}
|
|
||||||
rules={[
|
|
||||||
{ required: true, message: 'Please input Google Auth Client Secret!' },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Card style={{ marginBottom: '1rem' }}>
|
|
||||||
<Space>
|
|
||||||
<InfoCircleFilled />
|
|
||||||
<Typography>
|
|
||||||
Google OAuth2 won’t be enabled unless you enter all the attributes above
|
|
||||||
</Typography>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditGoogleAuth;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { InfoCircleFilled } from '@ant-design/icons';
|
|
||||||
import { Card, Form, Input, Space, Typography } from 'antd';
|
|
||||||
|
|
||||||
function EditSAML(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Item
|
|
||||||
label="SAML ACS URL"
|
|
||||||
name={['samlConfig', 'samlIdp']}
|
|
||||||
rules={[{ required: true, message: 'Please input your ACS URL!' }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
label="SAML Entity ID"
|
|
||||||
name={['samlConfig', 'samlEntity']}
|
|
||||||
rules={[{ required: true, message: 'Please input your Entity Id!' }]}
|
|
||||||
>
|
|
||||||
<Input />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
rules={[{ required: true, message: 'Please input your Certificate!' }]}
|
|
||||||
label="SAML X.509 Certificate"
|
|
||||||
name={['samlConfig', 'samlCert']}
|
|
||||||
>
|
|
||||||
<Input.TextArea rows={4} />
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Card style={{ marginBottom: '1rem' }}>
|
|
||||||
<Space>
|
|
||||||
<InfoCircleFilled />
|
|
||||||
<Typography>
|
|
||||||
SAML won’t be enabled unless you enter all the attributes above
|
|
||||||
</Typography>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditSAML;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import {
|
|
||||||
AuthDomain,
|
|
||||||
GOOGLE_AUTH,
|
|
||||||
GoogleAuthConfig,
|
|
||||||
isGoogleAuthConfig,
|
|
||||||
isSAMLConfig,
|
|
||||||
SAML,
|
|
||||||
SAMLConfig,
|
|
||||||
} from 'types/api/SAML/listDomain';
|
|
||||||
|
|
||||||
export function parseSamlForm(
|
|
||||||
current: AuthDomain,
|
|
||||||
formValues: AuthDomain,
|
|
||||||
): SAMLConfig | undefined {
|
|
||||||
if (current?.ssoType === SAML && isSAMLConfig(formValues?.samlConfig)) {
|
|
||||||
return {
|
|
||||||
...current.samlConfig,
|
|
||||||
...formValues?.samlConfig,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return current.samlConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseGoogleAuthForm(
|
|
||||||
current: AuthDomain,
|
|
||||||
formValues: AuthDomain,
|
|
||||||
): GoogleAuthConfig | undefined {
|
|
||||||
if (
|
|
||||||
current?.ssoType === GOOGLE_AUTH &&
|
|
||||||
isGoogleAuthConfig(formValues?.googleAuthConfig)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...current.googleAuthConfig,
|
|
||||||
...formValues?.googleAuthConfig,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return current.googleAuthConfig;
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Button, Form, Space } from 'antd';
|
|
||||||
import { useForm } from 'antd/lib/form/Form';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { AuthDomain, GOOGLE_AUTH, SAML } from 'types/api/SAML/listDomain';
|
|
||||||
|
|
||||||
import EditGoogleAuth from './EditGoogleAuth';
|
|
||||||
import EditSAML from './EditSAML';
|
|
||||||
import { parseGoogleAuthForm, parseSamlForm } from './helpers';
|
|
||||||
|
|
||||||
// renderFormInputs selectively renders form fields depending upon
|
|
||||||
// sso type
|
|
||||||
const renderFormInputs = (
|
|
||||||
record: AuthDomain | undefined,
|
|
||||||
): JSX.Element | undefined => {
|
|
||||||
switch (record?.ssoType) {
|
|
||||||
case GOOGLE_AUTH:
|
|
||||||
return <EditGoogleAuth />;
|
|
||||||
case SAML:
|
|
||||||
default:
|
|
||||||
return <EditSAML />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function EditSSO({
|
|
||||||
onRecordUpdateHandler,
|
|
||||||
record,
|
|
||||||
setEditModalOpen,
|
|
||||||
}: EditFormProps): JSX.Element {
|
|
||||||
const [form] = useForm<AuthDomain>();
|
|
||||||
|
|
||||||
const { t } = useTranslation(['common']);
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const onFinishHandler = useCallback(() => {
|
|
||||||
form
|
|
||||||
.validateFields()
|
|
||||||
.then(async (values) => {
|
|
||||||
await onRecordUpdateHandler({
|
|
||||||
...record,
|
|
||||||
ssoEnabled: true,
|
|
||||||
ssoType: record.ssoType,
|
|
||||||
samlConfig: parseSamlForm(record, values),
|
|
||||||
googleAuthConfig: parseGoogleAuthForm(record, values),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
notifications.error({
|
|
||||||
message: t('something_went_wrong', { ns: 'common' }),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [form, onRecordUpdateHandler, record, t, notifications]);
|
|
||||||
|
|
||||||
const onResetHandler = useCallback(() => {
|
|
||||||
form.resetFields();
|
|
||||||
setEditModalOpen(false);
|
|
||||||
}, [setEditModalOpen, form]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
name="basic"
|
|
||||||
initialValues={record}
|
|
||||||
onFinishFailed={(error): void => {
|
|
||||||
error.errorFields.forEach(({ errors }) => {
|
|
||||||
notifications.error({
|
|
||||||
message:
|
|
||||||
errors[0].toString() || t('something_went_wrong', { ns: 'common' }),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
form.resetFields();
|
|
||||||
}}
|
|
||||||
layout="vertical"
|
|
||||||
onFinish={onFinishHandler}
|
|
||||||
autoComplete="off"
|
|
||||||
form={form}
|
|
||||||
>
|
|
||||||
{renderFormInputs(record)}
|
|
||||||
<Space
|
|
||||||
style={{ width: '100%', justifyContent: 'flex-end' }}
|
|
||||||
align="end"
|
|
||||||
direction="horizontal"
|
|
||||||
>
|
|
||||||
<Button htmlType="button" onClick={onResetHandler}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="primary" htmlType="submit">
|
|
||||||
Save Settings
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EditFormProps {
|
|
||||||
onRecordUpdateHandler: (record: AuthDomain) => Promise<boolean>;
|
|
||||||
record: AuthDomain;
|
|
||||||
setEditModalOpen: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditSSO;
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { Switch } from 'antd';
|
|
||||||
import { useMemo, useState } from 'react';
|
|
||||||
import { AuthDomain } from 'types/api/SAML/listDomain';
|
|
||||||
|
|
||||||
import { isSSOConfigValid } from '../helpers';
|
|
||||||
|
|
||||||
function SwitchComponent({
|
|
||||||
isDefaultChecked,
|
|
||||||
onRecordUpdateHandler,
|
|
||||||
record,
|
|
||||||
}: SwitchComponentProps): JSX.Element {
|
|
||||||
const [isChecked, setIsChecked] = useState<boolean>(isDefaultChecked);
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const onChangeHandler = async (checked: boolean): Promise<void> => {
|
|
||||||
setIsLoading(true);
|
|
||||||
const response = await onRecordUpdateHandler({
|
|
||||||
...record,
|
|
||||||
ssoEnabled: checked,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response) {
|
|
||||||
setIsChecked(checked);
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isInValidVerificate = useMemo(() => !isSSOConfigValid(record), [record]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={isInValidVerificate}
|
|
||||||
checked={isChecked}
|
|
||||||
onChange={onChangeHandler}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SwitchComponentProps {
|
|
||||||
isDefaultChecked: boolean;
|
|
||||||
onRecordUpdateHandler: (record: AuthDomain) => Promise<boolean>;
|
|
||||||
record: AuthDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SwitchComponent;
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { AuthDomain, SAML } from 'types/api/SAML/listDomain';
|
|
||||||
|
|
||||||
import { isSSOConfigValid } from './helpers';
|
|
||||||
|
|
||||||
const inValidCase: AuthDomain['samlConfig'][] = [
|
|
||||||
{
|
|
||||||
samlCert: '',
|
|
||||||
samlEntity: '',
|
|
||||||
samlIdp: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
samlCert: '',
|
|
||||||
samlEntity: '',
|
|
||||||
samlIdp: 'asd',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
samlCert: 'sample certificate',
|
|
||||||
samlEntity: '',
|
|
||||||
samlIdp: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
samlCert: 'sample cert',
|
|
||||||
samlEntity: 'sample entity',
|
|
||||||
samlIdp: '',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const validCase: AuthDomain['samlConfig'][] = [
|
|
||||||
{
|
|
||||||
samlCert: 'sample cert',
|
|
||||||
samlEntity: 'sample entity',
|
|
||||||
samlIdp: 'sample idp',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
describe('Utils', () => {
|
|
||||||
inValidCase.forEach((config) => {
|
|
||||||
it('should return invalid saml config', () => {
|
|
||||||
expect(
|
|
||||||
isSSOConfigValid({
|
|
||||||
id: 'test-0',
|
|
||||||
name: 'test',
|
|
||||||
orgId: '32ed234',
|
|
||||||
ssoEnabled: true,
|
|
||||||
ssoType: SAML,
|
|
||||||
samlConfig: {
|
|
||||||
samlCert: config?.samlCert || '',
|
|
||||||
samlEntity: config?.samlEntity || '',
|
|
||||||
samlIdp: config?.samlIdp || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
validCase.forEach((config) => {
|
|
||||||
it('should return invalid saml config', () => {
|
|
||||||
expect(
|
|
||||||
isSSOConfigValid({
|
|
||||||
id: 'test-0',
|
|
||||||
name: 'test',
|
|
||||||
orgId: '32ed234',
|
|
||||||
ssoEnabled: true,
|
|
||||||
ssoType: SAML,
|
|
||||||
samlConfig: {
|
|
||||||
samlCert: config?.samlCert || '',
|
|
||||||
samlEntity: config?.samlEntity || '',
|
|
||||||
samlIdp: config?.samlIdp || '',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { AuthDomain, GOOGLE_AUTH, SAML } from 'types/api/SAML/listDomain';
|
|
||||||
|
|
||||||
export const ConfigureSsoButtonText = (
|
|
||||||
ssoType: AuthDomain['ssoType'],
|
|
||||||
): string => {
|
|
||||||
switch (ssoType) {
|
|
||||||
case SAML:
|
|
||||||
return 'Edit SAML';
|
|
||||||
case GOOGLE_AUTH:
|
|
||||||
return 'Edit Google Auth';
|
|
||||||
default:
|
|
||||||
return 'Configure SSO';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditModalTitleText = (
|
|
||||||
ssoType: AuthDomain['ssoType'] | undefined,
|
|
||||||
): string => {
|
|
||||||
switch (ssoType) {
|
|
||||||
case SAML:
|
|
||||||
return 'Edit SAML Configuration';
|
|
||||||
case GOOGLE_AUTH:
|
|
||||||
return 'Edit Google Authentication';
|
|
||||||
default:
|
|
||||||
return 'Configure SSO';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isSSOConfigValid = (domain: AuthDomain): boolean => {
|
|
||||||
switch (domain.ssoType) {
|
|
||||||
case SAML:
|
|
||||||
return (
|
|
||||||
domain.samlConfig?.samlCert?.length !== 0 &&
|
|
||||||
domain.samlConfig?.samlEntity?.length !== 0 &&
|
|
||||||
domain.samlConfig?.samlIdp?.length !== 0
|
|
||||||
);
|
|
||||||
case GOOGLE_AUTH:
|
|
||||||
return (
|
|
||||||
domain.googleAuthConfig?.clientId?.length !== 0 &&
|
|
||||||
domain.googleAuthConfig?.clientSecret?.length !== 0
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import { Button, Modal, Space, Typography } from 'antd';
|
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
|
||||||
import deleteDomain from 'api/v1/domains/delete';
|
|
||||||
import listAllDomain from 'api/v1/domains/list';
|
|
||||||
import updateDomain from 'api/v1/domains/update';
|
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
|
||||||
import TextToolTip from 'components/TextToolTip';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
import { useAppContext } from 'providers/App/App';
|
|
||||||
import { Dispatch, SetStateAction, useCallback, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useQuery } from 'react-query';
|
|
||||||
import APIError from 'types/api/error';
|
|
||||||
import { AuthDomain } from 'types/api/SAML/listDomain';
|
|
||||||
import { v4 } from 'uuid';
|
|
||||||
|
|
||||||
import AddDomain from './AddDomain';
|
|
||||||
import Create from './Create';
|
|
||||||
import EditSSO from './Edit';
|
|
||||||
import { ConfigureSsoButtonText, EditModalTitleText } from './helpers';
|
|
||||||
import { ColumnWithTooltip } from './styles';
|
|
||||||
import SwitchComponent from './Switch';
|
|
||||||
|
|
||||||
function AuthDomains(): JSX.Element {
|
|
||||||
const { t } = useTranslation(['common', 'organizationsettings']);
|
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState<boolean>(false);
|
|
||||||
const { org } = useAppContext();
|
|
||||||
const [currentDomain, setCurrentDomain] = useState<AuthDomain>();
|
|
||||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const { data, isLoading, refetch } = useQuery(['saml'], {
|
|
||||||
queryFn: () => listAllDomain(),
|
|
||||||
enabled: org !== null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
|
||||||
|
|
||||||
const assignSsoMethod = useCallback(
|
|
||||||
(typ: AuthDomain['ssoType']): void => {
|
|
||||||
setCurrentDomain({ ...currentDomain, ssoType: typ } as AuthDomain);
|
|
||||||
},
|
|
||||||
[currentDomain, setCurrentDomain],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onCloseHandler = useCallback(
|
|
||||||
(func: Dispatch<SetStateAction<boolean>>) => (): void => {
|
|
||||||
func(false);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onRecordUpdateHandler = useCallback(
|
|
||||||
async (record: AuthDomain): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
await updateDomain(record);
|
|
||||||
notifications.success({
|
|
||||||
message: t('saml_settings', {
|
|
||||||
ns: 'organizationsettings',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
refetch();
|
|
||||||
onCloseHandler(setIsEditModalOpen)();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error({
|
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[refetch, t, onCloseHandler, notifications],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onOpenHandler = useCallback(
|
|
||||||
(func: Dispatch<SetStateAction<boolean>>) => (): void => {
|
|
||||||
func(true);
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onEditHandler = useCallback(
|
|
||||||
(record: AuthDomain) => (): void => {
|
|
||||||
if (!record.ssoType) {
|
|
||||||
onOpenHandler(setIsSettingsOpen)();
|
|
||||||
} else {
|
|
||||||
onOpenHandler(setIsEditModalOpen)();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentDomain(record);
|
|
||||||
},
|
|
||||||
[onOpenHandler],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onDeleteHandler = useCallback(
|
|
||||||
(record: AuthDomain) => (): void => {
|
|
||||||
Modal.confirm({
|
|
||||||
centered: true,
|
|
||||||
title: t('delete_domain', {
|
|
||||||
ns: 'organizationsettings',
|
|
||||||
}),
|
|
||||||
content: t('delete_domain_message', {
|
|
||||||
ns: 'organizationsettings',
|
|
||||||
}),
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
await deleteDomain({
|
|
||||||
...record,
|
|
||||||
});
|
|
||||||
|
|
||||||
notifications.success({
|
|
||||||
message: t('common:success'),
|
|
||||||
});
|
|
||||||
refetch();
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error({
|
|
||||||
message: (error as APIError).getErrorCode(),
|
|
||||||
description: (error as APIError).getErrorMessage(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[refetch, t, notifications],
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns: ColumnsType<AuthDomain> = [
|
|
||||||
{
|
|
||||||
title: 'Domain',
|
|
||||||
dataIndex: 'name',
|
|
||||||
key: 'name',
|
|
||||||
width: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: (
|
|
||||||
<ColumnWithTooltip>
|
|
||||||
<Typography>Enforce SSO</Typography>
|
|
||||||
<TextToolTip
|
|
||||||
{...{
|
|
||||||
text: `When enabled, this option restricts users to SSO based authentication. For more information, click `,
|
|
||||||
url: 'https://signoz.io/docs/userguide/sso-authentication/',
|
|
||||||
}}
|
|
||||||
/>{' '}
|
|
||||||
</ColumnWithTooltip>
|
|
||||||
),
|
|
||||||
dataIndex: 'ssoEnabled',
|
|
||||||
key: 'ssoEnabled',
|
|
||||||
width: 80,
|
|
||||||
render: (value: boolean, record: AuthDomain): JSX.Element => (
|
|
||||||
<SwitchComponent
|
|
||||||
onRecordUpdateHandler={onRecordUpdateHandler}
|
|
||||||
isDefaultChecked={value}
|
|
||||||
record={record}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: 'description',
|
|
||||||
key: 'description',
|
|
||||||
width: 100,
|
|
||||||
render: (_, record: AuthDomain): JSX.Element => (
|
|
||||||
<Button type="link" onClick={onEditHandler(record)}>
|
|
||||||
{ConfigureSsoButtonText(record.ssoType)}
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Action',
|
|
||||||
dataIndex: 'action',
|
|
||||||
key: 'action',
|
|
||||||
width: 50,
|
|
||||||
render: (_, record): JSX.Element => (
|
|
||||||
<Button onClick={onDeleteHandler(record)} danger type="link">
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!isLoading && data?.data?.length === 0) {
|
|
||||||
return (
|
|
||||||
<Space direction="vertical" size="middle">
|
|
||||||
<AddDomain refetch={refetch} />
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
centered
|
|
||||||
title="Configure Authentication Method"
|
|
||||||
onCancel={onCloseHandler(setIsSettingsOpen)}
|
|
||||||
destroyOnClose
|
|
||||||
open={isSettingsOpen}
|
|
||||||
footer={null}
|
|
||||||
>
|
|
||||||
<Create
|
|
||||||
ssoMethod={currentDomain?.ssoType as AuthDomain['ssoType']}
|
|
||||||
assignSsoMethod={assignSsoMethod}
|
|
||||||
setIsEditModalOpen={setIsEditModalOpen}
|
|
||||||
setIsSettingsOpen={setIsSettingsOpen}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
<ResizeTable
|
|
||||||
columns={columns}
|
|
||||||
rowKey={(record: AuthDomain): string => record.name + v4()}
|
|
||||||
dataSource={[]}
|
|
||||||
tableLayout="fixed"
|
|
||||||
bordered
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableData = data?.data || [];
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
centered
|
|
||||||
title="Configure Authentication Method"
|
|
||||||
onCancel={onCloseHandler(setIsSettingsOpen)}
|
|
||||||
destroyOnClose
|
|
||||||
open={isSettingsOpen}
|
|
||||||
footer={null}
|
|
||||||
>
|
|
||||||
<Create
|
|
||||||
ssoMethod={currentDomain?.ssoType as AuthDomain['ssoType']}
|
|
||||||
assignSsoMethod={assignSsoMethod}
|
|
||||||
setIsSettingsOpen={setIsSettingsOpen}
|
|
||||||
setIsEditModalOpen={setIsEditModalOpen}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
open={isEditModalOpen}
|
|
||||||
centered
|
|
||||||
title={EditModalTitleText(currentDomain?.ssoType)}
|
|
||||||
onCancel={onCloseHandler(setIsEditModalOpen)}
|
|
||||||
destroyOnClose
|
|
||||||
style={{ minWidth: '600px' }}
|
|
||||||
footer={null}
|
|
||||||
>
|
|
||||||
<EditSSO
|
|
||||||
onRecordUpdateHandler={onRecordUpdateHandler}
|
|
||||||
record={currentDomain as AuthDomain}
|
|
||||||
setEditModalOpen={setIsEditModalOpen}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<div className="auth-domains-container">
|
|
||||||
<AddDomain refetch={refetch} />
|
|
||||||
|
|
||||||
<ResizeTable
|
|
||||||
columns={columns}
|
|
||||||
dataSource={tableData}
|
|
||||||
loading={isLoading}
|
|
||||||
tableLayout="fixed"
|
|
||||||
rowKey={(record: AuthDomain): string => record.name + v4()}
|
|
||||||
bordered
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AuthDomains;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Row } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ColumnWithTooltip = styled(Row)`
|
|
||||||
&&& > article {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,23 +1,10 @@
|
|||||||
import { Button, Form, Modal } from 'antd';
|
import { Button, Form, Modal } from 'antd';
|
||||||
import { FormInstance } from 'antd/lib';
|
import { FormInstance } from 'antd/lib';
|
||||||
import sendInvite from 'api/v1/invite/create';
|
import sendInvite from 'api/v1/invite/create';
|
||||||
import get from 'api/v1/invite/get';
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useCallback, useState } from 'react';
|
||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { PendingInvite } from 'types/api/user/getPendingInvites';
|
|
||||||
import { ROLES } from 'types/roles';
|
|
||||||
|
|
||||||
import InviteTeamMembers from '../InviteTeamMembers';
|
import InviteTeamMembers from '../InviteTeamMembers';
|
||||||
import { InviteMemberFormValues } from '../PendingInvitesContainer';
|
import { InviteMemberFormValues } from '../PendingInvitesContainer';
|
||||||
@@ -26,17 +13,7 @@ export interface InviteUserModalProps {
|
|||||||
isInviteTeamMemberModalOpen: boolean;
|
isInviteTeamMemberModalOpen: boolean;
|
||||||
toggleModal: (value: boolean) => void;
|
toggleModal: (value: boolean) => void;
|
||||||
form: FormInstance<InviteMemberFormValues>;
|
form: FormInstance<InviteMemberFormValues>;
|
||||||
setDataSource?: Dispatch<SetStateAction<DataProps[]>>;
|
onClose: () => void;
|
||||||
shouldCallApi?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataProps {
|
|
||||||
key: number;
|
|
||||||
name: string;
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
accessLevel: ROLES;
|
|
||||||
inviteLink: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
||||||
@@ -44,54 +21,15 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
|||||||
isInviteTeamMemberModalOpen,
|
isInviteTeamMemberModalOpen,
|
||||||
toggleModal,
|
toggleModal,
|
||||||
form,
|
form,
|
||||||
setDataSource,
|
|
||||||
shouldCallApi = false,
|
onClose,
|
||||||
} = props;
|
} = props;
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||||
const { user } = useAppContext();
|
|
||||||
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
|
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
|
||||||
const [modalForm] = Form.useForm<InviteMemberFormValues>(form);
|
const [modalForm] = Form.useForm<InviteMemberFormValues>(form);
|
||||||
|
|
||||||
const getPendingInvitesResponse = useQuery<
|
|
||||||
SuccessResponseV2<PendingInvite[]>,
|
|
||||||
APIError
|
|
||||||
>({
|
|
||||||
queryFn: get,
|
|
||||||
queryKey: ['getPendingInvites', user?.accessJwt],
|
|
||||||
enabled: shouldCallApi,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getParsedInviteData = useCallback(
|
|
||||||
(payload: PendingInvite[] = []) =>
|
|
||||||
payload?.map((data) => ({
|
|
||||||
key: data.createdAt,
|
|
||||||
name: data?.name,
|
|
||||||
id: data.id,
|
|
||||||
email: data.email,
|
|
||||||
accessLevel: data.role,
|
|
||||||
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
|
|
||||||
})),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
getPendingInvitesResponse.status === 'success' &&
|
|
||||||
getPendingInvitesResponse?.data?.data
|
|
||||||
) {
|
|
||||||
const data = getParsedInviteData(
|
|
||||||
getPendingInvitesResponse?.data?.data || [],
|
|
||||||
);
|
|
||||||
setDataSource?.(data);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
getParsedInviteData,
|
|
||||||
getPendingInvitesResponse?.data?.data,
|
|
||||||
getPendingInvitesResponse.status,
|
|
||||||
setDataSource,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onInviteClickHandler = useCallback(
|
const onInviteClickHandler = useCallback(
|
||||||
async (values: InviteMemberFormValues): Promise<void> => {
|
async (values: InviteMemberFormValues): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -119,10 +57,7 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
const { data, status } = await getPendingInvitesResponse.refetch();
|
onClose();
|
||||||
if (status === 'success' && data.data) {
|
|
||||||
setDataSource?.(getParsedInviteData(data?.data || []));
|
|
||||||
}
|
|
||||||
setIsInvitingMembers?.(false);
|
setIsInvitingMembers?.(false);
|
||||||
toggleModal(false);
|
toggleModal(false);
|
||||||
}, 2000);
|
}, 2000);
|
||||||
@@ -134,15 +69,7 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[notifications, onClose, t, toggleModal],
|
||||||
getParsedInviteData,
|
|
||||||
getPendingInvitesResponse,
|
|
||||||
notifications,
|
|
||||||
setDataSource,
|
|
||||||
setIsInvitingMembers,
|
|
||||||
t,
|
|
||||||
toggleModal,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -177,9 +104,4 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InviteUserModal.defaultProps = {
|
|
||||||
setDataSource: (): void => {},
|
|
||||||
shouldCallApi: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InviteUserModal;
|
export default InviteUserModal;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ColumnsType } from 'antd/lib/table';
|
|||||||
import getAll from 'api/v1/user/get';
|
import getAll from 'api/v1/user/get';
|
||||||
import deleteUser from 'api/v1/user/id/delete';
|
import deleteUser from 'api/v1/user/id/delete';
|
||||||
import update from 'api/v1/user/id/update';
|
import update from 'api/v1/user/id/update';
|
||||||
|
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -11,9 +12,7 @@ import { useAppContext } from 'providers/App/App';
|
|||||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { UserResponse } from 'types/api/user/getUsers';
|
|
||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
|
|
||||||
import DeleteMembersDetails from '../DeleteMembersDetails';
|
import DeleteMembersDetails from '../DeleteMembersDetails';
|
||||||
@@ -210,10 +209,8 @@ function UserFunction({
|
|||||||
|
|
||||||
function Members(): JSX.Element {
|
function Members(): JSX.Element {
|
||||||
const { org } = useAppContext();
|
const { org } = useAppContext();
|
||||||
const { status, data, isLoading } = useQuery<
|
|
||||||
SuccessResponseV2<UserResponse[]>,
|
const { data, isLoading, error } = useQuery({
|
||||||
APIError
|
|
||||||
>({
|
|
||||||
queryFn: () => getAll(),
|
queryFn: () => getAll(),
|
||||||
queryKey: ['getOrgUser', org?.[0].id],
|
queryKey: ['getOrgUser', org?.[0].id],
|
||||||
});
|
});
|
||||||
@@ -221,7 +218,7 @@ function Members(): JSX.Element {
|
|||||||
const [dataSource, setDataSource] = useState<DataType[]>([]);
|
const [dataSource, setDataSource] = useState<DataType[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'success' && data?.data && Array.isArray(data.data)) {
|
if (data?.data && Array.isArray(data.data)) {
|
||||||
const updatedData: DataType[] = data?.data?.map((e) => ({
|
const updatedData: DataType[] = data?.data?.map((e) => ({
|
||||||
accessLevel: e.role,
|
accessLevel: e.role,
|
||||||
email: e.email,
|
email: e.email,
|
||||||
@@ -231,7 +228,7 @@ function Members(): JSX.Element {
|
|||||||
}));
|
}));
|
||||||
setDataSource(updatedData);
|
setDataSource(updatedData);
|
||||||
}
|
}
|
||||||
}, [data?.data, status]);
|
}, [data]);
|
||||||
|
|
||||||
const columns: ColumnsType<DataType> = [
|
const columns: ColumnsType<DataType> = [
|
||||||
{
|
{
|
||||||
@@ -293,14 +290,17 @@ function Members(): JSX.Element {
|
|||||||
<div className="members-count"> ({dataSource.length}) </div>
|
<div className="members-count"> ({dataSource.length}) </div>
|
||||||
)}
|
)}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<ResizeTable
|
{!(error as APIError) && (
|
||||||
columns={columns}
|
<ResizeTable
|
||||||
tableLayout="fixed"
|
columns={columns}
|
||||||
dataSource={dataSource}
|
tableLayout="fixed"
|
||||||
pagination={false}
|
dataSource={dataSource}
|
||||||
loading={status === 'loading'}
|
pagination={false}
|
||||||
bordered
|
loading={isLoading}
|
||||||
/>
|
bordered
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{(error as APIError) && <ErrorContent error={error as APIError} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Button, Form, Space, Typography } from 'antd';
|
|||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
import get from 'api/v1/invite/get';
|
import get from 'api/v1/invite/get';
|
||||||
import deleteInvite from 'api/v1/invite/id/delete';
|
import deleteInvite from 'api/v1/invite/id/delete';
|
||||||
|
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import { INVITE_MEMBERS_HASH } from 'constants/app';
|
import { INVITE_MEMBERS_HASH } from 'constants/app';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@@ -13,7 +14,6 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { PendingInvite } from 'types/api/user/getPendingInvites';
|
import { PendingInvite } from 'types/api/user/getPendingInvites';
|
||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
@@ -48,10 +48,7 @@ function PendingInvitesContainer(): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [state.error, state.value, t, notifications]);
|
}, [state.error, state.value, t, notifications]);
|
||||||
|
|
||||||
const getPendingInvitesResponse = useQuery<
|
const { data, isLoading, error, isError, refetch } = useQuery({
|
||||||
SuccessResponseV2<PendingInvite[]>,
|
|
||||||
APIError
|
|
||||||
>({
|
|
||||||
queryFn: get,
|
queryFn: get,
|
||||||
queryKey: ['getPendingInvites', user?.accessJwt],
|
queryKey: ['getPendingInvites', user?.accessJwt],
|
||||||
});
|
});
|
||||||
@@ -90,20 +87,11 @@ function PendingInvitesContainer(): JSX.Element {
|
|||||||
}, [hash, toggleModal]);
|
}, [hash, toggleModal]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (data?.data) {
|
||||||
getPendingInvitesResponse.status === 'success' &&
|
const parsedData = getParsedInviteData(data?.data || []);
|
||||||
getPendingInvitesResponse?.data?.data
|
setDataSource(parsedData);
|
||||||
) {
|
|
||||||
const data = getParsedInviteData(
|
|
||||||
getPendingInvitesResponse?.data?.data || [],
|
|
||||||
);
|
|
||||||
setDataSource(data);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [data, getParsedInviteData]);
|
||||||
getParsedInviteData,
|
|
||||||
getPendingInvitesResponse?.data?.data,
|
|
||||||
getPendingInvitesResponse.status,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onRevokeHandler = async (id: string): Promise<void> => {
|
const onRevokeHandler = async (id: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -184,16 +172,15 @@ function PendingInvitesContainer(): JSX.Element {
|
|||||||
<InviteUserModal
|
<InviteUserModal
|
||||||
form={form}
|
form={form}
|
||||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||||
setDataSource={setDataSource}
|
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
shouldCallApi
|
onClose={refetch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="pending-invites-container">
|
<div className="pending-invites-container">
|
||||||
<TitleWrapper>
|
<TitleWrapper>
|
||||||
<Typography.Title level={3}>
|
<Typography.Title level={3}>
|
||||||
{t('pending_invites')}
|
{t('pending_invites')}
|
||||||
{getPendingInvitesResponse.status !== 'loading' && dataSource && (
|
{dataSource && (
|
||||||
<div className="members-count"> ({dataSource.length})</div>
|
<div className="members-count"> ({dataSource.length})</div>
|
||||||
)}
|
)}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
@@ -210,14 +197,17 @@ function PendingInvitesContainer(): JSX.Element {
|
|||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</TitleWrapper>
|
</TitleWrapper>
|
||||||
<ResizeTable
|
{!isError && (
|
||||||
columns={columns}
|
<ResizeTable
|
||||||
tableLayout="fixed"
|
columns={columns}
|
||||||
dataSource={dataSource}
|
tableLayout="fixed"
|
||||||
pagination={false}
|
dataSource={dataSource}
|
||||||
loading={getPendingInvitesResponse.status === 'loading'}
|
pagination={false}
|
||||||
bordered
|
loading={isLoading}
|
||||||
/>
|
bordered
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isError && <ErrorContent error={error as APIError} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import './OrganizationSettings.styles.scss';
|
|||||||
import { Space } from 'antd';
|
import { Space } from 'antd';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
|
||||||
import AuthDomains from './AuthDomains';
|
import AuthDomain from './AuthDomain';
|
||||||
import DisplayName from './DisplayName';
|
import DisplayName from './DisplayName';
|
||||||
import Members from './Members';
|
import Members from './Members';
|
||||||
import PendingInvitesContainer from './PendingInvitesContainer';
|
import PendingInvitesContainer from './PendingInvitesContainer';
|
||||||
@@ -26,7 +26,7 @@ function OrganizationSettings(): JSX.Element {
|
|||||||
<PendingInvitesContainer />
|
<PendingInvitesContainer />
|
||||||
|
|
||||||
<Members />
|
<Members />
|
||||||
<AuthDomains />
|
<AuthDomain />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const useActiveLicenseV3 = (isLoggedIn: boolean): UseLicense =>
|
|||||||
queryFn: getActive,
|
queryFn: getActive,
|
||||||
queryKey: [REACT_QUERY_KEY.GET_ACTIVE_LICENSE_V3],
|
queryKey: [REACT_QUERY_KEY.GET_ACTIVE_LICENSE_V3],
|
||||||
enabled: !!isLoggedIn,
|
enabled: !!isLoggedIn,
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
type UseLicense = UseQueryResult<SuccessResponseV2<LicenseResModel>, APIError>;
|
type UseLicense = UseQueryResult<SuccessResponseV2<LicenseResModel>, APIError>;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { HelmetProvider } from 'react-helmet-async';
|
|||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -19,9 +20,13 @@ const queryClient = new QueryClient({
|
|||||||
retry(failureCount, error): boolean {
|
retry(failureCount, error): boolean {
|
||||||
if (
|
if (
|
||||||
// in case of manually throwing errors please make sure to send error.response.status
|
// in case of manually throwing errors please make sure to send error.response.status
|
||||||
error instanceof AxiosError &&
|
(error instanceof AxiosError &&
|
||||||
error.response?.status &&
|
error.response?.status &&
|
||||||
(error.response?.status >= 400 || error.response?.status <= 499)
|
error.response?.status >= 400 &&
|
||||||
|
error.response?.status <= 499) ||
|
||||||
|
(error instanceof APIError &&
|
||||||
|
error.getHttpStatusCode() >= 400 &&
|
||||||
|
error.getHttpStatusCode() <= 499)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
import './Login.styles.scss';
|
import './Login.styles.scss';
|
||||||
|
|
||||||
import LoginContainer from 'container/Login';
|
import LoginContainer from 'container/Login';
|
||||||
import useURLQuery from 'hooks/useUrlQuery';
|
|
||||||
|
|
||||||
function Login(): JSX.Element {
|
function Login(): JSX.Element {
|
||||||
const urlQueryParams = useURLQuery();
|
|
||||||
const jwt = urlQueryParams.get('jwt') || '';
|
|
||||||
const refreshJwt = urlQueryParams.get('refreshjwt') || '';
|
|
||||||
const userId = urlQueryParams.get('usr') || '';
|
|
||||||
const ssoerror = urlQueryParams.get('ssoerror') || '';
|
|
||||||
const withPassword = urlQueryParams.get('password') || '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page-container">
|
<div className="login-page-container">
|
||||||
<div className="perilin-bg" />
|
<div className="perilin-bg" />
|
||||||
@@ -25,13 +17,7 @@ function Login(): JSX.Element {
|
|||||||
<div className="brand-title">SigNoz</div>
|
<div className="brand-title">SigNoz</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LoginContainer
|
<LoginContainer />
|
||||||
ssoerror={ssoerror}
|
|
||||||
jwt={jwt}
|
|
||||||
refreshjwt={refreshJwt}
|
|
||||||
userId={userId}
|
|
||||||
withPassword={withPassword}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,45 +1,33 @@
|
|||||||
import { Typography } from 'antd';
|
import getUserVersion from 'api/v1/version/get';
|
||||||
import getUserVersion from 'api/v1/version/getVersion';
|
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import ResetPasswordContainer from 'container/ResetPassword';
|
import ResetPasswordContainer from 'container/ResetPassword';
|
||||||
import { useAppContext } from 'providers/App/App';
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useQueries } from 'react-query';
|
import { useEffect } from 'react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
function ResetPassword(): JSX.Element {
|
function ResetPassword(): JSX.Element {
|
||||||
const { t } = useTranslation('common');
|
|
||||||
const { user, isLoggedIn } = useAppContext();
|
const { user, isLoggedIn } = useAppContext();
|
||||||
|
const { showErrorModal } = useErrorModal();
|
||||||
|
|
||||||
const [versionResponse] = useQueries([
|
const { data, isLoading, error } = useQuery({
|
||||||
{
|
queryFn: getUserVersion,
|
||||||
queryFn: getUserVersion,
|
queryKey: ['getUserVersion', user?.accessJwt],
|
||||||
queryKey: ['getUserVersion', user?.accessJwt],
|
enabled: !isLoggedIn,
|
||||||
enabled: !isLoggedIn,
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (
|
useEffect(() => {
|
||||||
versionResponse.status === 'error' ||
|
if (error) {
|
||||||
(versionResponse.status === 'success' &&
|
showErrorModal(error as APIError);
|
||||||
versionResponse.data?.statusCode !== 200)
|
}
|
||||||
) {
|
}, [error, showErrorModal]);
|
||||||
return (
|
|
||||||
<Typography>
|
|
||||||
{versionResponse.data?.error || t('something_went_wrong')}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (isLoading) {
|
||||||
versionResponse.status === 'loading' ||
|
|
||||||
!(versionResponse.data && versionResponse.data.payload)
|
|
||||||
) {
|
|
||||||
return <Spinner tip="Loading..." />;
|
return <Spinner tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { version } = versionResponse.data.payload;
|
return <ResetPasswordContainer version={data?.data.version || ''} />;
|
||||||
|
|
||||||
return <ResetPasswordContainer version={version} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ResetPassword;
|
export default ResetPassword;
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { Button, Form, Input, Typography } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import accept from 'api/v1/invite/id/accept';
|
import accept from 'api/v1/invite/id/accept';
|
||||||
import getInviteDetails from 'api/v1/invite/id/get';
|
import getInviteDetails from 'api/v1/invite/id/get';
|
||||||
import loginApi from 'api/v1/login/login';
|
import signUpApi from 'api/v1/register/post';
|
||||||
import signUpApi from 'api/v1/register/signup';
|
import passwordAuthNContext from 'api/v2/sessions/email_password/post';
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
|
||||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
@@ -17,10 +15,8 @@ import { useLocation } from 'react-router-dom';
|
|||||||
import { SuccessResponseV2 } from 'types/api';
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
import { InviteDetails } from 'types/api/user/getInviteDetails';
|
import { InviteDetails } from 'types/api/user/getInviteDetails';
|
||||||
import { Signup as LoginPrecheckPayloadProps } from 'types/api/user/loginPrecheck';
|
|
||||||
|
|
||||||
import { FormContainer, Label } from './styles';
|
import { FormContainer, Label } from './styles';
|
||||||
import { isPasswordNotValidMessage, isPasswordValid } from './utils';
|
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -34,17 +30,9 @@ type FormValues = {
|
|||||||
function SignUp(): JSX.Element {
|
function SignUp(): JSX.Element {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const [precheck, setPrecheck] = useState<LoginPrecheckPayloadProps>({
|
|
||||||
sso: false,
|
|
||||||
isUser: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [confirmPasswordError, setConfirmPasswordError] = useState<boolean>(
|
const [confirmPasswordError, setConfirmPasswordError] = useState<boolean>(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
const [isPasswordPolicyError, setIsPasswordPolicyError] = useState<boolean>(
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const params = new URLSearchParams(search);
|
const params = new URLSearchParams(search);
|
||||||
const token = params.get('token');
|
const token = params.get('token');
|
||||||
@@ -71,7 +59,6 @@ function SignUp(): JSX.Element {
|
|||||||
getInviteDetailsResponse.data.data
|
getInviteDetailsResponse.data.data
|
||||||
) {
|
) {
|
||||||
const responseDetails = getInviteDetailsResponse.data.data;
|
const responseDetails = getInviteDetailsResponse.data.data;
|
||||||
if (responseDetails.precheck) setPrecheck(responseDetails.precheck);
|
|
||||||
form.setFieldValue('firstName', responseDetails.name);
|
form.setFieldValue('firstName', responseDetails.name);
|
||||||
form.setFieldValue('email', responseDetails.email);
|
form.setFieldValue('email', responseDetails.email);
|
||||||
form.setFieldValue('organizationName', responseDetails.organization);
|
form.setFieldValue('organizationName', responseDetails.organization);
|
||||||
@@ -115,20 +102,20 @@ function SignUp(): JSX.Element {
|
|||||||
const signUp = async (values: FormValues): Promise<void> => {
|
const signUp = async (values: FormValues): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { organizationName, password, email } = values;
|
const { organizationName, password, email } = values;
|
||||||
await signUpApi({
|
const user = await signUpApi({
|
||||||
email,
|
email,
|
||||||
orgDisplayName: organizationName,
|
orgDisplayName: organizationName,
|
||||||
password,
|
password,
|
||||||
token: params.get('token') || undefined,
|
token: params.get('token') || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginResponse = await loginApi({
|
const token = await passwordAuthNContext({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
orgId: user.data.orgId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data } = loginResponse;
|
await afterLogin(token.data.accessToken, token.data.refreshToken);
|
||||||
await afterLogin(data.userId, data.accessJwt, data.refreshJwt);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorModal(error as APIError);
|
showErrorModal(error as APIError);
|
||||||
}
|
}
|
||||||
@@ -137,16 +124,17 @@ function SignUp(): JSX.Element {
|
|||||||
const acceptInvite = async (values: FormValues): Promise<void> => {
|
const acceptInvite = async (values: FormValues): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { password, email } = values;
|
const { password, email } = values;
|
||||||
await accept({
|
const user = await accept({
|
||||||
password,
|
password,
|
||||||
token: params.get('token') || '',
|
token: params.get('token') || '',
|
||||||
});
|
});
|
||||||
const loginResponse = await loginApi({
|
const token = await passwordAuthNContext({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
|
orgId: user.data.orgId,
|
||||||
});
|
});
|
||||||
const { data } = loginResponse;
|
|
||||||
await afterLogin(data.userId, data.accessJwt, data.refreshJwt);
|
await afterLogin(token.data.accessToken, token.data.refreshToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: (error as APIError).getErrorCode(),
|
message: (error as APIError).getErrorCode(),
|
||||||
@@ -155,42 +143,6 @@ function SignUp(): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitSSO = async (): Promise<void> => {
|
|
||||||
if (!params.get('token')) {
|
|
||||||
notifications.error({
|
|
||||||
message:
|
|
||||||
'Invite token is required for signup, please request one from your admin',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const response = await accept({
|
|
||||||
password: '',
|
|
||||||
token: params.get('token') || '',
|
|
||||||
sourceUrl: encodeURIComponent(window.location.href),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data?.sso) {
|
|
||||||
if (response.data?.ssoUrl) {
|
|
||||||
window.location.href = response.data?.ssoUrl;
|
|
||||||
} else {
|
|
||||||
notifications.error({
|
|
||||||
message: 'Signup completed but failed to initiate login',
|
|
||||||
});
|
|
||||||
// take user to login page as there is nothing to do here
|
|
||||||
history.push(ROUTES.LOGIN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error({
|
|
||||||
message: 'Something went wrong',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const handleSubmit = (): void => {
|
const handleSubmit = (): void => {
|
||||||
(async (): Promise<void> => {
|
(async (): Promise<void> => {
|
||||||
@@ -198,15 +150,6 @@ function SignUp(): JSX.Element {
|
|||||||
const values = form.getFieldsValue();
|
const values = form.getFieldsValue();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
if (!isPasswordValid(values.password)) {
|
|
||||||
logEvent('Account Creation Page - Invalid Password', {
|
|
||||||
email: values.email,
|
|
||||||
});
|
|
||||||
setIsPasswordPolicyError(true);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSignUp) {
|
if (isSignUp) {
|
||||||
await signUp(values);
|
await signUp(values);
|
||||||
logEvent('Account Created Successfully', {
|
logEvent('Account Created Successfully', {
|
||||||
@@ -232,9 +175,6 @@ function SignUp(): JSX.Element {
|
|||||||
if ('password' in changedValues || 'confirmPassword' in changedValues) {
|
if ('password' in changedValues || 'confirmPassword' in changedValues) {
|
||||||
const { password, confirmPassword } = form.getFieldsValue();
|
const { password, confirmPassword } = form.getFieldsValue();
|
||||||
|
|
||||||
const isInvalidPassword = !isPasswordValid(password) && password.length > 0;
|
|
||||||
setIsPasswordPolicyError(isInvalidPassword);
|
|
||||||
|
|
||||||
const isSamePassword = password === confirmPassword;
|
const isSamePassword = password === confirmPassword;
|
||||||
setConfirmPasswordError(!isSamePassword);
|
setConfirmPasswordError(!isSamePassword);
|
||||||
}
|
}
|
||||||
@@ -245,9 +185,9 @@ function SignUp(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
loading ||
|
loading ||
|
||||||
!values.email ||
|
!values.email ||
|
||||||
(!precheck.sso && (!values.password || !values.confirmPassword)) ||
|
!values.password ||
|
||||||
confirmPasswordError ||
|
!values.confirmPassword ||
|
||||||
isPasswordPolicyError
|
confirmPasswordError
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,7 +206,7 @@ function SignUp(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormContainer
|
<FormContainer
|
||||||
onFinish={!precheck.sso ? handleSubmit : handleSubmitSSO}
|
onFinish={handleSubmit}
|
||||||
onValuesChange={handleValuesChange}
|
onValuesChange={handleValuesChange}
|
||||||
form={form}
|
form={form}
|
||||||
className="signup-form"
|
className="signup-form"
|
||||||
@@ -292,23 +232,19 @@ function SignUp(): JSX.Element {
|
|||||||
</FormContainer.Item>
|
</FormContainer.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!precheck.sso && (
|
<div className="password-container">
|
||||||
<>
|
<Label htmlFor="currentPassword">Password</Label>
|
||||||
<div className="password-container">
|
<FormContainer.Item noStyle name="password">
|
||||||
<Label htmlFor="currentPassword">Password</Label>
|
<Input.Password required id="currentPassword" />
|
||||||
<FormContainer.Item noStyle name="password">
|
</FormContainer.Item>
|
||||||
<Input.Password required id="currentPassword" />
|
</div>
|
||||||
</FormContainer.Item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="password-container">
|
<div className="password-container">
|
||||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||||
<FormContainer.Item noStyle name="confirmPassword">
|
<FormContainer.Item noStyle name="confirmPassword">
|
||||||
<Input.Password required id="confirmPassword" />
|
<Input.Password required id="confirmPassword" />
|
||||||
</FormContainer.Item>
|
</FormContainer.Item>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="password-error-container">
|
<div className="password-error-container">
|
||||||
{confirmPasswordError && (
|
{confirmPasswordError && (
|
||||||
@@ -319,12 +255,6 @@ function SignUp(): JSX.Element {
|
|||||||
Passwords don’t match. Please try again
|
Passwords don’t match. Please try again
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isPasswordPolicyError && (
|
|
||||||
<Typography.Paragraph className="password-error-message">
|
|
||||||
{isPasswordNotValidMessage}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSignUp && (
|
{isSignUp && (
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* @function
|
|
||||||
* @description to check whether password is valid or not
|
|
||||||
* @reference stackoverflow.com/a/69807687
|
|
||||||
* @returns Boolean
|
|
||||||
*/
|
|
||||||
export const isPasswordValid = (value: string): boolean => {
|
|
||||||
// eslint-disable-next-line prefer-regex-literals
|
|
||||||
const pattern = new RegExp('^.{8,}$');
|
|
||||||
return pattern.test(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const isPasswordNotValidMessage = `Password must a have minimum of 8 characters`;
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import { Logout } from 'api/utils';
|
|
||||||
import listOrgPreferences from 'api/v1/org/preferences/list';
|
import listOrgPreferences from 'api/v1/org/preferences/list';
|
||||||
|
import get from 'api/v1/user/me/get';
|
||||||
import listUserPreferences from 'api/v1/user/preferences/list';
|
import listUserPreferences from 'api/v1/user/preferences/list';
|
||||||
import getUserVersion from 'api/v1/version/getVersion';
|
import getUserVersion from 'api/v1/version/get';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
|
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
|
||||||
import { useGetFeatureFlag } from 'hooks/useGetFeatureFlag';
|
import { useGetFeatureFlag } from 'hooks/useGetFeatureFlag';
|
||||||
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
|
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
|
||||||
import useGetUser from 'hooks/user/useGetUser';
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
@@ -40,7 +39,7 @@ import { getUserDefaults } from './utils';
|
|||||||
export const AppContext = createContext<IAppContext | undefined>(undefined);
|
export const AppContext = createContext<IAppContext | undefined>(undefined);
|
||||||
|
|
||||||
export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||||
// on load of the provider set the user defaults with access jwt , refresh jwt and user id from local storage
|
// on load of the provider set the user defaults with access token , refresh token from local storage
|
||||||
const [user, setUser] = useState<IUser>(() => getUserDefaults());
|
const [user, setUser] = useState<IUser>(() => getUserDefaults());
|
||||||
const [activeLicense, setActiveLicense] = useState<LicenseResModel | null>(
|
const [activeLicense, setActiveLicense] = useState<LicenseResModel | null>(
|
||||||
null,
|
null,
|
||||||
@@ -63,13 +62,6 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
|||||||
|
|
||||||
const [showChangelogModal, setShowChangelogModal] = useState<boolean>(false);
|
const [showChangelogModal, setShowChangelogModal] = useState<boolean>(false);
|
||||||
|
|
||||||
// if the user.id is not present, for migration older cases then we need to logout only for current logged in users!
|
|
||||||
useEffect(() => {
|
|
||||||
if (!user.id && isLoggedIn) {
|
|
||||||
Logout();
|
|
||||||
}
|
|
||||||
}, [isLoggedIn, user]);
|
|
||||||
|
|
||||||
// fetcher for user
|
// fetcher for user
|
||||||
// user will only be fetched if the user id and token is present
|
// user will only be fetched if the user id and token is present
|
||||||
// if logged out and trying to hit any route none of these calls will trigger
|
// if logged out and trying to hit any route none of these calls will trigger
|
||||||
@@ -77,7 +69,12 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
|||||||
data: userData,
|
data: userData,
|
||||||
isFetching: isFetchingUser,
|
isFetching: isFetchingUser,
|
||||||
error: userFetchError,
|
error: userFetchError,
|
||||||
} = useGetUser(user.id, isLoggedIn);
|
} = useQuery({
|
||||||
|
queryFn: get,
|
||||||
|
queryKey: ['/api/v1/user/me'],
|
||||||
|
enabled: isLoggedIn,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFetchingUser && userData && userData.data) {
|
if (!isFetchingUser && userData && userData.data) {
|
||||||
setUser((prev) => ({
|
setUser((prev) => ({
|
||||||
@@ -320,7 +317,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
|||||||
updateOrg,
|
updateOrg,
|
||||||
updateChangelog,
|
updateChangelog,
|
||||||
toggleChangelogModal,
|
toggleChangelogModal,
|
||||||
versionData: versionData?.payload || null,
|
versionData: versionData?.data || null,
|
||||||
hasEditPermission:
|
hasEditPermission:
|
||||||
user?.role === USER_ROLES.ADMIN || user?.role === USER_ROLES.EDITOR,
|
user?.role === USER_ROLES.ADMIN || user?.role === USER_ROLES.EDITOR,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
} from 'types/api/preferences/preference';
|
} from 'types/api/preferences/preference';
|
||||||
import { Organization } from 'types/api/user/getOrganization';
|
import { Organization } from 'types/api/user/getOrganization';
|
||||||
import { UserResponse as User } from 'types/api/user/getUser';
|
import { UserResponse as User } from 'types/api/user/getUser';
|
||||||
import { PayloadProps } from 'types/api/user/getVersion';
|
import { Info } from 'types/api/v1/version/get';
|
||||||
|
|
||||||
export interface IAppContext {
|
export interface IAppContext {
|
||||||
user: IUser;
|
user: IUser;
|
||||||
@@ -36,7 +36,7 @@ export interface IAppContext {
|
|||||||
updateOrg(orgId: string, updatedOrgName: string): void;
|
updateOrg(orgId: string, updatedOrgName: string): void;
|
||||||
updateChangelog(payload: ChangelogSchema): void;
|
updateChangelog(payload: ChangelogSchema): void;
|
||||||
toggleChangelogModal(): void;
|
toggleChangelogModal(): void;
|
||||||
versionData: PayloadProps | null;
|
versionData: Info | null;
|
||||||
hasEditPermission: boolean;
|
hasEditPermission: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,11 @@ function getUserDefaults(): IUser {
|
|||||||
getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN),
|
getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN),
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
const userId = defaultTo(getLocalStorageApi(LOCALSTORAGE.USER_ID), '');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessJwt,
|
accessJwt,
|
||||||
refreshJwt,
|
refreshJwt,
|
||||||
id: userId,
|
id: '',
|
||||||
email: '',
|
email: '',
|
||||||
displayName: '',
|
displayName: '',
|
||||||
createdAt: 0,
|
createdAt: 0,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { apiV3 } from 'api/apiV1';
|
import { apiV3 } from 'api/apiV1';
|
||||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||||
import { Logout } from 'api/utils';
|
import { Logout } from 'api/utils';
|
||||||
import loginApi from 'api/v1/login/login';
|
import post from 'api/v2/sessions/rotate/post';
|
||||||
import afterLogin from 'AppRoutes/utils';
|
import afterLogin from 'AppRoutes/utils';
|
||||||
import { ENVIRONMENT } from 'constants/env';
|
import { ENVIRONMENT } from 'constants/env';
|
||||||
import { LIVE_TAIL_HEARTBEAT_TIMEOUT } from 'constants/liveTail';
|
import { LIVE_TAIL_HEARTBEAT_TIMEOUT } from 'constants/liveTail';
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { useQueryClient } from 'react-query';
|
||||||
import APIError from 'types/api/error';
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
interface IEventSourceContext {
|
interface IEventSourceContext {
|
||||||
@@ -58,6 +59,7 @@ export function EventSourceProvider({
|
|||||||
const eventSourceRef = useRef<EventSourcePolyfill | null>(null);
|
const eventSourceRef = useRef<EventSourcePolyfill | null>(null);
|
||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const handleSetInitialLoading = useCallback((value: boolean) => {
|
const handleSetInitialLoading = useCallback((value: boolean) => {
|
||||||
setInitialLoading(value);
|
setInitialLoading(value);
|
||||||
@@ -75,15 +77,15 @@ export function EventSourceProvider({
|
|||||||
setInitialLoading(false);
|
setInitialLoading(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await loginApi({
|
const accessToken = getLocalStorageApi(LOCALSTORAGE.AUTH_TOKEN);
|
||||||
refreshToken: getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN) || '',
|
const refreshToken = getLocalStorageApi(LOCALSTORAGE.REFRESH_AUTH_TOKEN);
|
||||||
|
|
||||||
|
const response = await queryClient.fetchQuery({
|
||||||
|
queryFn: () => post({ refreshToken: refreshToken || '' }),
|
||||||
|
queryKey: ['/api/v2/sessions/rotate', accessToken, refreshToken],
|
||||||
});
|
});
|
||||||
afterLogin(
|
afterLogin(response.data.accessToken, response.data.refreshToken, true);
|
||||||
response.data.userId,
|
|
||||||
response.data.accessJwt,
|
|
||||||
response.data.refreshJwt,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
// If token refresh was successful, we'll let the component
|
// If token refresh was successful, we'll let the component
|
||||||
// handle reconnection through the reconnectDueToError state
|
// handle reconnection through the reconnectDueToError state
|
||||||
setReconnectDueToError(true);
|
setReconnectDueToError(true);
|
||||||
@@ -101,7 +103,7 @@ export function EventSourceProvider({
|
|||||||
eventSourceRef.current.close();
|
eventSourceRef.current.close();
|
||||||
Logout();
|
Logout();
|
||||||
}
|
}
|
||||||
}, [notifications]);
|
}, [notifications, queryClient]);
|
||||||
|
|
||||||
const destroyEventSourceSession = useCallback(() => {
|
const destroyEventSourceSession = useCallback(() => {
|
||||||
if (!eventSourceRef.current) return;
|
if (!eventSourceRef.current) return;
|
||||||
|
|||||||
@@ -55,3 +55,8 @@ export interface Warning {
|
|||||||
url: string;
|
url: string;
|
||||||
warnings: AdditionalWarnings[];
|
warnings: AdditionalWarnings[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawSuccessResponse<T> {
|
||||||
|
status: string;
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { UserResponse } from './getUser';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
token: string;
|
token: string;
|
||||||
password: string;
|
password: string;
|
||||||
@@ -13,6 +15,6 @@ export interface LoginPrecheckResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PayloadProps {
|
export interface PayloadProps {
|
||||||
data: LoginPrecheckResponse;
|
data: UserResponse;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { User } from 'types/reducer/app';
|
|||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
|
|
||||||
import { Organization } from './getOrganization';
|
import { Organization } from './getOrganization';
|
||||||
import { Signup as LoginPrecheckPayloadProps } from './loginPrecheck';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
inviteId: string;
|
inviteId: string;
|
||||||
@@ -20,5 +19,4 @@ export interface InviteDetails {
|
|||||||
role: ROLES;
|
role: ROLES;
|
||||||
token: string;
|
token: string;
|
||||||
organization: Organization['displayName'];
|
organization: Organization['displayName'];
|
||||||
precheck?: LoginPrecheckPayloadProps;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
export interface PayloadProps {
|
|
||||||
data: UserLoginResponse;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
email?: string;
|
|
||||||
password?: string;
|
|
||||||
refreshToken?: UserLoginResponse['refreshJwt'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserLoginResponse {
|
|
||||||
accessJwt: string;
|
|
||||||
accessJwtExpiry: number;
|
|
||||||
refreshJwt: string;
|
|
||||||
refreshJwtExpiry: number;
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
export interface PayloadProps {
|
|
||||||
data: Signup;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Signup {
|
|
||||||
sso: boolean;
|
|
||||||
ssoUrl?: string;
|
|
||||||
canSelfRegister?: boolean;
|
|
||||||
isUser: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
email: string;
|
|
||||||
path?: string;
|
|
||||||
}
|
|
||||||
44
frontend/src/types/api/v1/domains/list.ts
Normal file
44
frontend/src/types/api/v1/domains/list.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
export const SSOType = new Map<string, string>([
|
||||||
|
['google_auth', 'Google Auth'],
|
||||||
|
['saml', 'SAML'],
|
||||||
|
['email_password', 'Email Password'],
|
||||||
|
['oidc', 'OIDC'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
export interface GettableAuthDomain {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
orgId: string;
|
||||||
|
ssoEnabled: boolean;
|
||||||
|
ssoType: string;
|
||||||
|
samlConfig?: SAMLConfig;
|
||||||
|
googleAuthConfig?: GoogleAuthConfig;
|
||||||
|
oidcConfig?: OIDCConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SAMLConfig {
|
||||||
|
samlEntity: string;
|
||||||
|
samlIdp: string;
|
||||||
|
samlCert: string;
|
||||||
|
insecureSkipAuthNRequestsSigned: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleAuthConfig {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
redirectURI: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OIDCConfig {
|
||||||
|
issuer: string;
|
||||||
|
issuerAlias: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
claimMapping: ClaimMapping;
|
||||||
|
insecureSkipEmailVerified: boolean;
|
||||||
|
getUserInfo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimMapping {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
39
frontend/src/types/api/v1/domains/post.ts
Normal file
39
frontend/src/types/api/v1/domains/post.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export interface PostableAuthDomain {
|
||||||
|
name: string;
|
||||||
|
config: Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
ssoEnabled: boolean;
|
||||||
|
ssoType: string;
|
||||||
|
samlConfig?: SAMLConfig;
|
||||||
|
googleAuthConfig?: GoogleAuthConfig;
|
||||||
|
oidcConfig?: OIDCConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SAMLConfig {
|
||||||
|
samlEntity: string;
|
||||||
|
samlIdp: string;
|
||||||
|
samlCert: string;
|
||||||
|
insecureSkipAuthNRequestsSigned: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleAuthConfig {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
redirectURI: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OIDCConfig {
|
||||||
|
issuer: string;
|
||||||
|
issuerAlias: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
claimMapping: ClaimMapping;
|
||||||
|
insecureSkipEmailVerified: boolean;
|
||||||
|
getUserInfo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimMapping {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
37
frontend/src/types/api/v1/domains/put.ts
Normal file
37
frontend/src/types/api/v1/domains/put.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export interface UpdatableAuthDomain {
|
||||||
|
config: {
|
||||||
|
ssoEnabled: boolean;
|
||||||
|
ssoType: string;
|
||||||
|
samlConfig?: SAMLConfig;
|
||||||
|
googleAuthConfig?: GoogleAuthConfig;
|
||||||
|
oidcConfig?: OIDCConfig;
|
||||||
|
};
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SAMLConfig {
|
||||||
|
samlEntity: string;
|
||||||
|
samlIdp: string;
|
||||||
|
samlCert: string;
|
||||||
|
insecureSkipAuthNRequestsSigned: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GoogleAuthConfig {
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
redirectURI: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OIDCConfig {
|
||||||
|
issuer: string;
|
||||||
|
issuerAlias: string;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
|
claimMapping: ClaimMapping;
|
||||||
|
insecureSkipEmailVerified: boolean;
|
||||||
|
getUserInfo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimMapping {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
10
frontend/src/types/api/v1/register/post.ts
Normal file
10
frontend/src/types/api/v1/register/post.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { ROLES } from 'types/roles';
|
||||||
|
|
||||||
|
export interface SignupResponse {
|
||||||
|
createdAt: number;
|
||||||
|
email: string;
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
orgId: string;
|
||||||
|
role: ROLES;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface PayloadProps {
|
export interface Info {
|
||||||
version: string;
|
version: string;
|
||||||
ee: 'Y' | 'N';
|
ee: 'Y' | 'N';
|
||||||
setupCompleted: boolean;
|
setupCompleted: boolean;
|
||||||
32
frontend/src/types/api/v2/sessions/context/get.ts
Normal file
32
frontend/src/types/api/v2/sessions/context/get.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ErrorV2 } from 'types/api';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
email: string;
|
||||||
|
ref: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordAuthN {
|
||||||
|
provider: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CallbackAuthN {
|
||||||
|
provider: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthNSupport {
|
||||||
|
password: PasswordAuthN[];
|
||||||
|
callback: CallbackAuthN[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrgSessionContext {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
authNSupport: AuthNSupport;
|
||||||
|
warning?: ErrorV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionsContext {
|
||||||
|
exists: boolean;
|
||||||
|
orgs: OrgSessionContext[];
|
||||||
|
}
|
||||||
10
frontend/src/types/api/v2/sessions/email_password/post.ts
Normal file
10
frontend/src/types/api/v2/sessions/email_password/post.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export interface Props {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
orgId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
8
frontend/src/types/api/v2/sessions/rotate/post.ts
Normal file
8
frontend/src/types/api/v2/sessions/rotate/post.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface Props {
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}
|
||||||
14
go.mod
14
go.mod
@@ -9,6 +9,7 @@ require (
|
|||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||||
github.com/SigNoz/signoz-otel-collector v0.129.4
|
github.com/SigNoz/signoz-otel-collector v0.129.4
|
||||||
|
github.com/allegro/bigcache/v3 v3.1.0
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||||
github.com/antonmedv/expr v1.15.3
|
github.com/antonmedv/expr v1.15.3
|
||||||
github.com/cespare/xxhash/v2 v2.3.0
|
github.com/cespare/xxhash/v2 v2.3.0
|
||||||
@@ -17,8 +18,7 @@ require (
|
|||||||
github.com/go-co-op/gocron v1.30.1
|
github.com/go-co-op/gocron v1.30.1
|
||||||
github.com/go-openapi/runtime v0.28.0
|
github.com/go-openapi/runtime v0.28.0
|
||||||
github.com/go-openapi/strfmt v0.23.0
|
github.com/go-openapi/strfmt v0.23.0
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redismock/v9 v9.2.0
|
||||||
github.com/go-redis/redismock/v8 v8.11.5
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0
|
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||||
github.com/gojek/heimdall/v7 v7.0.3
|
github.com/gojek/heimdall/v7 v7.0.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
@@ -44,6 +44,8 @@ require (
|
|||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/prometheus/common v0.66.1
|
github.com/prometheus/common v0.66.1
|
||||||
github.com/prometheus/prometheus v0.304.1
|
github.com/prometheus/prometheus v0.304.1
|
||||||
|
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1
|
||||||
|
github.com/redis/go-redis/v9 v9.15.1
|
||||||
github.com/rs/cors v1.11.1
|
github.com/rs/cors v1.11.1
|
||||||
github.com/russellhaering/gosaml2 v0.9.0
|
github.com/russellhaering/gosaml2 v0.9.0
|
||||||
github.com/russellhaering/goxmldsig v1.2.0
|
github.com/russellhaering/goxmldsig v1.2.0
|
||||||
@@ -59,10 +61,12 @@ require (
|
|||||||
github.com/uptrace/bun v1.2.9
|
github.com/uptrace/bun v1.2.9
|
||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||||
|
github.com/uptrace/bun/extra/bunotel v1.2.9
|
||||||
go.opentelemetry.io/collector/confmap v1.34.0
|
go.opentelemetry.io/collector/confmap v1.34.0
|
||||||
go.opentelemetry.io/collector/otelcol v0.128.0
|
go.opentelemetry.io/collector/otelcol v0.128.0
|
||||||
go.opentelemetry.io/collector/pdata v1.34.0
|
go.opentelemetry.io/collector/pdata v1.34.0
|
||||||
go.opentelemetry.io/contrib/config v0.10.0
|
go.opentelemetry.io/contrib/config v0.10.0
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0
|
||||||
go.opentelemetry.io/otel v1.38.0
|
go.opentelemetry.io/otel v1.38.0
|
||||||
go.opentelemetry.io/otel/metric v1.38.0
|
go.opentelemetry.io/otel/metric v1.38.0
|
||||||
@@ -85,7 +89,9 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
|
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
modernc.org/libc v1.66.3 // indirect
|
modernc.org/libc v1.66.3 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
@@ -297,7 +303,7 @@ require (
|
|||||||
go.opentelemetry.io/collector/receiver/receiverhelper v0.128.0 // indirect
|
go.opentelemetry.io/collector/receiver/receiverhelper v0.128.0 // indirect
|
||||||
go.opentelemetry.io/collector/receiver/receivertest v0.128.0 // indirect
|
go.opentelemetry.io/collector/receiver/receivertest v0.128.0 // indirect
|
||||||
go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 // indirect
|
go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 // indirect
|
||||||
go.opentelemetry.io/collector/semconv v0.128.0 // indirect
|
go.opentelemetry.io/collector/semconv v0.128.0
|
||||||
go.opentelemetry.io/collector/service v0.128.0 // indirect
|
go.opentelemetry.io/collector/service v0.128.0 // indirect
|
||||||
go.opentelemetry.io/collector/service/hostcapabilities v0.128.0 // indirect
|
go.opentelemetry.io/collector/service/hostcapabilities v0.128.0 // indirect
|
||||||
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
|
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
|
||||||
@@ -314,7 +320,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/exporters/prometheus v0.58.0
|
go.opentelemetry.io/otel/exporters/prometheus v0.58.0
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 // indirect
|
||||||
go.opentelemetry.io/otel/log v0.12.2 // indirect
|
go.opentelemetry.io/otel/log v0.12.2 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
|
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
go.opentelemetry.io/otel/sdk/metric v1.38.0
|
||||||
|
|||||||
53
go.sum
53
go.sum
@@ -118,6 +118,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
|||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
|
||||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
|
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
|
||||||
|
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
|
||||||
|
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
@@ -158,6 +160,10 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
|||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||||
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
|
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||||
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
|
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
@@ -166,7 +172,6 @@ github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9
|
|||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
@@ -267,7 +272,6 @@ github.com/foxboron/go-tpm-keyfiles v0.0.0-20250323135004-b31fac66206e/go.mod h1
|
|||||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
@@ -325,16 +329,13 @@ github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvSc
|
|||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
|
||||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
|
||||||
github.com/go-redis/redismock/v8 v8.11.5 h1:RJFIiua58hrBrSpXhnGX3on79AU3S271H4ZhRI1wyVo=
|
|
||||||
github.com/go-redis/redismock/v8 v8.11.5/go.mod h1:UaAU9dEe1C+eGr+FHV5prCWIt0hafyPWbGMEWE0UWdA=
|
|
||||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
|
||||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
@@ -439,7 +440,6 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
|
|||||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
|
||||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
@@ -562,7 +562,6 @@ github.com/hetznercloud/hcloud-go/v2 v2.21.0 h1:wUpQT+fgAxIcdMtFvuCJ78ziqc/VARub
|
|||||||
github.com/hetznercloud/hcloud-go/v2 v2.21.0/go.mod h1:WSM7w+9tT86sJTNcF8a/oHljC3HUmQfcLxYsgx6PpSc=
|
github.com/hetznercloud/hcloud-go/v2 v2.21.0/go.mod h1:WSM7w+9tT86sJTNcF8a/oHljC3HUmQfcLxYsgx6PpSc=
|
||||||
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
|
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
|
||||||
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
|
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
|
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
|
||||||
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
|
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
|
||||||
github.com/huandu/go-sqlbuilder v1.35.0 h1:ESvxFHN8vxCTudY1Vq63zYpU5yJBESn19sf6k4v2T5Q=
|
github.com/huandu/go-sqlbuilder v1.35.0 h1:ESvxFHN8vxCTudY1Vq63zYpU5yJBESn19sf6k4v2T5Q=
|
||||||
@@ -736,7 +735,6 @@ github.com/natefinch/wrap v0.2.0/go.mod h1:6gMHlAl12DwYEfKP3TkuykYUfLSEAvHw67itm
|
|||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||||
@@ -746,16 +744,8 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
|||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
|
||||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
|
||||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||||
github.com/open-telemetry/opamp-go v0.19.0 h1:8LvQKDwqi+BU3Yy159SU31e2XB0vgnk+PN45pnKilPs=
|
github.com/open-telemetry/opamp-go v0.19.0 h1:8LvQKDwqi+BU3Yy159SU31e2XB0vgnk+PN45pnKilPs=
|
||||||
@@ -869,8 +859,12 @@ github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAo
|
|||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
github.com/redis/go-redis/v9 v9.9.0 h1:URbPQ4xVQSQhZ27WMQVmZSo3uT3pL+4IdHVcYq2nVfM=
|
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 h1:G3pzZlMvMX9VX9TBB8zr03CAkeyMtbyW2D59PdyaGkM=
|
||||||
github.com/redis/go-redis/v9 v9.9.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1/go.mod h1:JiJ4f0bngycE8LQqzY/4TB23witBbFnlUS6hPvHn6Zc=
|
||||||
|
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1 h1:gvNK57rhjwIjAiGTSZH2+XO37mcLyYCsJC1qlNUnBjs=
|
||||||
|
github.com/redis/go-redis/extra/redisotel/v9 v9.15.1/go.mod h1:O41kV1OVBXIT0Tipo902iT8+rbqF0zL5v5paLxp5/7s=
|
||||||
|
github.com/redis/go-redis/v9 v9.15.1 h1:BVn5z3pdIKIr5WI4Yv1MRXslB616gqBLBgVmhykiHIw=
|
||||||
|
github.com/redis/go-redis/v9 v9.15.1/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||||
@@ -1005,6 +999,10 @@ github.com/uptrace/bun/dialect/pgdialect v1.2.9 h1:caf5uFbOGiXvadV6pA5gn87k0awFF
|
|||||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9/go.mod h1:m7L9JtOp/Lt8HccET70ULxplMweE/u0S9lNUSxz2duo=
|
github.com/uptrace/bun/dialect/pgdialect v1.2.9/go.mod h1:m7L9JtOp/Lt8HccET70ULxplMweE/u0S9lNUSxz2duo=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9 h1:HLzGWXBh07sT8zhVPy6veYbbGrAtYq0KzyRHXBj+GjA=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9 h1:HLzGWXBh07sT8zhVPy6veYbbGrAtYq0KzyRHXBj+GjA=
|
||||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9/go.mod h1:dUR+ecoCWA0FIa9vhQVRnGtYYPpuCLJoEEtX9E1aiBU=
|
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9/go.mod h1:dUR+ecoCWA0FIa9vhQVRnGtYYPpuCLJoEEtX9E1aiBU=
|
||||||
|
github.com/uptrace/bun/extra/bunotel v1.2.9 h1:BGGrBga+iVL78SGiMpLt2N9MAKvrG3f8wLk8zCLwFJg=
|
||||||
|
github.com/uptrace/bun/extra/bunotel v1.2.9/go.mod h1:6dVl5Ko6xOhuoqUPWHpfFrntBDwmOnq0OMiR/SGwAC8=
|
||||||
|
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c=
|
||||||
|
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ=
|
||||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
github.com/vjeantet/grok v1.0.1 h1:2rhIR7J4gThTgcZ1m2JY4TrJZNgjn985U28kT2wQrJ4=
|
github.com/vjeantet/grok v1.0.1 h1:2rhIR7J4gThTgcZ1m2JY4TrJZNgjn985U28kT2wQrJ4=
|
||||||
@@ -1164,6 +1162,8 @@ go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 h1:u2E32P7j1a/gRgZDWhIXC+Shd
|
|||||||
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0/go.mod h1:pJPCLM8gzX4ASqLlyAXjHBEYxgbOQJ/9bidWxD6PEPQ=
|
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0/go.mod h1:pJPCLM8gzX4ASqLlyAXjHBEYxgbOQJ/9bidWxD6PEPQ=
|
||||||
go.opentelemetry.io/contrib/config v0.10.0 h1:2JknAzMaYjxrHkTnZh3eOme/Y2P5eHE2SWfhfV6Xd6c=
|
go.opentelemetry.io/contrib/config v0.10.0 h1:2JknAzMaYjxrHkTnZh3eOme/Y2P5eHE2SWfhfV6Xd6c=
|
||||||
go.opentelemetry.io/contrib/config v0.10.0/go.mod h1:aND2M6/KfNkntI5cyvHriR/zvZgPf8j9yETdSmvpfmc=
|
go.opentelemetry.io/contrib/config v0.10.0/go.mod h1:aND2M6/KfNkntI5cyvHriR/zvZgPf8j9yETdSmvpfmc=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0 h1:rATLgFjv0P9qyXQR/aChJ6JVbMtXOQjt49GgT36cBbk=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.63.0/go.mod h1:34csimR1lUhdT5HH4Rii9aKPrvBcnFRwxLwcevsU+Kk=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 h1:0tY123n7CdWMem7MOVdKOt0YfshufLCwfE5Bob+hQuM=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||||
@@ -1196,8 +1196,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 h1:12vMqzLLNZtXuXbJh
|
|||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2/go.mod h1:ZccPZoPOoq8x3Trik/fCsba7DEYDUnN6yX79pgp2BUQ=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0 h1:kJxSDN4SgWWTjG/hPp3O7LCGLcHXFlvS2/FFOrwL+SE=
|
||||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY=
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.38.0/go.mod h1:mgIOzS7iZeKJdeB8/NYHrJ48fdGc71Llo5bJ1J4DWUE=
|
||||||
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
|
go.opentelemetry.io/otel/log v0.12.2 h1:yob9JVHn2ZY24byZeaXpTVoPS6l+UrrxmxmPKohXTwc=
|
||||||
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
|
go.opentelemetry.io/otel/log v0.12.2/go.mod h1:ShIItIxSYxufUMt+1H5a2wbckGli3/iCfuEbVZi/98E=
|
||||||
go.opentelemetry.io/otel/log/logtest v0.0.0-20250526142609-aa5bd0e64989 h1:4JF7oY9CcHrPGfBLijDcXZyCzGckVEyOjuat5ktmQRg=
|
go.opentelemetry.io/otel/log/logtest v0.0.0-20250526142609-aa5bd0e64989 h1:4JF7oY9CcHrPGfBLijDcXZyCzGckVEyOjuat5ktmQRg=
|
||||||
@@ -1291,7 +1291,6 @@ golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
|||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -1315,7 +1314,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
|||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
@@ -1331,7 +1329,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
|
||||||
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
@@ -1381,7 +1378,6 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
|||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -1395,7 +1391,6 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -1403,7 +1398,6 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -1430,7 +1424,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -1539,7 +1532,6 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
|
|||||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
@@ -1752,7 +1744,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
|
|||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user