diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go
index fd44fba29a..601bed7714 100644
--- a/ee/query-service/app/api/api.go
+++ b/ee/query-service/app/api/api.go
@@ -9,6 +9,7 @@ import (
"go.signoz.io/signoz/ee/query-service/license"
baseapp "go.signoz.io/signoz/pkg/query-service/app"
baseint "go.signoz.io/signoz/pkg/query-service/interfaces"
+ basemodel "go.signoz.io/signoz/pkg/query-service/model"
rules "go.signoz.io/signoz/pkg/query-service/rules"
"go.signoz.io/signoz/pkg/query-service/version"
)
@@ -96,7 +97,7 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router) {
router.HandleFunc("/api/v1/complete/google",
baseapp.OpenAccess(ah.receiveGoogleAuth)).
Methods(http.MethodGet)
-
+
router.HandleFunc("/api/v1/orgs/{orgId}/domains",
baseapp.AdminAccess(ah.listDomainsByOrg)).
Methods(http.MethodGet)
@@ -127,5 +128,11 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router) {
func (ah *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
version := version.GetVersion()
- ah.WriteJSON(w, r, map[string]string{"version": version, "ee": "Y"})
+ versionResponse := basemodel.GetVersionResponse{
+ Version: version,
+ EE: "Y",
+ SetupCompleted: ah.SetupCompleted,
+ }
+
+ ah.WriteJSON(w, r, versionResponse)
}
diff --git a/ee/query-service/app/api/auth.go b/ee/query-service/app/api/auth.go
index 2e622408be..8d96320778 100644
--- a/ee/query-service/app/api/auth.go
+++ b/ee/query-service/app/api/auth.go
@@ -8,6 +8,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
+
"github.com/gorilla/mux"
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
@@ -87,9 +88,16 @@ func (ah *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
// get invite object
invite, err := baseauth.ValidateInvite(ctx, req)
- if err != nil || invite == nil {
+ if err != nil {
zap.S().Errorf("failed to validate invite token", err)
+ RespondError(w, model.BadRequest(err), nil)
+ return
+ }
+
+ if invite == nil {
+ zap.S().Errorf("failed to validate invite token: it is either empty or invalid", err)
RespondError(w, model.BadRequest(basemodel.ErrSignupFailed{}), nil)
+ return
}
// get auth domain from email domain
@@ -190,7 +198,7 @@ func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string)
}
// receiveGoogleAuth completes google OAuth response and forwards a request
-// to front-end to sign user in
+// to front-end to sign user in
func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) {
redirectUri := constants.GetDefaultSiteURL()
ctx := context.Background()
@@ -221,15 +229,15 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
// 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.
+ // fetch domain by parsing relay state.
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
if err != nil {
handleSsoError(w, r, redirectUri)
return
}
- // now that we have domain, use domain to fetch sso settings.
- // prepare google callback handler using parsedState -
+ // now that we have domain, use domain to fetch sso settings.
+ // prepare google callback handler using parsedState -
// which contains redirect URL (front-end endpoint)
callbackHandler, err := domain.PrepareGoogleOAuthProvider(parsedState)
@@ -239,7 +247,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
handleSsoError(w, r, redirectUri)
return
}
-
+
nextPage, err := ah.AppDao().PrepareSsoRedirect(ctx, redirectUri, identity.Email)
if err != nil {
zap.S().Errorf("[receiveGoogleAuth] failed to generate redirect URI after successful login ", domain.String(), zap.Error(err))
@@ -250,15 +258,12 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, nextPage, 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()
-
if !ah.CheckFeature(model.SSO) {
zap.S().Errorf("[receiveSAML] sso requested but feature unavailable %s in org domain %s", model.SSO)
http.Redirect(w, r, fmt.Sprintf("%s?ssoerror=%s", redirectUri, "feature unavailable, please upgrade your billing plan to access this feature"), http.StatusMovedPermanently)
@@ -287,13 +292,13 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
// 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.
+ // fetch domain by parsing relay state.
domain, err := ah.AppDao().GetDomainFromSsoResponse(ctx, parsedState)
if err != nil {
handleSsoError(w, r, redirectUri)
return
}
-
+
sp, err := domain.PrepareSamlRequest(parsedState)
if err != nil {
zap.S().Errorf("[receiveSAML] failed to prepare saml request for domain (%s): %v", domain.String(), err)
@@ -327,6 +332,6 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) {
handleSsoError(w, r, redirectUri)
return
}
-
+
http.Redirect(w, r, nextPage, http.StatusSeeOther)
}
diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx
index 640a7340a5..c31a2bf6f9 100644
--- a/frontend/src/container/AppLayout/index.tsx
+++ b/frontend/src/container/AppLayout/index.tsx
@@ -153,6 +153,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
type: UPDATE_CURRENT_VERSION,
payload: {
currentVersion: getUserVersionResponse.data.payload.version,
+ ee: getUserVersionResponse.data.payload.ee,
+ setupCompleted: getUserVersionResponse.data.payload.setupCompleted,
},
});
}
diff --git a/frontend/src/container/Login/index.tsx b/frontend/src/container/Login/index.tsx
index e8c25f79f0..a8fda93908 100644
--- a/frontend/src/container/Login/index.tsx
+++ b/frontend/src/container/Login/index.tsx
@@ -1,4 +1,5 @@
import { Button, Input, Space, Tooltip, Typography } from 'antd';
+import getUserVersion from 'api/user/getVersion';
import loginApi from 'api/user/login';
import loginPrecheckApi from 'api/user/loginPrecheck';
import afterLogin from 'AppRoutes/utils';
@@ -7,6 +8,7 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
+import { useQuery } from 'react-query';
import { PayloadProps as PrecheckResultType } from 'types/api/user/loginPrecheck';
import { FormContainer, FormWrapper, Label, ParentContainer } from './styles';
@@ -45,6 +47,26 @@ function Login({
const { notifications } = useNotifications();
+ const getUserVersionResponse = useQuery({
+ queryFn: getUserVersion,
+ queryKey: 'getUserVersion',
+ enabled: true,
+ });
+
+ useEffect(() => {
+ if (
+ getUserVersionResponse.isFetched &&
+ getUserVersionResponse.data &&
+ getUserVersionResponse.data.payload
+ ) {
+ const { setupCompleted } = getUserVersionResponse.data.payload;
+ if (!setupCompleted) {
+ // no org account registered yet, re-route user to sign up first
+ history.push(ROUTES.SIGN_UP);
+ }
+ }
+ }, [getUserVersionResponse]);
+
useEffect(() => {
if (withPassword === 'Y') {
setPrecheckComplete(true);
@@ -255,20 +277,6 @@ function Login({
)}
- {!canSelfRegister && (
-
- {t('prompt_create_account')}{' '}
- {
- history.push(ROUTES.SIGN_UP);
- }}
- style={{ fontWeight: 700 }}
- >
- {t('create_an_account')}
-
-
- )}
-
{canSelfRegister && (
{t('prompt_if_admin')}{' '}
diff --git a/frontend/src/store/reducers/app.ts b/frontend/src/store/reducers/app.ts
index 343ed35941..94ccbc9687 100644
--- a/frontend/src/store/reducers/app.ts
+++ b/frontend/src/store/reducers/app.ts
@@ -57,6 +57,8 @@ const InitialValue: InitialValueTypes = {
role: null,
configs: {},
userFlags: {},
+ ee: 'Y',
+ setupCompleted: true,
};
const appReducer = (
@@ -89,6 +91,8 @@ const appReducer = (
return {
...state,
currentVersion: action.payload.currentVersion,
+ ee: action.payload.ee,
+ setupCompleted: action.payload.setupCompleted,
};
}
diff --git a/frontend/src/types/actions/app.ts b/frontend/src/types/actions/app.ts
index 9b0df64afb..9efd43c45e 100644
--- a/frontend/src/types/actions/app.ts
+++ b/frontend/src/types/actions/app.ts
@@ -46,6 +46,8 @@ export interface UpdateAppVersion {
type: typeof UPDATE_CURRENT_VERSION;
payload: {
currentVersion: AppReducer['currentVersion'];
+ ee: AppReducer['ee'];
+ setupCompleted: AppReducer['setupCompleted'];
};
}
diff --git a/frontend/src/types/api/user/getVersion.ts b/frontend/src/types/api/user/getVersion.ts
index a729875bb6..5419a038c1 100644
--- a/frontend/src/types/api/user/getVersion.ts
+++ b/frontend/src/types/api/user/getVersion.ts
@@ -1,4 +1,5 @@
export interface PayloadProps {
version: string;
- ee: string;
+ ee: 'Y' | 'N';
+ setupCompleted: boolean;
}
diff --git a/frontend/src/types/reducer/app.ts b/frontend/src/types/reducer/app.ts
index a89f6cb7ae..89c4724310 100644
--- a/frontend/src/types/reducer/app.ts
+++ b/frontend/src/types/reducer/app.ts
@@ -29,4 +29,6 @@ export default interface AppReducer {
featureFlags: null | FeatureFlagPayload;
configs: ConfigPayload;
userFlags: null | UserFlags;
+ ee: 'Y' | 'N';
+ setupCompleted: boolean;
}
diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go
index 8d49dbb0ad..130d11e368 100644
--- a/pkg/query-service/app/http_handler.go
+++ b/pkg/query-service/app/http_handler.go
@@ -61,6 +61,11 @@ type APIHandler struct {
ruleManager *rules.Manager
featureFlags interfaces.FeatureLookup
ready func(http.HandlerFunc) http.HandlerFunc
+
+ // SetupCompleted indicates if SigNoz is ready for general use.
+ // at the moment, we mark the app ready when the first user
+ // is registers.
+ SetupCompleted bool
}
type APIHandlerOpts struct {
@@ -100,6 +105,19 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
// if errReadingDashboards != nil {
// return nil, errReadingDashboards
// }
+
+ // check if at least one user is created
+ hasUsers, err := aH.appDao.GetUsersWithOpts(context.Background(), 1)
+ if err.Error() != "" {
+ // raise warning but no panic as this is a recoverable condition
+ zap.S().Warnf("unexpected error while fetch user count while initializing base api handler", err.Error())
+ }
+ if len(hasUsers) != 0 {
+ // first user is already created, we can mark the app ready for general use.
+ // this means, we disable self-registration and expect new users
+ // to signup signoz through invite link only.
+ aH.SetupCompleted = true
+ }
return aH, nil
}
@@ -1645,7 +1663,13 @@ func (aH *APIHandler) getDisks(w http.ResponseWriter, r *http.Request) {
func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) {
version := version.GetVersion()
- aH.WriteJSON(w, r, map[string]string{"version": version, "ee": "N"})
+ versionResponse := model.GetVersionResponse{
+ Version: version,
+ EE: "Y",
+ SetupCompleted: aH.SetupCompleted,
+ }
+
+ aH.WriteJSON(w, r, versionResponse)
}
func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
@@ -1777,6 +1801,12 @@ func (aH *APIHandler) registerUser(w http.ResponseWriter, r *http.Request) {
return
}
+ if !aH.SetupCompleted {
+ // since the first user is now created, we can disable self-registration as
+ // from here onwards, we expect admin (owner) to invite other users.
+ aH.SetupCompleted = true
+ }
+
aH.Respond(w, nil)
}
diff --git a/pkg/query-service/auth/auth.go b/pkg/query-service/auth/auth.go
index ef83cb7fb7..b7fc34e1ed 100644
--- a/pkg/query-service/auth/auth.go
+++ b/pkg/query-service/auth/auth.go
@@ -267,7 +267,7 @@ func RegisterFirstUser(ctx context.Context, req *RegisterRequest) (*model.User,
func RegisterInvitedUser(ctx context.Context, req *RegisterRequest, nopassword bool) (*model.User, *model.ApiError) {
if req.InviteToken == "" {
- return nil, model.BadRequest(fmt.Errorf("invite token is required"))
+ return nil, model.BadRequest(ErrorAskAdmin)
}
if !nopassword && req.Password == "" {
diff --git a/pkg/query-service/auth/utils.go b/pkg/query-service/auth/utils.go
index df96057229..d76beca5c4 100644
--- a/pkg/query-service/auth/utils.go
+++ b/pkg/query-service/auth/utils.go
@@ -15,7 +15,7 @@ var (
ErrorInvalidRole = errors.New("Invalid role")
ErrorInvalidInviteToken = errors.New("Invalid invite token")
- ErrorAskAdmin = errors.New("You are not allowed to create an account. Please ask your admin to send an invite link")
+ ErrorAskAdmin = errors.New("An invitation is needed to create an account. Please ask your admin (the person who has first installed SIgNoz) to send an invite.")
)
func randomHex(sz int) (string, error) {
diff --git a/pkg/query-service/dao/interface.go b/pkg/query-service/dao/interface.go
index 974b313bf0..ceece7faef 100644
--- a/pkg/query-service/dao/interface.go
+++ b/pkg/query-service/dao/interface.go
@@ -19,6 +19,7 @@ type Queries interface {
GetUser(ctx context.Context, id string) (*model.UserPayload, *model.ApiError)
GetUserByEmail(ctx context.Context, email string) (*model.UserPayload, *model.ApiError)
GetUsers(ctx context.Context) ([]model.UserPayload, *model.ApiError)
+ GetUsersWithOpts(ctx context.Context, limit int) ([]model.UserPayload, *model.ApiError)
GetGroup(ctx context.Context, id string) (*model.Group, *model.ApiError)
GetGroupByName(ctx context.Context, name string) (*model.Group, *model.ApiError)
diff --git a/pkg/query-service/dao/sqlite/rbac.go b/pkg/query-service/dao/sqlite/rbac.go
index 11bed020a2..64ff7ed8ae 100644
--- a/pkg/query-service/dao/sqlite/rbac.go
+++ b/pkg/query-service/dao/sqlite/rbac.go
@@ -345,7 +345,13 @@ func (mds *ModelDaoSqlite) GetUserByEmail(ctx context.Context,
return &users[0], nil
}
+// GetUsers fetches total user count
func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]model.UserPayload, *model.ApiError) {
+ return mds.GetUsersWithOpts(ctx, 0)
+}
+
+// GetUsersWithOpts fetches users and supports additional search options
+func (mds *ModelDaoSqlite) GetUsersWithOpts(ctx context.Context, limit int) ([]model.UserPayload, *model.ApiError) {
users := []model.UserPayload{}
query := `select
@@ -364,6 +370,9 @@ func (mds *ModelDaoSqlite) GetUsers(ctx context.Context) ([]model.UserPayload, *
g.id = u.group_id and
o.id = u.org_id`
+ if limit > 0 {
+ query = fmt.Sprintf("%s LIMIT %d", query, limit)
+ }
err := mds.db.Select(&users, query)
if err != nil {
diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go
index 01afb950e4..8aa815bc19 100644
--- a/pkg/query-service/model/response.go
+++ b/pkg/query-service/model/response.go
@@ -585,3 +585,9 @@ func (ci *ClusterInfo) GetMapFromStruct() map[string]interface{} {
json.Unmarshal(data, &clusterInfoMap)
return clusterInfoMap
}
+
+type GetVersionResponse struct {
+ Version string `json:"version"`
+ EE string `json:"ee"`
+ SetupCompleted bool `json:"setupCompleted"`
+}