diff --git a/ee/licensing/config.go b/ee/licensing/config.go new file mode 100644 index 0000000000..598724d8e3 --- /dev/null +++ b/ee/licensing/config.go @@ -0,0 +1,26 @@ +package licensing + +import ( + "fmt" + "sync" + "time" + + "github.com/SigNoz/signoz/pkg/licensing" +) + +var ( + config licensing.Config + once sync.Once +) + +// initializes the licensing configuration +func Config(pollInterval time.Duration, failureThreshold int) licensing.Config { + once.Do(func() { + config = licensing.Config{PollInterval: pollInterval, FailureThreshold: failureThreshold} + if err := config.Validate(); err != nil { + panic(fmt.Errorf("invalid licensing config: %w", err)) + } + }) + + return config +} diff --git a/ee/licensing/httplicensing/api.go b/ee/licensing/httplicensing/api.go new file mode 100644 index 0000000000..9f9bc1f5da --- /dev/null +++ b/ee/licensing/httplicensing/api.go @@ -0,0 +1,168 @@ +package httplicensing + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/licensing" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/licensetypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type licensingAPI struct { + licensing licensing.Licensing +} + +func NewLicensingAPI(licensing licensing.Licensing) licensing.API { + return &licensingAPI{licensing: licensing} +} + +func (api *licensingAPI) Activate(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid")) + return + } + + req := new(licensetypes.PostableLicense) + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + render.Error(rw, err) + return + } + + err = api.licensing.Activate(r.Context(), orgID, req.Key) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusAccepted, nil) +} + +func (api *licensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid")) + return + } + + license, err := api.licensing.GetActive(r.Context(), orgID) + if err != nil { + render.Error(rw, err) + return + } + + gettableLicense := licensetypes.NewGettableLicense(license.Data, license.Key) + render.Success(rw, http.StatusOK, gettableLicense) +} + +func (api *licensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid")) + return + } + + err = api.licensing.Refresh(r.Context(), orgID) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusNoContent, nil) +} + +func (api *licensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid")) + return + } + + req := new(licensetypes.PostableSubscription) + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + render.Error(rw, err) + return + } + + gettableSubscription, err := api.licensing.Checkout(ctx, orgID, req) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusCreated, gettableSubscription) +} + +func (api *licensingAPI) Portal(rw http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid")) + return + } + + req := new(licensetypes.PostableSubscription) + if err := json.NewDecoder(r.Body).Decode(req); err != nil { + render.Error(rw, err) + return + } + + gettableSubscription, err := api.licensing.Portal(ctx, orgID, req) + if err != nil { + render.Error(rw, err) + return + } + + render.Success(rw, http.StatusCreated, gettableSubscription) +} diff --git a/ee/licensing/httplicensing/provider.go b/ee/licensing/httplicensing/provider.go new file mode 100644 index 0000000000..3764edc7e9 --- /dev/null +++ b/ee/licensing/httplicensing/provider.go @@ -0,0 +1,285 @@ +package httplicensing + +import ( + "context" + "encoding/json" + "time" + + "github.com/SigNoz/signoz/ee/licensing/licensingstore/sqllicensingstore" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/licensing" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types/featuretypes" + "github.com/SigNoz/signoz/pkg/types/licensetypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/SigNoz/signoz/pkg/zeus" + "github.com/tidwall/gjson" +) + +type provider struct { + store licensetypes.Store + zeus zeus.Zeus + config licensing.Config + settings factory.ScopedProviderSettings + stopChan chan struct{} +} + +func NewProviderFactory(store sqlstore.SQLStore, zeus zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] { + return factory.NewProviderFactory(factory.MustNewName("http"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) { + return New(ctx, providerSettings, config, store, zeus) + }) +} + +func New(ctx context.Context, ps factory.ProviderSettings, config licensing.Config, sqlstore sqlstore.SQLStore, zeus zeus.Zeus) (licensing.Licensing, error) { + settings := factory.NewScopedProviderSettings(ps, "github.com/SigNoz/signoz/ee/licensing/httplicensing") + licensestore := sqllicensingstore.New(sqlstore) + return &provider{store: licensestore, zeus: zeus, config: config, settings: settings, stopChan: make(chan struct{})}, nil +} + +func (provider *provider) Start(ctx context.Context) error { + tick := time.NewTicker(provider.config.PollInterval) + defer tick.Stop() + + err := provider.Validate(ctx) + if err != nil { + provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err) + } + + for { + select { + case <-provider.stopChan: + return nil + case <-tick.C: + err := provider.Validate(ctx) + if err != nil { + provider.settings.Logger().ErrorContext(ctx, "failed to validate license from upstream server", "error", err) + } + } + } +} + +func (provider *provider) Stop(ctx context.Context) error { + provider.settings.Logger().DebugContext(ctx, "license validation stopped") + close(provider.stopChan) + return nil +} + +func (provider *provider) Validate(ctx context.Context) error { + organizations, err := provider.store.ListOrganizations(ctx) + if err != nil { + return err + } + + for _, organizationID := range organizations { + err := provider.Refresh(ctx, organizationID) + if err != nil { + return err + } + } + + if len(organizations) == 0 { + provider.settings.Logger().DebugContext(ctx, "no organizations found, defaulting to basic plan") + err = provider.InitFeatures(ctx, licensetypes.BasicPlan) + if err != nil { + return err + } + } + + return nil +} + +func (provider *provider) Activate(ctx context.Context, organizationID valuer.UUID, key string) error { + data, err := provider.zeus.GetLicense(ctx, key) + if err != nil { + return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to fetch license data with upstream server") + } + + license, err := licensetypes.NewLicense(data, organizationID) + if err != nil { + return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity") + } + + storableLicense := licensetypes.NewStorableLicenseFromLicense(license) + err = provider.store.Create(ctx, storableLicense) + if err != nil { + return err + } + + err = provider.InitFeatures(ctx, license.Features) + if err != nil { + return err + } + + return nil +} + +func (provider *provider) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) { + storableLicenses, err := provider.store.GetAll(ctx, organizationID) + if err != nil { + return nil, err + } + + activeLicense, err := licensetypes.GetActiveLicenseFromStorableLicenses(storableLicenses, organizationID) + if err != nil { + return nil, err + } + + return activeLicense, nil +} + +func (provider *provider) Refresh(ctx context.Context, organizationID valuer.UUID) error { + provider.settings.Logger().DebugContext(ctx, "license validation started for organizationID", "organizationID", organizationID.StringValue()) + activeLicense, err := provider.GetActive(ctx, organizationID) + if err != nil && !errors.Ast(err, errors.TypeNotFound) { + provider.settings.Logger().ErrorContext(ctx, "license validation failed", "organizationID", organizationID.StringValue()) + return err + } + + if err != nil && errors.Ast(err, errors.TypeNotFound) { + provider.settings.Logger().DebugContext(ctx, "no active license found, defaulting to basic plan", "organizationID", organizationID.StringValue()) + err = provider.InitFeatures(ctx, licensetypes.BasicPlan) + if err != nil { + return err + } + return nil + } + + data, err := provider.zeus.GetLicense(ctx, activeLicense.Key) + if err != nil { + provider.settings.Logger().ErrorContext(ctx, "failed to validate the license with upstream server", "licenseID", activeLicense.Key, "organizationID", organizationID.StringValue()) + + if time.Since(activeLicense.LastValidatedAt) > time.Duration(provider.config.FailureThreshold)*provider.config.PollInterval { + provider.settings.Logger().ErrorContext(ctx, "license validation failed for consecutive poll intervals. defaulting to basic plan", "failureThreshold", provider.config.FailureThreshold, "licenseID", activeLicense.ID.StringValue(), "organizationID", organizationID.StringValue()) + err = provider.InitFeatures(ctx, licensetypes.BasicPlan) + if err != nil { + return err + } + return nil + } + return err + } + + err = activeLicense.Update(data) + if err != nil { + return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to create license entity from license data") + } + + provider.settings.Logger().DebugContext(ctx, "license validation completed successfully", "licenseID", activeLicense.ID, "organizationID", organizationID.StringValue()) + updatedStorableLicense := licensetypes.NewStorableLicenseFromLicense(activeLicense) + err = provider.store.Update(ctx, organizationID, updatedStorableLicense) + if err != nil { + return err + } + + return nil +} + +func (provider *provider) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) { + activeLicense, err := provider.GetActive(ctx, organizationID) + if err != nil { + return nil, err + } + + body, err := json.Marshal(postableSubscription) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal checkout payload") + } + + response, err := provider.zeus.GetCheckoutURL(ctx, activeLicense.Key, body) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate checkout session") + } + + return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil +} + +func (provider *provider) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) { + activeLicense, err := provider.GetActive(ctx, organizationID) + if err != nil { + return nil, err + } + + body, err := json.Marshal(postableSubscription) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal portal payload") + } + + response, err := provider.zeus.GetPortalURL(ctx, activeLicense.Key, body) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to generate portal session") + } + + return &licensetypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil +} + +// feature surrogate +func (provider *provider) CheckFeature(ctx context.Context, key string) error { + feature, err := provider.store.GetFeature(ctx, key) + if err != nil { + return err + } + if feature.Active { + return nil + } + return errors.Newf(errors.TypeUnsupported, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key) +} + +func (provider *provider) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) { + featureStatus, err := provider.store.GetFeature(ctx, key) + if err != nil { + return nil, err + } + return &featuretypes.GettableFeature{ + Name: featureStatus.Name, + Active: featureStatus.Active, + Usage: int64(featureStatus.Usage), + UsageLimit: int64(featureStatus.UsageLimit), + Route: featureStatus.Route, + }, nil +} + +func (provider *provider) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) { + storableFeatures, err := provider.store.GetAllFeatures(ctx) + if err != nil { + return nil, err + } + + gettableFeatures := make([]*featuretypes.GettableFeature, len(storableFeatures)) + for idx, gettableFeature := range storableFeatures { + gettableFeatures[idx] = &featuretypes.GettableFeature{ + Name: gettableFeature.Name, + Active: gettableFeature.Active, + Usage: int64(gettableFeature.Usage), + UsageLimit: int64(gettableFeature.UsageLimit), + Route: gettableFeature.Route, + } + } + + return gettableFeatures, nil +} + +func (provider *provider) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error { + featureStatus := make([]*featuretypes.StorableFeature, len(features)) + for i, f := range features { + featureStatus[i] = &featuretypes.StorableFeature{ + Name: f.Name, + Active: f.Active, + Usage: int(f.Usage), + UsageLimit: int(f.UsageLimit), + Route: f.Route, + } + } + + return provider.store.InitFeatures(ctx, featureStatus) +} + +func (provider *provider) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error { + return provider.store.UpdateFeature(ctx, &featuretypes.StorableFeature{ + Name: feature.Name, + Active: feature.Active, + Usage: int(feature.Usage), + UsageLimit: int(feature.UsageLimit), + Route: feature.Route, + }) +} diff --git a/ee/licensing/licensingstore/sqllicensingstore/store.go b/ee/licensing/licensingstore/sqllicensingstore/store.go new file mode 100644 index 0000000000..5167a5fc00 --- /dev/null +++ b/ee/licensing/licensingstore/sqllicensingstore/store.go @@ -0,0 +1,186 @@ +package sqllicensingstore + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/featuretypes" + "github.com/SigNoz/signoz/pkg/types/licensetypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type store struct { + sqlstore sqlstore.SQLStore +} + +func New(sqlstore sqlstore.SQLStore) licensetypes.Store { + return &store{sqlstore} +} + +func (store *store) Create(ctx context.Context, storableLicense *licensetypes.StorableLicense) error { + _, err := store. + sqlstore. + BunDB(). + NewInsert(). + Model(storableLicense). + Exec(ctx) + if err != nil { + return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "license with ID: %s already exists", storableLicense.ID) + } + + return nil +} + +func (store *store) Get(ctx context.Context, organizationID valuer.UUID, licenseID valuer.UUID) (*licensetypes.StorableLicense, error) { + storableLicense := new(licensetypes.StorableLicense) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(storableLicense). + Where("org_id = ?", organizationID). + Where("id = ?", licenseID). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "license with ID: %s does not exist", licenseID) + } + + return storableLicense, nil +} + +func (store *store) GetAll(ctx context.Context, organizationID valuer.UUID) ([]*licensetypes.StorableLicense, error) { + storableLicenses := make([]*licensetypes.StorableLicense, 0) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(&storableLicenses). + Where("org_id = ?", organizationID). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "licenses for organizationID: %s does not exists", organizationID) + } + + return storableLicenses, nil +} + +func (store *store) Update(ctx context.Context, organizationID valuer.UUID, storableLicense *licensetypes.StorableLicense) error { + _, err := store. + sqlstore. + BunDB(). + NewUpdate(). + Model(storableLicense). + WherePK(). + Where("org_id = ?", organizationID). + Exec(ctx) + if err != nil { + return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update license with ID: %s", storableLicense.ID) + } + + return nil +} + +func (store *store) ListOrganizations(ctx context.Context) ([]valuer.UUID, error) { + orgIDStrs := make([]string, 0) + err := store.sqlstore. + BunDB(). + NewSelect(). + Model(new(types.Organization)). + Column("id"). + Scan(ctx, &orgIDStrs) + if err != nil { + return nil, err + } + + orgIDs := make([]valuer.UUID, len(orgIDStrs)) + for idx, orgIDStr := range orgIDStrs { + orgID, err := valuer.NewUUID(orgIDStr) + if err != nil { + return nil, err + } + orgIDs[idx] = orgID + } + + return orgIDs, nil + +} + +func (store *store) CreateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error { + _, err := store. + sqlstore. + BunDB(). + NewInsert(). + Model(storableFeature). + Exec(ctx) + if err != nil { + return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "feature with name:%s already exists", storableFeature.Name) + } + + return nil +} + +func (store *store) GetFeature(ctx context.Context, key string) (*featuretypes.StorableFeature, error) { + storableFeature := new(featuretypes.StorableFeature) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(storableFeature). + Where("name = ?", key). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "feature with name:%s does not exist", key) + } + + return storableFeature, nil +} + +func (store *store) GetAllFeatures(ctx context.Context) ([]*featuretypes.StorableFeature, error) { + storableFeatures := make([]*featuretypes.StorableFeature, 0) + err := store. + sqlstore. + BunDB(). + NewSelect(). + Model(&storableFeatures). + Scan(ctx) + if err != nil { + return nil, store.sqlstore.WrapNotFoundErrf(err, errors.CodeNotFound, "features do not exist") + } + + return storableFeatures, nil +} + +func (store *store) InitFeatures(ctx context.Context, storableFeatures []*featuretypes.StorableFeature) error { + _, err := store. + sqlstore. + BunDB(). + NewInsert(). + Model(&storableFeatures). + On("CONFLICT (name) DO UPDATE"). + Set("active = EXCLUDED.active"). + Set("usage = EXCLUDED.usage"). + Set("usage_limit = EXCLUDED.usage_limit"). + Set("route = EXCLUDED.route"). + Exec(ctx) + if err != nil { + return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to initialise features") + } + + return nil +} + +func (store *store) UpdateFeature(ctx context.Context, storableFeature *featuretypes.StorableFeature) error { + _, err := store. + sqlstore. + BunDB(). + NewUpdate(). + Model(storableFeature). + Exec(ctx) + if err != nil { + return errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "unable to update feature with key: %s", storableFeature.Name) + } + + return nil +} diff --git a/ee/query-service/app/api/api.go b/ee/query-service/app/api/api.go index 812b83b73d..a433502c2f 100644 --- a/ee/query-service/app/api/api.go +++ b/ee/query-service/app/api/api.go @@ -6,29 +6,29 @@ import ( "net/http/httputil" "time" + "github.com/SigNoz/signoz/ee/licensing/httplicensing" "github.com/SigNoz/signoz/ee/query-service/dao" "github.com/SigNoz/signoz/ee/query-service/integrations/gateway" "github.com/SigNoz/signoz/ee/query-service/interfaces" - "github.com/SigNoz/signoz/ee/query-service/license" - "github.com/SigNoz/signoz/ee/query-service/model" "github.com/SigNoz/signoz/ee/query-service/usage" "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/apis/fields" "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/middleware" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/quickfilter" quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core" baseapp "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations" "github.com/SigNoz/signoz/pkg/query-service/app/integrations" "github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline" - baseint "github.com/SigNoz/signoz/pkg/query-service/interfaces" basemodel "github.com/SigNoz/signoz/pkg/query-service/model" rules "github.com/SigNoz/signoz/pkg/query-service/rules" "github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/licensetypes" "github.com/SigNoz/signoz/pkg/version" "github.com/gorilla/mux" "go.uber.org/zap" @@ -40,8 +40,6 @@ type APIHandlerOptions struct { AppDao dao.ModelDao RulesManager *rules.Manager UsageManager *usage.Manager - FeatureFlags baseint.FeatureLookup - LicenseManager *license.Manager IntegrationsController *integrations.Controller CloudIntegrationsController *cloudintegrations.Controller LogsParsingPipelineController *logparsingpipeline.LogParsingPipelineController @@ -67,12 +65,12 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, Reader: opts.DataConnector, PreferSpanMetrics: opts.PreferSpanMetrics, RuleManager: opts.RulesManager, - FeatureFlags: opts.FeatureFlags, IntegrationsController: opts.IntegrationsController, CloudIntegrationsController: opts.CloudIntegrationsController, LogsParsingPipelineController: opts.LogsParsingPipelineController, FluxInterval: opts.FluxInterval, AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager), + LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing), FieldsAPI: fields.NewAPI(signoz.TelemetryStore), Signoz: signoz, QuickFilters: quickFilter, @@ -90,18 +88,10 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler, return ah, nil } -func (ah *APIHandler) FF() baseint.FeatureLookup { - return ah.opts.FeatureFlags -} - func (ah *APIHandler) RM() *rules.Manager { return ah.opts.RulesManager } -func (ah *APIHandler) LM() *license.Manager { - return ah.opts.LicenseManager -} - func (ah *APIHandler) UM() *usage.Manager { return ah.opts.UsageManager } @@ -114,8 +104,8 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy { return ah.opts.Gateway } -func (ah *APIHandler) CheckFeature(f string) bool { - err := ah.FF().CheckFeature(f) +func (ah *APIHandler) CheckFeature(ctx context.Context, key string) bool { + err := ah.Signoz.Licensing.CheckFeature(ctx, key) return err == nil } @@ -151,18 +141,17 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.UpdateAPIKey)).Methods(http.MethodPut) router.HandleFunc("/api/v1/pats/{id}", am.AdminAccess(ah.Signoz.Handlers.User.RevokeAPIKey)).Methods(http.MethodDelete) - router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.checkout)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/checkout", am.AdminAccess(ah.LicensingAPI.Checkout)).Methods(http.MethodPost) router.HandleFunc("/api/v1/billing", am.AdminAccess(ah.getBilling)).Methods(http.MethodGet) - router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.portalSession)).Methods(http.MethodPost) + router.HandleFunc("/api/v1/portal", am.AdminAccess(ah.LicensingAPI.Portal)).Methods(http.MethodPost) router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut) router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut) // v3 - router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet) - router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost) - router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.refreshLicensesV3)).Methods(http.MethodPut) - router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.getActiveLicenseV3)).Methods(http.MethodGet) + router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Activate)).Methods(http.MethodPost) + router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.LicensingAPI.Refresh)).Methods(http.MethodPut) + router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.LicensingAPI.GetActive)).Methods(http.MethodGet) // v4 router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost) @@ -175,18 +164,14 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { } // TODO(nitya): remove this once we know how to get the FF's -func (ah *APIHandler) updateRequestContext(w http.ResponseWriter, r *http.Request) (*http.Request, error) { +func (ah *APIHandler) updateRequestContext(_ http.ResponseWriter, r *http.Request) (*http.Request, error) { ssoAvailable := true - err := ah.FF().CheckFeature(model.SSO) - if err != nil { - switch err.(type) { - case basemodel.ErrFeatureUnavailable: - // do nothing, just skip sso - ssoAvailable = false - default: - zap.L().Error("feature check failed", zap.String("featureKey", model.SSO), zap.Error(err)) - return r, errors.New(errors.TypeInternal, errors.CodeInternal, "error checking SSO feature") - } + err := ah.Signoz.Licensing.CheckFeature(r.Context(), licensetypes.SSO) + if err != nil && errors.Asc(err, licensing.ErrCodeFeatureUnavailable) { + ssoAvailable = false + } else if err != nil { + zap.L().Error("feature check failed", zap.String("featureKey", licensetypes.SSO), zap.Error(err)) + return r, errors.New(errors.TypeInternal, errors.CodeInternal, "error checking SSO feature") } ctx := context.WithValue(r.Context(), types.SSOAvailable, ssoAvailable) return r.WithContext(ctx), nil @@ -199,7 +184,6 @@ func (ah *APIHandler) loginPrecheck(w http.ResponseWriter, r *http.Request) { return } ah.Signoz.Handlers.User.LoginPrecheck(w, r) - return } func (ah *APIHandler) acceptInvite(w http.ResponseWriter, r *http.Request) { @@ -209,7 +193,6 @@ func (ah *APIHandler) acceptInvite(w http.ResponseWriter, r *http.Request) { return } ah.Signoz.Handlers.User.AcceptInvite(w, r) - return } func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) { @@ -219,7 +202,7 @@ func (ah *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) { return } ah.Signoz.Handlers.User.GetInvite(w, r) - return + } func (ah *APIHandler) RegisterCloudIntegrationsRoutes(router *mux.Router, am *middleware.AuthZ) { diff --git a/ee/query-service/app/api/auth.go b/ee/query-service/app/api/auth.go index 1d90df80d5..2843473945 100644 --- a/ee/query-service/app/api/auth.go +++ b/ee/query-service/app/api/auth.go @@ -12,8 +12,8 @@ import ( "go.uber.org/zap" "github.com/SigNoz/signoz/ee/query-service/constants" - "github.com/SigNoz/signoz/ee/query-service/model" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/types/licensetypes" ) func parseRequest(r *http.Request, req interface{}) error { @@ -35,7 +35,6 @@ func (ah *APIHandler) loginUser(w http.ResponseWriter, r *http.Request) { return } ah.Signoz.Handlers.User.Login(w, r) - return } func handleSsoError(w http.ResponseWriter, r *http.Request, redirectURL string) { @@ -52,7 +51,7 @@ func (ah *APIHandler) receiveGoogleAuth(w http.ResponseWriter, r *http.Request) redirectUri := constants.GetDefaultSiteURL() ctx := context.Background() - if !ah.CheckFeature(model.SSO) { + if !ah.CheckFeature(r.Context(), licensetypes.SSO) { zap.L().Error("[receiveGoogleAuth] 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 @@ -118,7 +117,7 @@ func (ah *APIHandler) receiveSAML(w http.ResponseWriter, r *http.Request) { redirectUri := constants.GetDefaultSiteURL() ctx := context.Background() - if !ah.CheckFeature(model.SSO) { + if !ah.CheckFeature(r.Context(), licensetypes.SSO) { 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 diff --git a/ee/query-service/app/api/cloudIntegrations.go b/ee/query-service/app/api/cloudIntegrations.go index 1251a52403..f73e488f28 100644 --- a/ee/query-service/app/api/cloudIntegrations.go +++ b/ee/query-service/app/api/cloudIntegrations.go @@ -36,6 +36,12 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW return } + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid")) + return + } + cloudProvider := mux.Vars(r)["cloudProvider"] if cloudProvider != "aws" { RespondError(w, basemodel.BadRequest(fmt.Errorf( @@ -56,11 +62,9 @@ func (ah *APIHandler) CloudIntegrationsGenerateConnectionParams(w http.ResponseW SigNozAPIKey: apiKey, } - license, apiErr := ah.LM().GetRepo().GetActiveLicense(r.Context()) - if apiErr != nil { - RespondError(w, basemodel.WrapApiError( - apiErr, "couldn't look for active license", - ), nil) + license, err := ah.Signoz.Licensing.GetActive(r.Context(), orgID) + if err != nil { + render.Error(w, err) return } diff --git a/ee/query-service/app/api/featureFlags.go b/ee/query-service/app/api/featureFlags.go index 1feca4b764..13572c2f5f 100644 --- a/ee/query-service/app/api/featureFlags.go +++ b/ee/query-service/app/api/featureFlags.go @@ -9,13 +9,29 @@ import ( "time" "github.com/SigNoz/signoz/ee/query-service/constants" - basemodel "github.com/SigNoz/signoz/pkg/query-service/model" + pkgError "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/featuretypes" + "github.com/SigNoz/signoz/pkg/valuer" "go.uber.org/zap" ) func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - featureSet, err := ah.FF().GetFeatureFlags() + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(w, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(w, pkgError.Newf(pkgError.TypeInvalidInput, pkgError.CodeInvalidInput, "orgId is invalid")) + return + } + + featureSet, err := ah.Signoz.Licensing.GetFeatureFlags(r.Context()) if err != nil { ah.HandleError(w, err, http.StatusInternalServerError) return @@ -23,7 +39,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { if constants.FetchFeatures == "true" { zap.L().Debug("fetching license") - license, err := ah.LM().GetRepo().GetActiveLicense(ctx) + license, err := ah.Signoz.Licensing.GetActive(ctx, orgID) if err != nil { zap.L().Error("failed to fetch license", zap.Error(err)) } else if license == nil { @@ -44,9 +60,8 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { } if ah.opts.PreferSpanMetrics { - for idx := range featureSet { - feature := &featureSet[idx] - if feature.Name == basemodel.UseSpanMetrics { + for idx, feature := range featureSet { + if feature.Name == featuretypes.UseSpanMetrics { featureSet[idx].Active = true } } @@ -57,7 +72,7 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { // fetchZeusFeatures makes an HTTP GET request to the /zeusFeatures endpoint // and returns the FeatureSet. -func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) { +func fetchZeusFeatures(url, licenseKey string) ([]*featuretypes.GettableFeature, error) { // Check if the URL is empty if url == "" { return nil, fmt.Errorf("url is empty") @@ -116,14 +131,14 @@ func fetchZeusFeatures(url, licenseKey string) (basemodel.FeatureSet, error) { } type ZeusFeaturesResponse struct { - Status string `json:"status"` - Data basemodel.FeatureSet `json:"data"` + Status string `json:"status"` + Data []*featuretypes.GettableFeature `json:"data"` } // MergeFeatureSets merges two FeatureSet arrays with precedence to zeusFeatures. -func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basemodel.FeatureSet { +func MergeFeatureSets(zeusFeatures, internalFeatures []*featuretypes.GettableFeature) []*featuretypes.GettableFeature { // Create a map to store the merged features - featureMap := make(map[string]basemodel.Feature) + featureMap := make(map[string]*featuretypes.GettableFeature) // Add all features from the otherFeatures set to the map for _, feature := range internalFeatures { @@ -137,7 +152,7 @@ func MergeFeatureSets(zeusFeatures, internalFeatures basemodel.FeatureSet) basem } // Convert the map back to a FeatureSet slice - var mergedFeatures basemodel.FeatureSet + var mergedFeatures []*featuretypes.GettableFeature for _, feature := range featureMap { mergedFeatures = append(mergedFeatures, feature) } diff --git a/ee/query-service/app/api/featureFlags_test.go b/ee/query-service/app/api/featureFlags_test.go index e64e2ea135..79032a43a5 100644 --- a/ee/query-service/app/api/featureFlags_test.go +++ b/ee/query-service/app/api/featureFlags_test.go @@ -3,58 +3,58 @@ package api import ( "testing" - basemodel "github.com/SigNoz/signoz/pkg/query-service/model" + "github.com/SigNoz/signoz/pkg/types/featuretypes" "github.com/stretchr/testify/assert" ) func TestMergeFeatureSets(t *testing.T) { tests := []struct { name string - zeusFeatures basemodel.FeatureSet - internalFeatures basemodel.FeatureSet - expected basemodel.FeatureSet + zeusFeatures []*featuretypes.GettableFeature + internalFeatures []*featuretypes.GettableFeature + expected []*featuretypes.GettableFeature }{ { name: "empty zeusFeatures and internalFeatures", - zeusFeatures: basemodel.FeatureSet{}, - internalFeatures: basemodel.FeatureSet{}, - expected: basemodel.FeatureSet{}, + zeusFeatures: []*featuretypes.GettableFeature{}, + internalFeatures: []*featuretypes.GettableFeature{}, + expected: []*featuretypes.GettableFeature{}, }, { name: "non-empty zeusFeatures and empty internalFeatures", - zeusFeatures: basemodel.FeatureSet{ + zeusFeatures: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature2", Active: false}, }, - internalFeatures: basemodel.FeatureSet{}, - expected: basemodel.FeatureSet{ + internalFeatures: []*featuretypes.GettableFeature{}, + expected: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature2", Active: false}, }, }, { name: "empty zeusFeatures and non-empty internalFeatures", - zeusFeatures: basemodel.FeatureSet{}, - internalFeatures: basemodel.FeatureSet{ + zeusFeatures: []*featuretypes.GettableFeature{}, + internalFeatures: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature2", Active: false}, }, - expected: basemodel.FeatureSet{ + expected: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature2", Active: false}, }, }, { name: "non-empty zeusFeatures and non-empty internalFeatures with no conflicts", - zeusFeatures: basemodel.FeatureSet{ + zeusFeatures: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature3", Active: false}, }, - internalFeatures: basemodel.FeatureSet{ + internalFeatures: []*featuretypes.GettableFeature{ {Name: "Feature2", Active: true}, {Name: "Feature4", Active: false}, }, - expected: basemodel.FeatureSet{ + expected: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature2", Active: true}, {Name: "Feature3", Active: false}, @@ -63,15 +63,15 @@ func TestMergeFeatureSets(t *testing.T) { }, { name: "non-empty zeusFeatures and non-empty internalFeatures with conflicts", - zeusFeatures: basemodel.FeatureSet{ + zeusFeatures: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature2", Active: false}, }, - internalFeatures: basemodel.FeatureSet{ + internalFeatures: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: false}, {Name: "Feature3", Active: true}, }, - expected: basemodel.FeatureSet{ + expected: []*featuretypes.GettableFeature{ {Name: "Feature1", Active: true}, {Name: "Feature2", Active: false}, {Name: "Feature3", Active: true}, diff --git a/ee/query-service/app/api/gateway.go b/ee/query-service/app/api/gateway.go index 54fc1759ed..fa1d52153f 100644 --- a/ee/query-service/app/api/gateway.go +++ b/ee/query-service/app/api/gateway.go @@ -5,10 +5,26 @@ import ( "strings" "github.com/SigNoz/signoz/ee/query-service/integrations/gateway" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/valuer" ) func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() + claims, err := authtypes.ClaimsFromContext(ctx) + if err != nil { + render.Error(rw, err) + return + } + + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(rw, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is invalid")) + return + } + validPath := false for _, allowedPrefix := range gateway.AllowedPrefix { if strings.HasPrefix(req.URL.Path, gateway.RoutePrefix+allowedPrefix) { @@ -22,9 +38,9 @@ func (ah *APIHandler) ServeGatewayHTTP(rw http.ResponseWriter, req *http.Request return } - license, err := ah.LM().GetRepo().GetActiveLicense(ctx) + license, err := ah.Signoz.Licensing.GetActive(ctx, orgID) if err != nil { - RespondError(rw, err, nil) + render.Error(rw, err) return } diff --git a/ee/query-service/app/api/license.go b/ee/query-service/app/api/license.go index 8fbbb0cccc..73b4ae56ac 100644 --- a/ee/query-service/app/api/license.go +++ b/ee/query-service/app/api/license.go @@ -6,11 +6,7 @@ import ( "net/http" "github.com/SigNoz/signoz/ee/query-service/constants" - "github.com/SigNoz/signoz/ee/query-service/integrations/signozio" "github.com/SigNoz/signoz/ee/query-service/model" - "github.com/SigNoz/signoz/pkg/http/render" - "github.com/SigNoz/signoz/pkg/query-service/telemetry" - "github.com/SigNoz/signoz/pkg/types/authtypes" ) type DayWiseBreakdown struct { @@ -49,10 +45,6 @@ type details struct { BillTotal float64 `json:"billTotal"` } -type Redirect struct { - RedirectURL string `json:"redirectURL"` -} - type billingDetails struct { Status string `json:"status"` Data struct { @@ -64,97 +56,6 @@ type billingDetails struct { } `json:"data"` } -type ApplyLicenseRequest struct { - LicenseKey string `json:"key"` -} - -func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) { - ah.listLicensesV2(w, r) -} - -func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) { - activeLicense, err := ah.LM().GetRepo().GetActiveLicenseV3(r.Context()) - if err != nil { - RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil) - return - } - - // return 404 not found if there is no active license - if activeLicense == nil { - RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil) - return - } - - // TODO deprecate this when we move away from key for stripe - activeLicense.Data["key"] = activeLicense.Key - render.Success(w, http.StatusOK, activeLicense.Data) -} - -// this function is called by zeus when inserting licenses in the query-service -func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) { - claims, err := authtypes.ClaimsFromContext(r.Context()) - if err != nil { - render.Error(w, err) - return - } - - var licenseKey ApplyLicenseRequest - if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil { - RespondError(w, model.BadRequest(err), nil) - return - } - - if licenseKey.LicenseKey == "" { - RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil) - return - } - - _, err = ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey) - if err != nil { - telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED, map[string]interface{}{"err": err.Error()}, claims.Email, true, false) - render.Error(w, err) - return - } - - render.Success(w, http.StatusAccepted, nil) -} - -func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) { - err := ah.LM().RefreshLicense(r.Context()) - if err != nil { - render.Error(w, err) - return - } - - render.Success(w, http.StatusNoContent, nil) -} - -func getCheckoutPortalResponse(redirectURL string) *Redirect { - return &Redirect{RedirectURL: redirectURL} -} - -func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) { - checkoutRequest := &model.CheckoutRequest{} - if err := json.NewDecoder(r.Body).Decode(checkoutRequest); err != nil { - RespondError(w, model.BadRequest(err), nil) - return - } - - license := ah.LM().GetActiveLicense() - if license == nil { - RespondError(w, model.BadRequestStr("cannot proceed with checkout without license key"), nil) - return - } - - redirectUrl, err := signozio.CheckoutSession(r.Context(), checkoutRequest, license.Key, ah.Signoz.Zeus) - if err != nil { - render.Error(w, err) - return - } - - ah.Respond(w, getCheckoutPortalResponse(redirectUrl)) -} - func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) { licenseKey := r.URL.Query().Get("licenseKey") @@ -188,71 +89,3 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) { // TODO(srikanthccv):Fetch the current day usage and add it to the response ah.Respond(w, billingResponse.Data) } - -func convertLicenseV3ToLicenseV2(licenses []*model.LicenseV3) []model.License { - licensesV2 := []model.License{} - for _, l := range licenses { - planKeyFromPlanName, ok := model.MapOldPlanKeyToNewPlanName[l.PlanName] - if !ok { - planKeyFromPlanName = model.Basic - } - licenseV2 := model.License{ - Key: l.Key, - ActivationId: "", - PlanDetails: "", - FeatureSet: l.Features, - ValidationMessage: "", - IsCurrent: l.IsCurrent, - LicensePlan: model.LicensePlan{ - PlanKey: planKeyFromPlanName, - ValidFrom: l.ValidFrom, - ValidUntil: l.ValidUntil, - Status: l.Status}, - } - licensesV2 = append(licensesV2, licenseV2) - } - return licensesV2 -} - -func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) { - licensesV3, apierr := ah.LM().GetLicensesV3(r.Context()) - if apierr != nil { - RespondError(w, apierr, nil) - return - } - licenses := convertLicenseV3ToLicenseV2(licensesV3) - - resp := model.Licenses{ - TrialStart: -1, - TrialEnd: -1, - OnTrial: false, - WorkSpaceBlock: false, - TrialConvertedToSubscription: false, - GracePeriodEnd: -1, - Licenses: licenses, - } - - ah.Respond(w, resp) -} - -func (ah *APIHandler) portalSession(w http.ResponseWriter, r *http.Request) { - portalRequest := &model.PortalRequest{} - if err := json.NewDecoder(r.Body).Decode(portalRequest); err != nil { - RespondError(w, model.BadRequest(err), nil) - return - } - - license := ah.LM().GetActiveLicense() - if license == nil { - RespondError(w, model.BadRequestStr("cannot request the portal session without license key"), nil) - return - } - - redirectUrl, err := signozio.PortalSession(r.Context(), portalRequest, license.Key, ah.Signoz.Zeus) - if err != nil { - render.Error(w, err) - return - } - - ah.Respond(w, getCheckoutPortalResponse(redirectUrl)) -} diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index 3c249cb938..9411c05e90 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -18,6 +18,7 @@ import ( "github.com/SigNoz/signoz/ee/query-service/dao/sqlite" "github.com/SigNoz/signoz/ee/query-service/integrations/gateway" "github.com/SigNoz/signoz/ee/query-service/rules" + "github.com/SigNoz/signoz/ee/query-service/usage" "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/http/middleware" @@ -30,9 +31,6 @@ import ( "github.com/rs/cors" "github.com/soheilhy/cmux" - licensepkg "github.com/SigNoz/signoz/ee/query-service/license" - "github.com/SigNoz/signoz/ee/query-service/usage" - "github.com/SigNoz/signoz/pkg/query-service/agentConf" baseapp "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations" @@ -96,12 +94,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } - // initiate license manager - lm, err := licensepkg.StartManager(serverOptions.SigNoz.SQLStore.SQLxDB(), serverOptions.SigNoz.SQLStore, serverOptions.SigNoz.Zeus) - if err != nil { - return nil, err - } - fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail) if err != nil { return nil, err @@ -168,11 +160,11 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { } // start the usagemanager - usageManager, err := usage.New(modelDao, lm.GetRepo(), serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus) + usageManager, err := usage.New(modelDao, serverOptions.SigNoz.Licensing, serverOptions.SigNoz.TelemetryStore.ClickhouseDB(), serverOptions.SigNoz.Zeus, serverOptions.SigNoz.Modules.Organization) if err != nil { return nil, err } - err = usageManager.Start() + err = usageManager.Start(context.Background()) if err != nil { return nil, err } @@ -197,8 +189,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { AppDao: modelDao, RulesManager: rm, UsageManager: usageManager, - FeatureFlags: lm, - LicenseManager: lm, IntegrationsController: integrationsController, CloudIntegrationsController: cloudIntegrationsController, LogsParsingPipelineController: logParsingPipelineController, @@ -431,15 +421,15 @@ func (s *Server) Start(ctx context.Context) error { return nil } -func (s *Server) Stop() error { +func (s *Server) Stop(ctx context.Context) error { if s.httpServer != nil { - if err := s.httpServer.Shutdown(context.Background()); err != nil { + if err := s.httpServer.Shutdown(ctx); err != nil { return err } } if s.privateHTTP != nil { - if err := s.privateHTTP.Shutdown(context.Background()); err != nil { + if err := s.privateHTTP.Shutdown(ctx); err != nil { return err } } @@ -447,11 +437,11 @@ func (s *Server) Stop() error { s.opampServer.Stop() if s.ruleManager != nil { - s.ruleManager.Stop(context.Background()) + s.ruleManager.Stop(ctx) } // stop usage manager - s.usageManager.Stop() + s.usageManager.Stop(ctx) return nil } diff --git a/ee/query-service/integrations/signozio/signozio.go b/ee/query-service/integrations/signozio/signozio.go deleted file mode 100644 index d1bd765728..0000000000 --- a/ee/query-service/integrations/signozio/signozio.go +++ /dev/null @@ -1,67 +0,0 @@ -package signozio - -import ( - "context" - "encoding/json" - - "github.com/SigNoz/signoz/ee/query-service/model" - "github.com/SigNoz/signoz/pkg/zeus" - "github.com/tidwall/gjson" -) - -func ValidateLicenseV3(ctx context.Context, licenseKey string, zeus zeus.Zeus) (*model.LicenseV3, error) { - data, err := zeus.GetLicense(ctx, licenseKey) - if err != nil { - return nil, err - } - - var m map[string]any - if err = json.Unmarshal(data, &m); err != nil { - return nil, err - } - - license, err := model.NewLicenseV3(m) - if err != nil { - return nil, err - } - - return license, nil -} - -// SendUsage reports the usage of signoz to license server -func SendUsage(ctx context.Context, usage model.UsagePayload, zeus zeus.Zeus) error { - body, err := json.Marshal(usage) - if err != nil { - return err - } - - return zeus.PutMeters(ctx, usage.LicenseKey.String(), body) -} - -func CheckoutSession(ctx context.Context, checkoutRequest *model.CheckoutRequest, licenseKey string, zeus zeus.Zeus) (string, error) { - body, err := json.Marshal(checkoutRequest) - if err != nil { - return "", err - } - - response, err := zeus.GetCheckoutURL(ctx, licenseKey, body) - if err != nil { - return "", err - } - - return gjson.GetBytes(response, "url").String(), nil -} - -func PortalSession(ctx context.Context, portalRequest *model.PortalRequest, licenseKey string, zeus zeus.Zeus) (string, error) { - body, err := json.Marshal(portalRequest) - if err != nil { - return "", err - } - - response, err := zeus.GetPortalURL(ctx, licenseKey, body) - if err != nil { - return "", err - } - - return gjson.GetBytes(response, "url").String(), nil -} diff --git a/ee/query-service/license/db.go b/ee/query-service/license/db.go deleted file mode 100644 index c241ad8766..0000000000 --- a/ee/query-service/license/db.go +++ /dev/null @@ -1,248 +0,0 @@ -package license - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "time" - - "github.com/jmoiron/sqlx" - "github.com/mattn/go-sqlite3" - - "github.com/SigNoz/signoz/ee/query-service/model" - basemodel "github.com/SigNoz/signoz/pkg/query-service/model" - "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/SigNoz/signoz/pkg/types" - "go.uber.org/zap" -) - -// Repo is license repo. stores license keys in a secured DB -type Repo struct { - db *sqlx.DB - store sqlstore.SQLStore -} - -// NewLicenseRepo initiates a new license repo -func NewLicenseRepo(db *sqlx.DB, store sqlstore.SQLStore) Repo { - return Repo{ - db: db, - store: store, - } -} - -func (r *Repo) GetLicensesV3(ctx context.Context) ([]*model.LicenseV3, error) { - licensesData := []model.LicenseDB{} - licenseV3Data := []*model.LicenseV3{} - - query := "SELECT id,key,data FROM licenses_v3" - - err := r.db.Select(&licensesData, query) - if err != nil { - return nil, fmt.Errorf("failed to get licenses from db: %v", err) - } - - for _, l := range licensesData { - var licenseData map[string]interface{} - err := json.Unmarshal([]byte(l.Data), &licenseData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err) - } - - license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData) - if err != nil { - return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err) - } - licenseV3Data = append(licenseV3Data, license) - } - - return licenseV3Data, nil -} - -// GetActiveLicense fetches the latest active license from DB. -// If the license is not present, expect a nil license and a nil error in the output. -func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) { - activeLicenseV3, err := r.GetActiveLicenseV3(ctx) - if err != nil { - return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err)) - } - - if activeLicenseV3 == nil { - return nil, nil - } - activeLicenseV2 := model.ConvertLicenseV3ToLicenseV2(activeLicenseV3) - return activeLicenseV2, nil -} - -func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) { - var err error - licenses := []model.LicenseDB{} - - query := "SELECT id,key,data FROM licenses_v3" - - err = r.db.Select(&licenses, query) - if err != nil { - return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err)) - } - - var active *model.LicenseV3 - for _, l := range licenses { - var licenseData map[string]interface{} - err := json.Unmarshal([]byte(l.Data), &licenseData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err) - } - - license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData) - if err != nil { - return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err) - } - - if active == nil && - (license.ValidFrom != 0) && - (license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) { - active = license - } - if active != nil && - license.ValidFrom > active.ValidFrom && - (license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) { - active = license - } - } - - return active, nil -} - -// InsertLicenseV3 inserts a new license v3 in db -func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) *model.ApiError { - - query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)` - - // licsense is the entity of zeus so putting the entire license here without defining schema - licenseData, err := json.Marshal(l.Data) - if err != nil { - return &model.ApiError{Typ: basemodel.ErrorBadData, Err: err} - } - - _, err = r.db.ExecContext(ctx, - query, - l.ID, - l.Key, - string(licenseData), - ) - - if err != nil { - if sqliteErr, ok := err.(sqlite3.Error); ok { - if sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique { - zap.L().Error("error in inserting license data: ", zap.Error(sqliteErr)) - return &model.ApiError{Typ: model.ErrorConflict, Err: sqliteErr} - } - } - zap.L().Error("error in inserting license data: ", zap.Error(err)) - return &model.ApiError{Typ: basemodel.ErrorExec, Err: err} - } - - return nil -} - -// UpdateLicenseV3 updates a new license v3 in db -func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error { - - // the key and id for the license can't change so only update the data here! - query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;` - - license, err := json.Marshal(l.Data) - if err != nil { - return fmt.Errorf("insert license failed: license marshal error") - } - _, err = r.db.ExecContext(ctx, - query, - license, - l.ID, - ) - - if err != nil { - zap.L().Error("error in updating license data: ", zap.Error(err)) - return fmt.Errorf("failed to update license in db: %v", err) - } - - return nil -} - -func (r *Repo) CreateFeature(req *types.FeatureStatus) *basemodel.ApiError { - - _, err := r.store.BunDB().NewInsert(). - Model(req). - Exec(context.Background()) - if err != nil { - return &basemodel.ApiError{Typ: basemodel.ErrorInternal, Err: err} - } - return nil -} - -func (r *Repo) GetFeature(featureName string) (types.FeatureStatus, error) { - var feature types.FeatureStatus - - err := r.store.BunDB().NewSelect(). - Model(&feature). - Where("name = ?", featureName). - Scan(context.Background()) - - if err != nil { - return feature, err - } - if feature.Name == "" { - return feature, basemodel.ErrFeatureUnavailable{Key: featureName} - } - return feature, nil -} - -func (r *Repo) GetAllFeatures() ([]basemodel.Feature, error) { - - var feature []basemodel.Feature - - err := r.db.Select(&feature, - `SELECT * FROM feature_status;`) - if err != nil { - return feature, err - } - - return feature, nil -} - -func (r *Repo) UpdateFeature(req types.FeatureStatus) error { - - _, err := r.store.BunDB().NewUpdate(). - Model(&req). - Where("name = ?", req.Name). - Exec(context.Background()) - if err != nil { - return err - } - return nil -} - -func (r *Repo) InitFeatures(req []types.FeatureStatus) error { - // get a feature by name, if it doesn't exist, create it. If it does exist, update it. - for _, feature := range req { - currentFeature, err := r.GetFeature(feature.Name) - if err != nil && err == sql.ErrNoRows { - err := r.CreateFeature(&feature) - if err != nil { - return err - } - continue - } else if err != nil { - return err - } - feature.Usage = int(currentFeature.Usage) - if feature.Usage >= feature.UsageLimit && feature.UsageLimit != -1 { - feature.Active = false - } - err = r.UpdateFeature(feature) - if err != nil { - return err - } - } - return nil -} diff --git a/ee/query-service/license/manager.go b/ee/query-service/license/manager.go deleted file mode 100644 index ae5b5c8979..0000000000 --- a/ee/query-service/license/manager.go +++ /dev/null @@ -1,318 +0,0 @@ -package license - -import ( - "context" - "sync/atomic" - "time" - - "github.com/jmoiron/sqlx" - - "sync" - - baseconstants "github.com/SigNoz/signoz/pkg/query-service/constants" - "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/SigNoz/signoz/pkg/types" - "github.com/SigNoz/signoz/pkg/zeus" - - validate "github.com/SigNoz/signoz/ee/query-service/integrations/signozio" - "github.com/SigNoz/signoz/ee/query-service/model" - basemodel "github.com/SigNoz/signoz/pkg/query-service/model" - "github.com/SigNoz/signoz/pkg/query-service/telemetry" - "go.uber.org/zap" -) - -var LM *Manager - -// validate and update license every 24 hours -var validationFrequency = 24 * 60 * time.Minute - -type Manager struct { - repo *Repo - zeus zeus.Zeus - mutex sync.Mutex - validatorRunning bool - // end the license validation, this is important to gracefully - // stopping validation and protect in-consistent updates - done chan struct{} - // terminated waits for the validate go routine to end - terminated chan struct{} - // last time the license was validated - lastValidated int64 - // keep track of validation failure attempts - failedAttempts uint64 - // keep track of active license and features - activeLicenseV3 *model.LicenseV3 - activeFeatures basemodel.FeatureSet -} - -func StartManager(db *sqlx.DB, store sqlstore.SQLStore, zeus zeus.Zeus, features ...basemodel.Feature) (*Manager, error) { - if LM != nil { - return LM, nil - } - - repo := NewLicenseRepo(db, store) - m := &Manager{ - repo: &repo, - zeus: zeus, - } - if err := m.start(features...); err != nil { - return m, err - } - - LM = m - return m, nil -} - -// start loads active license in memory and initiates validator -func (lm *Manager) start(features ...basemodel.Feature) error { - return lm.LoadActiveLicenseV3(features...) -} - -func (lm *Manager) Stop() { - close(lm.done) - <-lm.terminated -} - -func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) { - lm.mutex.Lock() - defer lm.mutex.Unlock() - - if l == nil { - return - } - - lm.activeLicenseV3 = l - lm.activeFeatures = append(l.Features, features...) - // set default features - setDefaultFeatures(lm) - - err := lm.InitFeatures(lm.activeFeatures) - if err != nil { - zap.L().Panic("Couldn't activate features", zap.Error(err)) - } - if !lm.validatorRunning { - // we want to make sure only one validator runs, - // we already have lock() so good to go - lm.validatorRunning = true - go lm.ValidatorV3(context.Background()) - } - -} - -func setDefaultFeatures(lm *Manager) { - lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...) -} - -func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error { - active, err := lm.repo.GetActiveLicenseV3(context.Background()) - if err != nil { - return err - } - - if active != nil { - lm.SetActiveV3(active, features...) - } else { - zap.L().Info("No active license found, defaulting to basic plan") - // if no active license is found, we default to basic(free) plan with all default features - lm.activeFeatures = model.BasicPlan - setDefaultFeatures(lm) - err := lm.InitFeatures(lm.activeFeatures) - if err != nil { - zap.L().Error("Couldn't initialize features", zap.Error(err)) - return err - } - } - - return nil -} - -func (lm *Manager) GetLicensesV3(ctx context.Context) (response []*model.LicenseV3, apiError *model.ApiError) { - - licenses, err := lm.repo.GetLicensesV3(ctx) - if err != nil { - return nil, model.InternalError(err) - } - - for _, l := range licenses { - if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key { - l.IsCurrent = true - } - if l.ValidUntil == -1 { - // for subscriptions, there is no end-date as such - // but for showing user some validity we default one year timespan - l.ValidUntil = l.ValidFrom + 31556926 - } - response = append(response, l) - } - - return response, nil -} - -// Validator validates license after an epoch of time -func (lm *Manager) ValidatorV3(ctx context.Context) { - zap.L().Info("ValidatorV3 started!") - defer close(lm.terminated) - - tick := time.NewTicker(validationFrequency) - defer tick.Stop() - - _ = lm.ValidateV3(ctx) - for { - select { - case <-lm.done: - return - default: - select { - case <-lm.done: - return - case <-tick.C: - _ = lm.ValidateV3(ctx) - } - } - - } -} - -func (lm *Manager) RefreshLicense(ctx context.Context) error { - license, err := validate.ValidateLicenseV3(ctx, lm.activeLicenseV3.Key, lm.zeus) - if err != nil { - return err - } - - err = lm.repo.UpdateLicenseV3(ctx, license) - if err != nil { - return err - } - lm.SetActiveV3(license) - - return nil -} - -func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) { - if lm.activeLicenseV3 == nil { - return nil - } - - defer func() { - lm.mutex.Lock() - - lm.lastValidated = time.Now().Unix() - if reterr != nil { - zap.L().Error("License validation completed with error", zap.Error(reterr)) - - atomic.AddUint64(&lm.failedAttempts, 1) - // default to basic plan if validation fails for three consecutive times - if atomic.LoadUint64(&lm.failedAttempts) > 3 { - zap.L().Error("License validation completed with error for three consecutive times, defaulting to basic plan", zap.String("license_id", lm.activeLicenseV3.ID), zap.Bool("license_validation", false)) - lm.activeLicenseV3 = nil - lm.activeFeatures = model.BasicPlan - setDefaultFeatures(lm) - err := lm.InitFeatures(lm.activeFeatures) - if err != nil { - zap.L().Error("Couldn't initialize features", zap.Error(err)) - } - lm.done <- struct{}{} - lm.validatorRunning = false - } - - telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED, - map[string]interface{}{"err": reterr.Error()}, "", true, false) - } else { - // reset the failed attempts counter - atomic.StoreUint64(&lm.failedAttempts, 0) - zap.L().Info("License validation completed with no errors") - } - - lm.mutex.Unlock() - }() - - err := lm.RefreshLicense(ctx) - - if err != nil { - return err - } - return nil -} - -func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (*model.LicenseV3, error) { - license, err := validate.ValidateLicenseV3(ctx, licenseKey, lm.zeus) - if err != nil { - return nil, err - } - - // insert the new license to the sqlite db - modelErr := lm.repo.InsertLicenseV3(ctx, license) - if modelErr != nil { - zap.L().Error("failed to activate license", zap.Error(modelErr)) - return nil, modelErr - } - - // license is valid, activate it - lm.SetActiveV3(license) - return license, nil -} - -func (lm *Manager) GetActiveLicense() *model.LicenseV3 { - return lm.activeLicenseV3 -} - -// CheckFeature will be internally used by backend routines -// for feature gating -func (lm *Manager) CheckFeature(featureKey string) error { - feature, err := lm.repo.GetFeature(featureKey) - if err != nil { - return err - } - if feature.Active { - return nil - } - return basemodel.ErrFeatureUnavailable{Key: featureKey} -} - -// GetFeatureFlags returns current active features -func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) { - return lm.repo.GetAllFeatures() -} - -func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error { - featureStatus := make([]types.FeatureStatus, len(features)) - for i, f := range features { - featureStatus[i] = types.FeatureStatus{ - Name: f.Name, - Active: f.Active, - Usage: int(f.Usage), - UsageLimit: int(f.UsageLimit), - Route: f.Route, - } - } - return lm.repo.InitFeatures(featureStatus) -} - -func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error { - return lm.repo.UpdateFeature(types.FeatureStatus{ - Name: feature.Name, - Active: feature.Active, - Usage: int(feature.Usage), - UsageLimit: int(feature.UsageLimit), - Route: feature.Route, - }) -} - -func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) { - featureStatus, err := lm.repo.GetFeature(key) - if err != nil { - return basemodel.Feature{}, err - } - return basemodel.Feature{ - Name: featureStatus.Name, - Active: featureStatus.Active, - Usage: int64(featureStatus.Usage), - UsageLimit: int64(featureStatus.UsageLimit), - Route: featureStatus.Route, - }, nil -} - -// GetRepo return the license repo -func (lm *Manager) GetRepo() *Repo { - return lm.repo -} diff --git a/ee/query-service/main.go b/ee/query-service/main.go index bb8e0d7e17..9d11b7f2e9 100644 --- a/ee/query-service/main.go +++ b/ee/query-service/main.go @@ -6,6 +6,8 @@ import ( "os" "time" + "github.com/SigNoz/signoz/ee/licensing" + "github.com/SigNoz/signoz/ee/licensing/httplicensing" eeuserimpl "github.com/SigNoz/signoz/ee/modules/user/impluser" "github.com/SigNoz/signoz/ee/query-service/app" "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore" @@ -16,6 +18,7 @@ import ( "github.com/SigNoz/signoz/pkg/config/fileprovider" "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/factory" + pkglicensing "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/user" baseconst "github.com/SigNoz/signoz/pkg/query-service/constants" "github.com/SigNoz/signoz/pkg/signoz" @@ -23,6 +26,7 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/version" + pkgzeus "github.com/SigNoz/signoz/pkg/zeus" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -90,8 +94,9 @@ func main() { loggerMgr := initZapLog() zap.ReplaceGlobals(loggerMgr) defer loggerMgr.Sync() // flushes buffer, if any + ctx := context.Background() - config, err := signoz.NewConfig(context.Background(), config.ResolverConfig{ + config, err := signoz.NewConfig(ctx, config.ResolverConfig{ Uris: []string{"env:"}, ProviderFactories: []config.ProviderFactory{ envprovider.NewFactory(), @@ -129,6 +134,10 @@ func main() { config, zeus.Config(), httpzeus.NewProviderFactory(), + licensing.Config(24*time.Hour, 3), + func(sqlstore sqlstore.SQLStore, zeus pkgzeus.Zeus) factory.ProviderFactory[pkglicensing.Licensing, pkglicensing.Config] { + return httplicensing.NewProviderFactory(sqlstore, zeus) + }, signoz.NewEmailingProviderFactories(), signoz.NewCacheProviderFactories(), signoz.NewWebProviderFactories(), @@ -163,22 +172,22 @@ func main() { zap.L().Fatal("Failed to create server", zap.Error(err)) } - if err := server.Start(context.Background()); err != nil { + if err := server.Start(ctx); err != nil { zap.L().Fatal("Could not start server", zap.Error(err)) } - signoz.Start(context.Background()) + signoz.Start(ctx) - if err := signoz.Wait(context.Background()); err != nil { + if err := signoz.Wait(ctx); err != nil { zap.L().Fatal("Failed to start signoz", zap.Error(err)) } - err = server.Stop() + err = server.Stop(ctx) if err != nil { zap.L().Fatal("Failed to stop server", zap.Error(err)) } - err = signoz.Stop(context.Background()) + err = signoz.Stop(ctx) if err != nil { zap.L().Fatal("Failed to stop signoz", zap.Error(err)) } diff --git a/ee/query-service/model/license.go b/ee/query-service/model/license.go deleted file mode 100644 index 513d080891..0000000000 --- a/ee/query-service/model/license.go +++ /dev/null @@ -1,244 +0,0 @@ -package model - -import ( - "encoding/json" - "fmt" - "reflect" - "time" - - basemodel "github.com/SigNoz/signoz/pkg/query-service/model" - "github.com/pkg/errors" -) - -type License struct { - Key string `json:"key" db:"key"` - ActivationId string `json:"activationId" db:"activationId"` - CreatedAt time.Time `db:"created_at"` - - // PlanDetails contains the encrypted plan info - PlanDetails string `json:"planDetails" db:"planDetails"` - - // stores parsed license details - LicensePlan - - FeatureSet basemodel.FeatureSet - - // populated in case license has any errors - ValidationMessage string `db:"validationMessage"` - - // used only for sending details to front-end - IsCurrent bool `json:"isCurrent"` -} - -func (l *License) MarshalJSON() ([]byte, error) { - - return json.Marshal(&struct { - Key string `json:"key" db:"key"` - ActivationId string `json:"activationId" db:"activationId"` - ValidationMessage string `db:"validationMessage"` - IsCurrent bool `json:"isCurrent"` - PlanKey string `json:"planKey"` - ValidFrom time.Time `json:"ValidFrom"` - ValidUntil time.Time `json:"ValidUntil"` - Status string `json:"status"` - }{ - Key: l.Key, - ActivationId: l.ActivationId, - IsCurrent: l.IsCurrent, - PlanKey: l.PlanKey, - ValidFrom: time.Unix(l.ValidFrom, 0), - ValidUntil: time.Unix(l.ValidUntil, 0), - Status: l.Status, - ValidationMessage: l.ValidationMessage, - }) -} - -type LicensePlan struct { - PlanKey string `json:"planKey"` - ValidFrom int64 `json:"validFrom"` - ValidUntil int64 `json:"validUntil"` - Status string `json:"status"` -} - -type Licenses struct { - TrialStart int64 `json:"trialStart"` - TrialEnd int64 `json:"trialEnd"` - OnTrial bool `json:"onTrial"` - WorkSpaceBlock bool `json:"workSpaceBlock"` - TrialConvertedToSubscription bool `json:"trialConvertedToSubscription"` - GracePeriodEnd int64 `json:"gracePeriodEnd"` - Licenses []License `json:"licenses"` -} - -type SubscriptionServerResp struct { - Status string `json:"status"` - Data Licenses `json:"data"` -} - -type Plan struct { - Name string `json:"name"` -} - -type LicenseDB struct { - ID string `json:"id"` - Key string `json:"key"` - Data string `json:"data"` -} -type LicenseV3 struct { - ID string - Key string - Data map[string]interface{} - PlanName string - Features basemodel.FeatureSet - Status string - IsCurrent bool - ValidFrom int64 - ValidUntil int64 -} - -func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) { - var zeroValue T - if val, ok := data[key]; ok { - if value, ok := val.(T); ok { - return value, nil - } - return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue)) - } - return zeroValue, fmt.Errorf("%s key is missing", key) -} - -func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) { - var features basemodel.FeatureSet - - // extract id from data - licenseID, err := extractKeyFromMapStringInterface[string](data, "id") - if err != nil { - return nil, err - } - delete(data, "id") - - // extract key from data - licenseKey, err := extractKeyFromMapStringInterface[string](data, "key") - if err != nil { - return nil, err - } - delete(data, "key") - - // extract status from data - status, err := extractKeyFromMapStringInterface[string](data, "status") - if err != nil { - return nil, err - } - - planMap, err := extractKeyFromMapStringInterface[map[string]any](data, "plan") - if err != nil { - return nil, err - } - - planName, err := extractKeyFromMapStringInterface[string](planMap, "name") - if err != nil { - return nil, err - } - // if license status is invalid then default it to basic - if status == LicenseStatusInvalid { - planName = PlanNameBasic - } - - featuresFromZeus := basemodel.FeatureSet{} - if _features, ok := data["features"]; ok { - featuresData, err := json.Marshal(_features) - if err != nil { - return nil, errors.Wrap(err, "failed to marshal features data") - } - - if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal features data") - } - } - - switch planName { - case PlanNameEnterprise: - features = append(features, EnterprisePlan...) - case PlanNameBasic: - features = append(features, BasicPlan...) - default: - features = append(features, BasicPlan...) - } - - if len(featuresFromZeus) > 0 { - for _, feature := range featuresFromZeus { - exists := false - for i, existingFeature := range features { - if existingFeature.Name == feature.Name { - features[i] = feature // Replace existing feature - exists = true - break - } - } - if !exists { - features = append(features, feature) // Append if it doesn't exist - } - } - } - data["features"] = features - - _validFrom, err := extractKeyFromMapStringInterface[float64](data, "valid_from") - if err != nil { - _validFrom = 0 - } - validFrom := int64(_validFrom) - - _validUntil, err := extractKeyFromMapStringInterface[float64](data, "valid_until") - if err != nil { - _validUntil = 0 - } - validUntil := int64(_validUntil) - - return &LicenseV3{ - ID: licenseID, - Key: licenseKey, - Data: data, - PlanName: planName, - Features: features, - ValidFrom: validFrom, - ValidUntil: validUntil, - Status: status, - }, nil - -} - -func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) { - licenseDataWithIdAndKey := data - licenseDataWithIdAndKey["id"] = id - licenseDataWithIdAndKey["key"] = key - return NewLicenseV3(licenseDataWithIdAndKey) -} - -func ConvertLicenseV3ToLicenseV2(l *LicenseV3) *License { - planKeyFromPlanName, ok := MapOldPlanKeyToNewPlanName[l.PlanName] - if !ok { - planKeyFromPlanName = Basic - } - return &License{ - Key: l.Key, - ActivationId: "", - PlanDetails: "", - FeatureSet: l.Features, - ValidationMessage: "", - IsCurrent: l.IsCurrent, - LicensePlan: LicensePlan{ - PlanKey: planKeyFromPlanName, - ValidFrom: l.ValidFrom, - ValidUntil: l.ValidUntil, - Status: l.Status}, - } - -} - -type CheckoutRequest struct { - SuccessURL string `json:"url"` -} - -type PortalRequest struct { - SuccessURL string `json:"url"` -} diff --git a/ee/query-service/model/license_test.go b/ee/query-service/model/license_test.go deleted file mode 100644 index 710541eea3..0000000000 --- a/ee/query-service/model/license_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package model - -import ( - "encoding/json" - "testing" - - "github.com/SigNoz/signoz/pkg/query-service/model" - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewLicenseV3(t *testing.T) { - testCases := []struct { - name string - data []byte - pass bool - expected *LicenseV3 - error error - }{ - { - name: "Error for missing license id", - data: []byte(`{}`), - pass: false, - error: errors.New("id key is missing"), - }, - { - name: "Error for license id not being a valid string", - data: []byte(`{"id": 10}`), - pass: false, - error: errors.New("id key is not a valid string"), - }, - { - name: "Error for missing license key", - data: []byte(`{"id":"does-not-matter"}`), - pass: false, - error: errors.New("key key is missing"), - }, - { - name: "Error for invalid string license key", - data: []byte(`{"id":"does-not-matter","key":10}`), - pass: false, - error: errors.New("key key is not a valid string"), - }, - { - name: "Error for missing license status", - data: []byte(`{"id":"does-not-matter", "key": "does-not-matter","category":"FREE"}`), - pass: false, - error: errors.New("status key is missing"), - }, - { - name: "Error for invalid string license status", - data: []byte(`{"id":"does-not-matter","key": "does-not-matter", "category":"FREE", "status":10}`), - pass: false, - error: errors.New("status key is not a valid string"), - }, - { - name: "Error for missing license plan", - data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`), - pass: false, - error: errors.New("plan key is missing"), - }, - { - name: "Error for invalid json license plan", - data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`), - pass: false, - error: errors.New("plan key is not a valid map[string]interface {}"), - }, - { - name: "Error for invalid license plan", - data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`), - pass: false, - error: errors.New("name key is missing"), - }, - { - name: "Parse the entire license properly", - data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`), - pass: true, - expected: &LicenseV3{ - ID: "does-not-matter", - Key: "does-not-matter-key", - Data: map[string]interface{}{ - "plan": map[string]interface{}{ - "name": "ENTERPRISE", - }, - "category": "FREE", - "status": "ACTIVE", - "valid_from": float64(1730899309), - "valid_until": float64(-1), - }, - PlanName: PlanNameEnterprise, - ValidFrom: 1730899309, - ValidUntil: -1, - Status: "ACTIVE", - IsCurrent: false, - Features: model.FeatureSet{}, - }, - }, - { - name: "Fallback to basic plan if license status is invalid", - data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`), - pass: true, - expected: &LicenseV3{ - ID: "does-not-matter", - Key: "does-not-matter-key", - Data: map[string]interface{}{ - "plan": map[string]interface{}{ - "name": "ENTERPRISE", - }, - "category": "FREE", - "status": "INVALID", - "valid_from": float64(1730899309), - "valid_until": float64(-1), - }, - PlanName: PlanNameBasic, - ValidFrom: 1730899309, - ValidUntil: -1, - Status: "INVALID", - IsCurrent: false, - Features: model.FeatureSet{}, - }, - }, - { - name: "fallback states for validFrom and validUntil", - data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from":1234.456,"valid_until":5678.567}`), - pass: true, - expected: &LicenseV3{ - ID: "does-not-matter", - Key: "does-not-matter-key", - Data: map[string]interface{}{ - "plan": map[string]interface{}{ - "name": "ENTERPRISE", - }, - "valid_from": 1234.456, - "valid_until": 5678.567, - "category": "FREE", - "status": "ACTIVE", - }, - PlanName: PlanNameEnterprise, - ValidFrom: 1234, - ValidUntil: 5678, - Status: "ACTIVE", - IsCurrent: false, - Features: model.FeatureSet{}, - }, - }, - } - - for _, tc := range testCases { - var licensePayload map[string]interface{} - err := json.Unmarshal(tc.data, &licensePayload) - require.NoError(t, err) - license, err := NewLicenseV3(licensePayload) - if license != nil { - license.Features = make(model.FeatureSet, 0) - delete(license.Data, "features") - } - - if tc.pass { - require.NoError(t, err) - require.NotNil(t, license) - assert.Equal(t, tc.expected, license) - } else { - require.Error(t, err) - assert.EqualError(t, err, tc.error.Error()) - require.Nil(t, license) - } - - } -} diff --git a/ee/query-service/usage/manager.go b/ee/query-service/usage/manager.go index e42946cf9c..56341a5b39 100644 --- a/ee/query-service/usage/manager.go +++ b/ee/query-service/usage/manager.go @@ -15,8 +15,9 @@ import ( "go.uber.org/zap" "github.com/SigNoz/signoz/ee/query-service/dao" - "github.com/SigNoz/signoz/ee/query-service/license" "github.com/SigNoz/signoz/ee/query-service/model" + "github.com/SigNoz/signoz/pkg/licensing" + "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/query-service/utils/encryption" "github.com/SigNoz/signoz/pkg/zeus" ) @@ -35,64 +36,72 @@ var ( type Manager struct { clickhouseConn clickhouse.Conn - licenseRepo *license.Repo + licenseService licensing.Licensing scheduler *gocron.Scheduler modelDao dao.ModelDao zeus zeus.Zeus + + organizationModule organization.Module } -func New(modelDao dao.ModelDao, licenseRepo *license.Repo, clickhouseConn clickhouse.Conn, zeus zeus.Zeus) (*Manager, error) { +func New(modelDao dao.ModelDao, licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus,organizationModule organization.Module) (*Manager, error) { m := &Manager{ clickhouseConn: clickhouseConn, - licenseRepo: licenseRepo, + licenseService: licenseService, scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC modelDao: modelDao, zeus: zeus, + organizationModule: organizationModule, } return m, nil } // start loads collects and exports any exported snapshot and starts the exporter -func (lm *Manager) Start() error { +func (lm *Manager) Start(ctx context.Context) error { // compares the locker and stateUnlocked if both are same lock is applied else returns error if !atomic.CompareAndSwapUint32(&locker, stateUnlocked, stateLocked) { return fmt.Errorf("usage exporter is locked") } - _, err := lm.scheduler.Do(func() { lm.UploadUsage() }) + // upload usage once when starting the service + + _, err := lm.scheduler.Do(func() { lm.UploadUsage(ctx) }) if err != nil { return err } - // upload usage once when starting the service - lm.UploadUsage() - + lm.UploadUsage(ctx) lm.scheduler.StartAsync() - return nil } -func (lm *Manager) UploadUsage() { - ctx := context.Background() - // check if license is present or not - license, err := lm.licenseRepo.GetActiveLicense(ctx) +func (lm *Manager) UploadUsage(ctx context.Context) { + + organizations, err := lm.organizationModule.GetAll(context.Background()) if err != nil { - zap.L().Error("failed to get active license", zap.Error(err)) - return - } - if license == nil { - // we will not start the usage reporting if license is not present. - zap.L().Info("no license present, skipping usage reporting") + zap.L().Error("failed to get organizations", zap.Error(err)) return } + for _, organization := range organizations { + // check if license is present or not + license, err := lm.licenseService.GetActive(ctx, organization.ID) + if err != nil { + zap.L().Error("failed to get active license", zap.Error(err)) + return + } + if license == nil { + // we will not start the usage reporting if license is not present. + zap.L().Info("no license present, skipping usage reporting") + return + } - usages := []model.UsageDB{} + usages := []model.UsageDB{} - // get usage from clickhouse - dbs := []string{"signoz_logs", "signoz_traces", "signoz_metrics"} - query := ` + // get usage from clickhouse + dbs := []string{"signoz_logs", "signoz_traces", "signoz_metrics"} + query := ` SELECT tenant, collector_id, exporter_id, timestamp, data FROM %s.distributed_usage as u1 GLOBAL INNER JOIN @@ -107,76 +116,76 @@ func (lm *Manager) UploadUsage() { order by timestamp ` - for _, db := range dbs { - dbusages := []model.UsageDB{} - err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour))) - if err != nil && !strings.Contains(err.Error(), "doesn't exist") { - zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err)) - return + for _, db := range dbs { + dbusages := []model.UsageDB{} + err := lm.clickhouseConn.Select(ctx, &dbusages, fmt.Sprintf(query, db, db), time.Now().Add(-(24 * time.Hour))) + if err != nil && !strings.Contains(err.Error(), "doesn't exist") { + zap.L().Error("failed to get usage from clickhouse: %v", zap.Error(err)) + return + } + for _, u := range dbusages { + u.Type = db + usages = append(usages, u) + } } - for _, u := range dbusages { - u.Type = db - usages = append(usages, u) - } - } - if len(usages) <= 0 { - zap.L().Info("no snapshots to upload, skipping.") - return - } - - zap.L().Info("uploading usage data") - - usagesPayload := []model.Usage{} - for _, usage := range usages { - usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data)) - if err != nil { - zap.L().Error("error while decrypting usage data: %v", zap.Error(err)) + if len(usages) <= 0 { + zap.L().Info("no snapshots to upload, skipping.") return } - usageData := model.Usage{} - err = json.Unmarshal(usageDataBytes, &usageData) - if err != nil { - zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err)) + zap.L().Info("uploading usage data") + + usagesPayload := []model.Usage{} + for _, usage := range usages { + usageDataBytes, err := encryption.Decrypt([]byte(usage.ExporterID[:32]), []byte(usage.Data)) + if err != nil { + zap.L().Error("error while decrypting usage data: %v", zap.Error(err)) + return + } + + usageData := model.Usage{} + err = json.Unmarshal(usageDataBytes, &usageData) + if err != nil { + zap.L().Error("error while unmarshalling usage data: %v", zap.Error(err)) + return + } + + usageData.CollectorID = usage.CollectorID + usageData.ExporterID = usage.ExporterID + usageData.Type = usage.Type + usageData.Tenant = "default" + usageData.OrgName = "default" + usageData.TenantId = "default" + usagesPayload = append(usagesPayload, usageData) + } + + key, _ := uuid.Parse(license.Key) + payload := model.UsagePayload{ + LicenseKey: key, + Usage: usagesPayload, + } + + body, errv2 := json.Marshal(payload) + if errv2 != nil { + zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2)) return } - usageData.CollectorID = usage.CollectorID - usageData.ExporterID = usage.ExporterID - usageData.Type = usage.Type - usageData.Tenant = "default" - usageData.OrgName = "default" - usageData.TenantId = "default" - usagesPayload = append(usagesPayload, usageData) - } - - key, _ := uuid.Parse(license.Key) - payload := model.UsagePayload{ - LicenseKey: key, - Usage: usagesPayload, - } - - body, errv2 := json.Marshal(payload) - if errv2 != nil { - zap.L().Error("error while marshalling usage payload: %v", zap.Error(errv2)) - return - } - - errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body) - if errv2 != nil { - zap.L().Error("failed to upload usage: %v", zap.Error(errv2)) - // not returning error here since it is captured in the failed count - return + errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body) + if errv2 != nil { + zap.L().Error("failed to upload usage: %v", zap.Error(errv2)) + // not returning error here since it is captured in the failed count + return + } } } -func (lm *Manager) Stop() { +func (lm *Manager) Stop(ctx context.Context) { lm.scheduler.Stop() zap.L().Info("sending usage data before shutting down") // send usage before shutting down - lm.UploadUsage() - + lm.UploadUsage(ctx) atomic.StoreUint32(&locker, stateUnlocked) } diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 092fedaae6..9c9719694b 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -36,8 +36,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { user, isLoggedIn: isLoggedInState, isFetchingOrgPreferences, - activeLicenseV3, - isFetchingActiveLicenseV3, + activeLicense, + isFetchingActiveLicense, trialInfo, featureFlags, } = useAppContext(); @@ -145,16 +145,16 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { }; useEffect(() => { - if (!isFetchingActiveLicenseV3 && activeLicenseV3) { + if (!isFetchingActiveLicense && activeLicense) { const currentRoute = mapRoutes.get('current'); - const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED; - const isExpired = activeLicenseV3.state === LicenseState.EXPIRED; - const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED; + const isTerminated = activeLicense.state === LicenseState.TERMINATED; + const isExpired = activeLicense.state === LicenseState.EXPIRED; + const isCancelled = activeLicense.state === LicenseState.CANCELLED; const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled; - const { platform } = activeLicenseV3; + const { platform } = activeLicense; if ( isWorkspaceAccessRestricted && @@ -164,26 +164,26 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { navigateToWorkSpaceAccessRestricted(currentRoute); } } - }, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]); + }, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]); useEffect(() => { - if (!isFetchingActiveLicenseV3) { + if (!isFetchingActiveLicense) { const currentRoute = mapRoutes.get('current'); const shouldBlockWorkspace = trialInfo?.workSpaceBlock; if ( shouldBlockWorkspace && currentRoute && - activeLicenseV3?.platform === LicensePlatform.CLOUD + activeLicense?.platform === LicensePlatform.CLOUD ) { navigateToWorkSpaceBlocked(currentRoute); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - isFetchingActiveLicenseV3, + isFetchingActiveLicense, trialInfo?.workSpaceBlock, - activeLicenseV3?.platform, + activeLicense?.platform, mapRoutes, pathname, ]); @@ -197,20 +197,20 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { }; useEffect(() => { - if (!isFetchingActiveLicenseV3 && activeLicenseV3) { + if (!isFetchingActiveLicense && activeLicense) { const currentRoute = mapRoutes.get('current'); const shouldSuspendWorkspace = - activeLicenseV3.state === LicenseState.DEFAULTED; + activeLicense.state === LicenseState.DEFAULTED; if ( shouldSuspendWorkspace && currentRoute && - activeLicenseV3.platform === LicensePlatform.CLOUD + activeLicense.platform === LicensePlatform.CLOUD ) { navigateToWorkSpaceSuspended(currentRoute); } } - }, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]); + }, [isFetchingActiveLicense, activeLicense, mapRoutes, pathname]); useEffect(() => { if (org && org.length > 0 && org[0].id !== undefined) { diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index e24704a22c..e2c693b4bf 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -13,9 +13,9 @@ import AppLayout from 'container/AppLayout'; import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys'; import { useThemeConfig } from 'hooks/useDarkMode'; import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; -import { LICENSE_PLAN_KEY } from 'hooks/useLicense'; import { NotificationProvider } from 'hooks/useNotifications'; import { ResourceProvider } from 'hooks/useResourceAttribute'; +import { StatusCodes } from 'http-status-codes'; import history from 'lib/history'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import posthog from 'posthog-js'; @@ -41,14 +41,13 @@ import defaultRoutes, { function App(): JSX.Element { const themeConfig = useThemeConfig(); const { - licenses, user, isFetchingUser, - isFetchingLicenses, isFetchingFeatureFlags, trialInfo, - activeLicenseV3, - isFetchingActiveLicenseV3, + activeLicense, + isFetchingActiveLicense, + activeLicenseFetchError, userFetchError, featureFlagsFetchError, isLoggedIn: isLoggedInState, @@ -66,7 +65,7 @@ function App(): JSX.Element { const enableAnalytics = useCallback( (user: IUser): void => { // wait for the required data to be loaded before doing init for anything! - if (!isFetchingActiveLicenseV3 && activeLicenseV3 && org) { + if (!isFetchingActiveLicense && activeLicense && org) { const orgName = org && Array.isArray(org) && org.length > 0 ? org[0].displayName : ''; @@ -153,8 +152,8 @@ function App(): JSX.Element { }, [ hostname, - isFetchingActiveLicenseV3, - activeLicenseV3, + isFetchingActiveLicense, + activeLicense, org, trialInfo?.trialConvertedToSubscription, ], @@ -163,18 +162,17 @@ function App(): JSX.Element { // eslint-disable-next-line sonarjs/cognitive-complexity useEffect(() => { if ( - !isFetchingLicenses && - licenses && + !isFetchingActiveLicense && + (activeLicense || activeLicenseFetchError) && !isFetchingUser && user && !!user.email ) { const isOnBasicPlan = - licenses.licenses?.some( - (license) => - license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN, - ) || licenses.licenses === null; - + activeLicenseFetchError && + [StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes( + activeLicenseFetchError?.getHttpStatusCode(), + ); const isIdentifiedUser = getLocalStorageApi(LOCALSTORAGE.IS_IDENTIFIED_USER); if (isLoggedInState && user && user.id && user.email && !isIdentifiedUser) { @@ -204,11 +202,12 @@ function App(): JSX.Element { }, [ isLoggedInState, user, - licenses, isCloudUser, isEnterpriseSelfHostedUser, - isFetchingLicenses, + isFetchingActiveLicense, isFetchingUser, + activeLicense, + activeLicenseFetchError, ]); useEffect(() => { @@ -231,8 +230,7 @@ function App(): JSX.Element { if ( !isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError) && - licenses && - activeLicenseV3 && + activeLicense && trialInfo ) { let isChatSupportEnabled = false; @@ -270,8 +268,7 @@ function App(): JSX.Element { featureFlags, isFetchingFeatureFlags, featureFlagsFetchError, - licenses, - activeLicenseV3, + activeLicense, trialInfo, isCloudUser, isEnterpriseSelfHostedUser, @@ -333,7 +330,7 @@ function App(): JSX.Element { // if the user is in logged in state if (isLoggedInState) { // if the setup calls are loading then return a spinner - if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) { + if (isFetchingActiveLicense || isFetchingUser || isFetchingFeatureFlags) { return ; } @@ -345,7 +342,11 @@ function App(): JSX.Element { } // if all of the data is not set then return a spinner, this is required because there is some gap between loading states and data setting - if ((!licenses || !user.email || !featureFlags) && !userFetchError) { + if ( + (!activeLicense || !user.email || !featureFlags) && + !userFetchError && + !activeLicenseFetchError + ) { return ; } } diff --git a/frontend/src/api/billing/checkout.ts b/frontend/src/api/billing/checkout.ts deleted file mode 100644 index f8eaf39748..0000000000 --- a/frontend/src/api/billing/checkout.ts +++ /dev/null @@ -1,29 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { - CheckoutRequestPayloadProps, - CheckoutSuccessPayloadProps, -} from 'types/api/billing/checkout'; - -const updateCreditCardApi = async ( - props: CheckoutRequestPayloadProps, -): Promise | ErrorResponse> => { - try { - const response = await axios.post('/checkout', { - url: props.url, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default updateCreditCardApi; diff --git a/frontend/src/api/billing/manage.ts b/frontend/src/api/billing/manage.ts deleted file mode 100644 index 1ea8fa762d..0000000000 --- a/frontend/src/api/billing/manage.ts +++ /dev/null @@ -1,29 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { - CheckoutRequestPayloadProps, - CheckoutSuccessPayloadProps, -} from 'types/api/billing/checkout'; - -const manageCreditCardApi = async ( - props: CheckoutRequestPayloadProps, -): Promise | ErrorResponse> => { - try { - const response = await axios.post('/portal', { - url: props.url, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default manageCreditCardApi; diff --git a/frontend/src/api/licenses/apply.ts b/frontend/src/api/licenses/apply.ts deleted file mode 100644 index c691ad836f..0000000000 --- a/frontend/src/api/licenses/apply.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ApiV3Instance as axios } from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/licenses/apply'; - -const apply = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.post('/licenses', { - key: props.key, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default apply; diff --git a/frontend/src/api/licenses/getAll.ts b/frontend/src/api/licenses/getAll.ts deleted file mode 100644 index b05cdcb9e2..0000000000 --- a/frontend/src/api/licenses/getAll.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiV3Instance as axios } from 'api'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps } from 'types/api/licenses/getAll'; - -const getAll = async (): Promise< - SuccessResponse | ErrorResponse -> => { - const response = await axios.get('/licenses'); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; -}; - -export default getAll; diff --git a/frontend/src/api/licensesV3/getActive.ts b/frontend/src/api/licensesV3/getActive.ts deleted file mode 100644 index 48dd0a3a43..0000000000 --- a/frontend/src/api/licensesV3/getActive.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiV3Instance as axios } from 'api'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { LicenseV3EventQueueResModel } from 'types/api/licensesV3/getActive'; - -const getActive = async (): Promise< - SuccessResponse | ErrorResponse -> => { - const response = await axios.get('/licenses/active'); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; -}; - -export default getActive; diff --git a/frontend/src/api/v1/checkout/create.ts b/frontend/src/api/v1/checkout/create.ts new file mode 100644 index 0000000000..2e71a64768 --- /dev/null +++ b/frontend/src/api/v1/checkout/create.ts @@ -0,0 +1,28 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { + CheckoutRequestPayloadProps, + CheckoutSuccessPayloadProps, + PayloadProps, +} from 'types/api/billing/checkout'; + +const updateCreditCardApi = async ( + props: CheckoutRequestPayloadProps, +): Promise> => { + try { + const response = await axios.post('/checkout', { + url: props.url, + }); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default updateCreditCardApi; diff --git a/frontend/src/api/v1/portal/create.ts b/frontend/src/api/v1/portal/create.ts new file mode 100644 index 0000000000..1c6854ffe2 --- /dev/null +++ b/frontend/src/api/v1/portal/create.ts @@ -0,0 +1,28 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { + CheckoutRequestPayloadProps, + CheckoutSuccessPayloadProps, + PayloadProps, +} from 'types/api/billing/checkout'; + +const manageCreditCardApi = async ( + props: CheckoutRequestPayloadProps, +): Promise> => { + try { + const response = await axios.post('/portal', { + url: props.url, + }); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default manageCreditCardApi; diff --git a/frontend/src/api/v3/licenses/active/get.ts b/frontend/src/api/v3/licenses/active/get.ts new file mode 100644 index 0000000000..7bf73e95ca --- /dev/null +++ b/frontend/src/api/v3/licenses/active/get.ts @@ -0,0 +1,25 @@ +import { ApiV3Instance as axios } from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { + LicenseEventQueueResModel, + PayloadProps, +} from 'types/api/licensesV3/getActive'; + +const getActive = async (): Promise< + SuccessResponseV2 +> => { + try { + const response = await axios.get('/licenses/active'); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default getActive; diff --git a/frontend/src/api/v3/licenses/put.ts b/frontend/src/api/v3/licenses/put.ts new file mode 100644 index 0000000000..4cd971acc0 --- /dev/null +++ b/frontend/src/api/v3/licenses/put.ts @@ -0,0 +1,24 @@ +import { ApiV3Instance as axios } from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { PayloadProps, Props } from 'types/api/licenses/apply'; + +const apply = async ( + props: Props, +): Promise> => { + try { + const response = await axios.post('/licenses', { + key: props.key, + }); + + return { + httpStatusCode: response.status, + data: response.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default apply; diff --git a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx index 94e8de8194..64657c8398 100644 --- a/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx +++ b/frontend/src/components/ChatSupportGateway/ChatSupportGateway.tsx @@ -1,14 +1,14 @@ import { Button, Modal, Typography } from 'antd'; -import updateCreditCardApi from 'api/billing/checkout'; import logEvent from 'api/common/logEvent'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; +import updateCreditCardApi from 'api/v1/checkout/create'; import { useNotifications } from 'hooks/useNotifications'; import { CreditCard, X } from 'lucide-react'; import { useState } from 'react'; import { useMutation } from 'react-query'; import { useLocation } from 'react-router-dom'; -import { ErrorResponse, SuccessResponse } from 'types/api'; +import { SuccessResponseV2 } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; +import APIError from 'types/api/error'; export default function ChatSupportGateway(): JSX.Element { const { notifications } = useNotifications(); @@ -18,20 +18,21 @@ export default function ChatSupportGateway(): JSX.Element { ); const handleBillingOnSuccess = ( - data: ErrorResponse | SuccessResponse, + data: SuccessResponseV2, ): void => { - if (data?.payload?.redirectURL) { + if (data?.data?.redirectURL) { const newTab = document.createElement('a'); - newTab.href = data.payload.redirectURL; + newTab.href = data.data.redirectURL; newTab.target = '_blank'; newTab.rel = 'noopener noreferrer'; newTab.click(); } }; - const handleBillingOnError = (): void => { + const handleBillingOnError = (error: APIError): void => { notifications.error({ - message: SOMETHING_WENT_WRONG, + message: error.getErrorCode(), + description: error.getErrorMessage(), }); }; diff --git a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx index e04004a292..d25e8156ed 100644 --- a/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx +++ b/frontend/src/components/LaunchChatSupport/LaunchChatSupport.tsx @@ -1,10 +1,9 @@ import './LaunchChatSupport.styles.scss'; import { Button, Modal, Tooltip, Typography } from 'antd'; -import updateCreditCardApi from 'api/billing/checkout'; import logEvent from 'api/common/logEvent'; +import updateCreditCardApi from 'api/v1/checkout/create'; import cx from 'classnames'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; import { FeatureKeys } from 'constants/features'; import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; import { useNotifications } from 'hooks/useNotifications'; @@ -14,8 +13,9 @@ import { useAppContext } from 'providers/App/App'; import { useMemo, useState } from 'react'; import { useMutation } from 'react-query'; import { useLocation } from 'react-router-dom'; -import { ErrorResponse, SuccessResponse } from 'types/api'; +import { SuccessResponseV2 } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; +import APIError from 'types/api/error'; export interface LaunchChatSupportProps { eventName: string; @@ -118,20 +118,21 @@ function LaunchChatSupport({ }; const handleBillingOnSuccess = ( - data: ErrorResponse | SuccessResponse, + data: SuccessResponseV2, ): void => { - if (data?.payload?.redirectURL) { + if (data?.data?.redirectURL) { const newTab = document.createElement('a'); - newTab.href = data.payload.redirectURL; + newTab.href = data.data.redirectURL; newTab.target = '_blank'; newTab.rel = 'noopener noreferrer'; newTab.click(); } }; - const handleBillingOnError = (): void => { + const handleBillingOnError = (error: APIError): void => { notifications.error({ - message: SOMETHING_WENT_WRONG, + message: error.getErrorCode(), + description: error.getErrorMessage(), }); }; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 03accc56f0..e481a4165c 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -5,16 +5,15 @@ import './AppLayout.styles.scss'; import * as Sentry from '@sentry/react'; import { Flex } from 'antd'; -import manageCreditCardApi from 'api/billing/manage'; import getLocalStorageApi from 'api/browser/localstorage/get'; import setLocalStorageApi from 'api/browser/localstorage/set'; import logEvent from 'api/common/logEvent'; +import manageCreditCardApi from 'api/v1/portal/create'; import getUserLatestVersion from 'api/v1/version/getLatestVersion'; import getUserVersion from 'api/v1/version/getVersion'; import cx from 'classnames'; import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; import { Events } from 'constants/events'; import { FeatureKeys } from 'constants/features'; import { LOCALSTORAGE } from 'constants/localStorage'; @@ -51,8 +50,9 @@ import { UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION_ERROR, } from 'types/actions/app'; -import { ErrorResponse, SuccessResponse } from 'types/api'; +import { SuccessResponseV2 } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; +import APIError from 'types/api/error'; import { LicenseEvent, LicensePlatform, @@ -75,8 +75,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element { isLoggedIn, user, trialInfo, - activeLicenseV3, - isFetchingActiveLicenseV3, + activeLicense, + isFetchingActiveLicense, featureFlags, isFetchingFeatureFlags, featureFlagsFetchError, @@ -93,20 +93,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const [slowApiWarningShown, setSlowApiWarningShown] = useState(false); const handleBillingOnSuccess = ( - data: ErrorResponse | SuccessResponse, + data: SuccessResponseV2, ): void => { - if (data?.payload?.redirectURL) { + if (data?.data?.redirectURL) { const newTab = document.createElement('a'); - newTab.href = data.payload.redirectURL; + newTab.href = data.data.redirectURL; newTab.target = '_blank'; newTab.rel = 'noopener noreferrer'; newTab.click(); } }; - const handleBillingOnError = (): void => { + const handleBillingOnError = (error: APIError): void => { notifications.error({ - message: SOMETHING_WENT_WRONG, + message: error.getErrorCode(), + description: error.getErrorMessage(), }); }; @@ -260,8 +261,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element { useEffect(() => { if ( - !isFetchingActiveLicenseV3 && - activeLicenseV3 && + !isFetchingActiveLicense && + activeLicense && trialInfo?.onTrial && !trialInfo?.trialConvertedToSubscription && !trialInfo?.workSpaceBlock && @@ -269,16 +270,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element { ) { setShowTrialExpiryBanner(true); } - }, [isFetchingActiveLicenseV3, activeLicenseV3, trialInfo]); + }, [isFetchingActiveLicense, activeLicense, trialInfo]); useEffect(() => { - if (!isFetchingActiveLicenseV3 && activeLicenseV3) { - const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED; - const isExpired = activeLicenseV3.state === LicenseState.EXPIRED; - const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED; - const isDefaulted = activeLicenseV3.state === LicenseState.DEFAULTED; + if (!isFetchingActiveLicense && activeLicense) { + const isTerminated = activeLicense.state === LicenseState.TERMINATED; + const isExpired = activeLicense.state === LicenseState.EXPIRED; + const isCancelled = activeLicense.state === LicenseState.CANCELLED; + const isDefaulted = activeLicense.state === LicenseState.DEFAULTED; const isEvaluationExpired = - activeLicenseV3.state === LicenseState.EVALUATION_EXPIRED; + activeLicense.state === LicenseState.EVALUATION_EXPIRED; const isWorkspaceAccessRestricted = isTerminated || @@ -287,7 +288,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { isDefaulted || isEvaluationExpired; - const { platform } = activeLicenseV3; + const { platform } = activeLicense; if ( isWorkspaceAccessRestricted && @@ -296,17 +297,17 @@ function AppLayout(props: AppLayoutProps): JSX.Element { setShowWorkspaceRestricted(true); } } - }, [isFetchingActiveLicenseV3, activeLicenseV3]); + }, [isFetchingActiveLicense, activeLicense]); useEffect(() => { if ( - !isFetchingActiveLicenseV3 && - !isNull(activeLicenseV3) && - activeLicenseV3?.event_queue?.event === LicenseEvent.DEFAULT + !isFetchingActiveLicense && + !isNull(activeLicense) && + activeLicense?.event_queue?.event === LicenseEvent.DEFAULT ) { setShowPaymentFailedWarning(true); } - }, [activeLicenseV3, isFetchingActiveLicenseV3]); + }, [activeLicense, isFetchingActiveLicense]); useEffect(() => { // after logging out hide the trial expiry banner @@ -392,7 +393,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { if ( !isFetchingFeatureFlags && (featureFlags || featureFlagsFetchError) && - activeLicenseV3 && + activeLicense && trialInfo ) { let isChatSupportEnabled = false; @@ -421,7 +422,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { isCloudUserVal, isFetchingFeatureFlags, isLoggedIn, - activeLicenseV3, + activeLicense, trialInfo, ]); @@ -523,14 +524,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element { const renderWorkspaceRestrictedBanner = (): JSX.Element => (
- {activeLicenseV3?.state === LicenseState.TERMINATED && ( + {activeLicense?.state === LicenseState.TERMINATED && ( <> Your SigNoz license is terminated, enterprise features have been disabled. Please contact support at{' '} support@signoz.io for new license )} - {activeLicenseV3?.state === LicenseState.EXPIRED && ( + {activeLicense?.state === LicenseState.EXPIRED && ( <> Your SigNoz license has expired. Please contact support at{' '} support@signoz.io for renewal to @@ -544,7 +545,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { )} - {activeLicenseV3?.state === LicenseState.CANCELLED && ( + {activeLicense?.state === LicenseState.CANCELLED && ( <> Your SigNoz license is cancelled. Please contact support at{' '} support@signoz.io for reactivation @@ -559,7 +560,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { )} - {activeLicenseV3?.state === LicenseState.DEFAULTED && ( + {activeLicense?.state === LicenseState.DEFAULTED && ( <> Your SigNoz license is defaulted. Please clear the bill to continue using the enterprise features. Contact support at{' '} @@ -575,7 +576,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { )} - {activeLicenseV3?.state === LicenseState.EVALUATION_EXPIRED && ( + {activeLicense?.state === LicenseState.EVALUATION_EXPIRED && ( <> Your SigNoz trial has ended. Please contact support at{' '} support@signoz.io for next steps to @@ -624,7 +625,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { Your bill payment has failed. Your workspace will get suspended on{' '} {getFormattedDateWithMinutes( - dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() || Date.now(), + dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(), )} . diff --git a/frontend/src/container/BillingContainer/BillingContainer.tsx b/frontend/src/container/BillingContainer/BillingContainer.tsx index 9906cace1e..e2f4bc847f 100644 --- a/frontend/src/container/BillingContainer/BillingContainer.tsx +++ b/frontend/src/container/BillingContainer/BillingContainer.tsx @@ -16,10 +16,10 @@ import { Typography, } from 'antd'; import { ColumnsType } from 'antd/es/table'; -import updateCreditCardApi from 'api/billing/checkout'; import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage'; -import manageCreditCardApi from 'api/billing/manage'; import logEvent from 'api/common/logEvent'; +import updateCreditCardApi from 'api/v1/checkout/create'; +import manageCreditCardApi from 'api/v1/portal/create'; import Spinner from 'components/Spinner'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; @@ -31,9 +31,8 @@ import { useAppContext } from 'providers/App/App'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQuery } from 'react-query'; -import { ErrorResponse, SuccessResponse } from 'types/api'; +import { SuccessResponseV2 } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; -import { License } from 'types/api/licenses/def'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph'; @@ -126,7 +125,6 @@ export default function BillingContainer(): JSX.Element { const daysRemainingStr = t('days_remaining'); const [headerText, setHeaderText] = useState(''); const [billAmount, setBillAmount] = useState(0); - const [activeLicense, setActiveLicense] = useState(null); const [daysRemaining, setDaysRemaining] = useState(0); const [isFreeTrial, setIsFreeTrial] = useState(false); const [data, setData] = useState([]); @@ -137,11 +135,10 @@ export default function BillingContainer(): JSX.Element { const { user, org, - licenses, trialInfo, - isFetchingActiveLicenseV3, - activeLicenseV3, - activeLicenseV3FetchError, + isFetchingActiveLicense, + activeLicense, + activeLicenseFetchError, } = useAppContext(); const { notifications } = useNotifications(); @@ -216,14 +213,9 @@ export default function BillingContainer(): JSX.Element { }); useEffect(() => { - const activeValidLicense = - licenses?.licenses?.find((license) => license.isCurrent === true) || null; - - setActiveLicense(activeValidLicense); - if ( - !isFetchingActiveLicenseV3 && - !activeLicenseV3FetchError && + !isFetchingActiveLicense && + !activeLicenseFetchError && trialInfo?.onTrial ) { const remainingDays = getRemainingDays(trialInfo?.trialEnd); @@ -238,12 +230,11 @@ export default function BillingContainer(): JSX.Element { ); } }, [ - licenses?.licenses, - activeLicenseV3, + activeLicense, trialInfo?.onTrial, trialInfo?.trialEnd, - isFetchingActiveLicenseV3, - activeLicenseV3FetchError, + isFetchingActiveLicense, + activeLicenseFetchError, ]); const columns: ColumnsType = [ @@ -288,11 +279,11 @@ export default function BillingContainer(): JSX.Element { ); const handleBillingOnSuccess = ( - data: ErrorResponse | SuccessResponse, + data: SuccessResponseV2, ): void => { - if (data?.payload?.redirectURL) { + if (data?.data?.redirectURL) { const newTab = document.createElement('a'); - newTab.href = data.payload.redirectURL; + newTab.href = data.data.redirectURL; newTab.target = '_blank'; newTab.rel = 'noopener noreferrer'; newTab.click(); diff --git a/frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx b/frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx index 82a94f4c75..f9d50e06f6 100644 --- a/frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx +++ b/frontend/src/container/Home/DataSourceInfo/DataSourceInfo.tsx @@ -19,12 +19,12 @@ function DataSourceInfo({ dataSentToSigNoz: boolean; isLoading: boolean; }): JSX.Element { - const { activeLicenseV3 } = useAppContext(); + const { activeLicense } = useAppContext(); const notSendingData = !dataSentToSigNoz; const isEnabled = - activeLicenseV3 && activeLicenseV3.platform === LicensePlatform.CLOUD; + activeLicense && activeLicense.platform === LicensePlatform.CLOUD; const { data: deploymentsData, @@ -88,8 +88,8 @@ function DataSourceInfo({ logEvent('Homepage: Connect dataSource clicked', {}); if ( - activeLicenseV3 && - activeLicenseV3.platform === LicensePlatform.CLOUD + activeLicense && + activeLicense.platform === LicensePlatform.CLOUD ) { history.push(ROUTES.GET_STARTED_WITH_CLOUD); } else { @@ -105,8 +105,8 @@ function DataSourceInfo({ logEvent('Homepage: Connect dataSource clicked', {}); if ( - activeLicenseV3 && - activeLicenseV3.platform === LicensePlatform.CLOUD + activeLicense && + activeLicense.platform === LicensePlatform.CLOUD ) { history.push(ROUTES.GET_STARTED_WITH_CLOUD); } else { diff --git a/frontend/src/container/Home/Home.tsx b/frontend/src/container/Home/Home.tsx index 58446801a7..2150faee7b 100644 --- a/frontend/src/container/Home/Home.tsx +++ b/frontend/src/container/Home/Home.tsx @@ -300,17 +300,17 @@ export default function Home(): JSX.Element { } }, [hostData, k8sPodsData, handleUpdateChecklistDoneItem]); - const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext(); + const { activeLicense, isFetchingActiveLicense } = useAppContext(); const [isEnabled, setIsEnabled] = useState(false); useEffect(() => { - if (isFetchingActiveLicenseV3) { + if (isFetchingActiveLicense) { setIsEnabled(false); return; } - setIsEnabled(Boolean(activeLicenseV3?.platform === LicensePlatform.CLOUD)); - }, [activeLicenseV3, isFetchingActiveLicenseV3]); + setIsEnabled(Boolean(activeLicense?.platform === LicensePlatform.CLOUD)); + }, [activeLicense, isFetchingActiveLicense]); const { data: deploymentsData } = useGetDeploymentsData(isEnabled); diff --git a/frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx b/frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx index 3bf52945d0..6dab3d0752 100644 --- a/frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx +++ b/frontend/src/container/Home/HomeChecklist/HomeChecklist.tsx @@ -32,7 +32,7 @@ function HomeChecklist({ onSkip: (item: ChecklistItem) => void; isLoading: boolean; }): JSX.Element { - const { user, activeLicenseV3 } = useAppContext(); + const { user, activeLicense } = useAppContext(); const [completedChecklistItems, setCompletedChecklistItems] = useState< ChecklistItem[] @@ -94,8 +94,8 @@ function HomeChecklist({ if (item.toRoute !== ROUTES.GET_STARTED_WITH_CLOUD) { history.push(item.toRoute || ''); } else if ( - activeLicenseV3 && - activeLicenseV3.platform === LicensePlatform.CLOUD + activeLicense && + activeLicense.platform === LicensePlatform.CLOUD ) { history.push(item.toRoute || ''); } else { diff --git a/frontend/src/container/Home/Services/ServiceMetrics.tsx b/frontend/src/container/Home/Services/ServiceMetrics.tsx index 7f1baf92f1..d588f9d65d 100644 --- a/frontend/src/container/Home/Services/ServiceMetrics.tsx +++ b/frontend/src/container/Home/Services/ServiceMetrics.tsx @@ -23,7 +23,7 @@ import { Link } from 'react-router-dom'; import { AppState } from 'store/reducers'; import { LicensePlatform, - LicenseV3ResModel, + LicenseResModel, } from 'types/api/licensesV3/getActive'; import { ServicesList } from 'types/api/metrics/getService'; import { GlobalReducer } from 'types/reducer/globalTime'; @@ -42,7 +42,7 @@ const EmptyState = memo( activeLicenseV3, }: { user: IUser; - activeLicenseV3: LicenseV3ResModel | null; + activeLicenseV3: LicenseResModel | null; }): JSX.Element => (
@@ -146,7 +146,7 @@ function ServiceMetrics({ GlobalReducer >((state) => state.globalTime); - const { user, activeLicenseV3 } = useAppContext(); + const { user, activeLicense } = useAppContext(); const [timeRange, setTimeRange] = useState(() => { const now = new Date().getTime(); @@ -335,7 +335,7 @@ function ServiceMetrics({ {servicesExist ? ( ) : ( - + )} diff --git a/frontend/src/container/Home/Services/ServiceTraces.tsx b/frontend/src/container/Home/Services/ServiceTraces.tsx index 23d3f613ba..fd4b0592e1 100644 --- a/frontend/src/container/Home/Services/ServiceTraces.tsx +++ b/frontend/src/container/Home/Services/ServiceTraces.tsx @@ -32,7 +32,7 @@ export default function ServiceTraces({ (state) => state.globalTime, ); - const { user, activeLicenseV3 } = useAppContext(); + const { user, activeLicense } = useAppContext(); const now = new Date().getTime(); const [timeRange, setTimeRange] = useState({ @@ -124,8 +124,8 @@ export default function ServiceTraces({ }); if ( - activeLicenseV3 && - activeLicenseV3.platform === LicensePlatform.CLOUD + activeLicense && + activeLicense.platform === LicensePlatform.CLOUD ) { history.push(ROUTES.GET_STARTED_WITH_CLOUD); } else { @@ -160,7 +160,7 @@ export default function ServiceTraces({
), - [user?.role, activeLicenseV3], + [user?.role, activeLicense], ); const renderDashboardsList = useCallback( diff --git a/frontend/src/container/Licenses/ApplyLicenseForm.tsx b/frontend/src/container/Licenses/ApplyLicenseForm.tsx index 6b6da72660..38d774f549 100644 --- a/frontend/src/container/Licenses/ApplyLicenseForm.tsx +++ b/frontend/src/container/Licenses/ApplyLicenseForm.tsx @@ -1,8 +1,9 @@ import { Button, Form, Input } from 'antd'; -import apply from 'api/licenses/apply'; +import apply from 'api/v3/licenses/put'; import { useNotifications } from 'hooks/useNotifications'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import APIError from 'types/api/error'; import { requireErrorMessage } from 'utils/form/requireErrorMessage'; import { @@ -36,27 +37,18 @@ function ApplyLicenseForm({ setIsLoading(true); try { - const response = await apply({ + await apply({ key: params.key, }); - - if (response.statusCode === 200) { - await Promise.all([licenseRefetch()]); - - notifications.success({ - message: 'Success', - description: t('license_applied'), - }); - } else { - notifications.error({ - message: 'Error', - description: response.error || t('unexpected_error'), - }); - } + await Promise.all([licenseRefetch()]); + notifications.success({ + message: 'Success', + description: t('license_applied'), + }); } catch (e) { notifications.error({ - message: 'Error', - description: t('unexpected_error'), + message: (e as APIError).getErrorCode(), + description: (e as APIError).getErrorMessage(), }); } setIsLoading(false); diff --git a/frontend/src/container/Licenses/ListLicenses.tsx b/frontend/src/container/Licenses/ListLicenses.tsx deleted file mode 100644 index 45c14bd238..0000000000 --- a/frontend/src/container/Licenses/ListLicenses.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Typography } from 'antd'; -import { ColumnsType } from 'antd/lib/table'; -import { ResizeTable } from 'components/ResizeTable'; -import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats'; -import { useTimezone } from 'providers/Timezone'; -import { useTranslation } from 'react-i18next'; -import { License } from 'types/api/licenses/def'; - -function ValidityColumn({ value }: { value: string }): JSX.Element { - const { formatTimezoneAdjustedTimestamp } = useTimezone(); - - return ( - - {formatTimezoneAdjustedTimestamp(value, DATE_TIME_FORMATS.ISO_DATETIME_UTC)} - - ); -} - -function ListLicenses({ licenses }: ListLicensesProps): JSX.Element { - const { t } = useTranslation(['licenses']); - - const columns: ColumnsType = [ - { - title: t('column_license_status'), - dataIndex: 'status', - key: 'status', - width: 100, - }, - { - title: t('column_license_key'), - dataIndex: 'key', - key: 'key', - width: 80, - }, - { - title: t('column_valid_from'), - dataIndex: 'ValidFrom', - key: 'valid from', - render: (value: string): JSX.Element => ValidityColumn({ value }), - width: 80, - }, - { - title: t('column_valid_until'), - dataIndex: 'ValidUntil', - key: 'valid until', - render: (value: string): JSX.Element => ValidityColumn({ value }), - width: 80, - }, - ]; - - return ; -} - -interface ListLicensesProps { - licenses: License[]; -} - -export default ListLicenses; diff --git a/frontend/src/container/Licenses/index.tsx b/frontend/src/container/Licenses/index.tsx index 6eeed645e9..cae9ad566e 100644 --- a/frontend/src/container/Licenses/index.tsx +++ b/frontend/src/container/Licenses/index.tsx @@ -4,29 +4,20 @@ import { useAppContext } from 'providers/App/App'; import { useTranslation } from 'react-i18next'; import ApplyLicenseForm from './ApplyLicenseForm'; -import ListLicenses from './ListLicenses'; function Licenses(): JSX.Element { const { t, ready: translationsReady } = useTranslation(['licenses']); - const { licenses, licensesRefetch } = useAppContext(); + const { activeLicenseRefetch } = useAppContext(); if (!translationsReady) { return ; } - const allValidLicense = - licenses?.licenses?.filter((license) => license.isCurrent) || []; - const tabs = [ { label: t('tab_current_license'), key: 'licenses', - children: , - }, - { - label: t('tab_license_history'), - key: 'history', - children: , + children: , }, ]; diff --git a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx index 4561d19b85..e7fe99e0ae 100644 --- a/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx +++ b/frontend/src/container/ServiceApplication/ServiceMetrics/ServiceMetricTable.tsx @@ -33,7 +33,7 @@ function ServiceMetricTable({ const { notifications } = useNotifications(); const { t: getText } = useTranslation(['services']); - const { isFetchingActiveLicenseV3, trialInfo } = useAppContext(); + const { isFetchingActiveLicense, trialInfo } = useAppContext(); const { isCloudUser: isCloudUserVal } = useGetTenantLicense(); const queries = useGetQueriesRange(queryRangeRequestData, ENTITY_VERSION_V4, { @@ -70,7 +70,7 @@ function ServiceMetricTable({ useEffect(() => { if ( - !isFetchingActiveLicenseV3 && + !isFetchingActiveLicense && trialInfo?.onTrial && !trialInfo?.trialConvertedToSubscription && isCloudUserVal @@ -85,7 +85,7 @@ function ServiceMetricTable({ }, [ services, isCloudUserVal, - isFetchingActiveLicenseV3, + isFetchingActiveLicense, trialInfo?.onTrial, trialInfo?.trialConvertedToSubscription, ]); diff --git a/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx index c579933920..7c4305374f 100644 --- a/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx +++ b/frontend/src/container/ServiceApplication/ServiceTraces/ServiceTracesTable.tsx @@ -21,13 +21,13 @@ function ServiceTraceTable({ const [RPS, setRPS] = useState(0); const { t: getText } = useTranslation(['services']); - const { isFetchingActiveLicenseV3, trialInfo } = useAppContext(); + const { isFetchingActiveLicense, trialInfo } = useAppContext(); const { isCloudUser: isCloudUserVal } = useGetTenantLicense(); const tableColumns = useMemo(() => getColumns(search, false), [search]); useEffect(() => { if ( - !isFetchingActiveLicenseV3 && + !isFetchingActiveLicense && trialInfo?.onTrial && !trialInfo?.trialConvertedToSubscription && isCloudUserVal @@ -42,7 +42,7 @@ function ServiceTraceTable({ }, [ services, isCloudUserVal, - isFetchingActiveLicenseV3, + isFetchingActiveLicense, trialInfo?.onTrial, trialInfo?.trialConvertedToSubscription, ]); diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index eaef32a9d8..2603981f14 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -12,7 +12,7 @@ import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts'; import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys'; import useComponentPermission from 'hooks/useComponentPermission'; import { useGetTenantLicense } from 'hooks/useGetTenantLicense'; -import { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS } from 'hooks/useLicense'; +import { StatusCodes } from 'http-status-codes'; import history from 'lib/history'; import { AlertTriangle, @@ -26,7 +26,6 @@ import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { AppState } from 'store/reducers'; -import { License } from 'types/api/licenses/def'; import AppReducer from 'types/reducer/app'; import { USER_ROLES } from 'types/roles'; import { checkVersionState } from 'utils/app'; @@ -59,7 +58,13 @@ function SideNav(): JSX.Element { AppReducer >((state) => state.app); - const { user, featureFlags, licenses, trialInfo } = useAppContext(); + const { + user, + featureFlags, + trialInfo, + activeLicense, + activeLicenseFetchError, + } = useAppContext(); const isOnboardingV3Enabled = featureFlags?.find( (flag) => flag.name === FeatureKeys.ONBOARDING_V3, @@ -96,14 +101,11 @@ function SideNav(): JSX.Element { const { t } = useTranslation(''); - const licenseStatus: string = - licenses?.licenses?.find((e: License) => e.isCurrent)?.status || ''; + const licenseStatus: string = activeLicense?.status || ''; const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false; - const isLicenseActive = - licenseStatus?.toLocaleLowerCase() === - LICENSE_PLAN_STATUS.VALID.toLocaleLowerCase(); + const isLicenseActive = licenseStatus === 'VALID'; const onClickSignozCloud = (): void => { window.open( @@ -299,10 +301,10 @@ function SideNav(): JSX.Element { } const isOnBasicPlan = - licenses?.licenses?.some( - (license: License) => - license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN, - ) || licenses?.licenses === null; + activeLicenseFetchError && + [StatusCodes.NOT_FOUND, StatusCodes.NOT_IMPLEMENTED].includes( + activeLicenseFetchError?.getHttpStatusCode(), + ); if (user.role !== USER_ROLES.ADMIN || isOnBasicPlan) { updatedMenuItems = updatedMenuItems.filter( @@ -347,10 +349,10 @@ function SideNav(): JSX.Element { isEnterpriseSelfHostedUser, isCurrentVersionError, isLatestVersion, - licenses?.licenses, onClickVersionHandler, t, user.role, + activeLicenseFetchError, ]); return ( @@ -443,7 +445,7 @@ function SideNav(): JSX.Element { onClick={onClickShortcuts} /> - {licenses && !isLicenseActive && ( + {!isLicenseActive && ( useQuery({ @@ -11,9 +12,6 @@ const useActiveLicenseV3 = (isLoggedIn: boolean): UseLicense => enabled: !!isLoggedIn, }); -type UseLicense = UseQueryResult< - SuccessResponse | ErrorResponse, - unknown ->; +type UseLicense = UseQueryResult, APIError>; export default useActiveLicenseV3; diff --git a/frontend/src/hooks/useGetTenantLicense.ts b/frontend/src/hooks/useGetTenantLicense.ts index edc99c32e8..c241e23b4c 100644 --- a/frontend/src/hooks/useGetTenantLicense.ts +++ b/frontend/src/hooks/useGetTenantLicense.ts @@ -1,4 +1,3 @@ -import { AxiosError } from 'axios'; import { useAppContext } from 'providers/App/App'; import { LicensePlatform } from 'types/api/licensesV3/getActive'; @@ -8,26 +7,26 @@ export const useGetTenantLicense = (): { isCommunityUser: boolean; isCommunityEnterpriseUser: boolean; } => { - const { activeLicenseV3, activeLicenseV3FetchError } = useAppContext(); + const { activeLicense, activeLicenseFetchError } = useAppContext(); const responsePayload = { - isCloudUser: activeLicenseV3?.platform === LicensePlatform.CLOUD || false, + isCloudUser: activeLicense?.platform === LicensePlatform.CLOUD || false, isEnterpriseSelfHostedUser: - activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED || false, + activeLicense?.platform === LicensePlatform.SELF_HOSTED || false, isCommunityUser: false, isCommunityEnterpriseUser: false, }; if ( - activeLicenseV3FetchError && - (activeLicenseV3FetchError as AxiosError)?.response?.status === 404 + activeLicenseFetchError && + activeLicenseFetchError.getHttpStatusCode() === 404 ) { responsePayload.isCommunityEnterpriseUser = true; } if ( - activeLicenseV3FetchError && - (activeLicenseV3FetchError as AxiosError)?.response?.status === 501 + activeLicenseFetchError && + activeLicenseFetchError.getHttpStatusCode() === 501 ) { responsePayload.isCommunityUser = true; } diff --git a/frontend/src/hooks/useLicense/constant.ts b/frontend/src/hooks/useLicense/constant.ts deleted file mode 100644 index 71134fc08f..0000000000 --- a/frontend/src/hooks/useLicense/constant.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const LICENSE_PLAN_KEY = { - ENTERPRISE_PLAN: 'ENTERPRISE_PLAN', - BASIC_PLAN: 'BASIC_PLAN', -}; - -export const LICENSE_PLAN_STATUS = { - VALID: 'VALID', -}; diff --git a/frontend/src/hooks/useLicense/index.ts b/frontend/src/hooks/useLicense/index.ts deleted file mode 100644 index 387e93e7d6..0000000000 --- a/frontend/src/hooks/useLicense/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS } from './constant'; -import useLicense from './useLicense'; - -export default useLicense; - -export { LICENSE_PLAN_KEY, LICENSE_PLAN_STATUS }; diff --git a/frontend/src/hooks/useLicense/useLicense.tsx b/frontend/src/hooks/useLicense/useLicense.tsx deleted file mode 100644 index 89d8ded974..0000000000 --- a/frontend/src/hooks/useLicense/useLicense.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import getAll from 'api/licenses/getAll'; -import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; -// import { useAppContext } from 'providers/App/App'; -import { useQuery, UseQueryResult } from 'react-query'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps } from 'types/api/licenses/getAll'; - -const useLicense = (isLoggedIn: boolean): UseLicense => - useQuery({ - queryFn: getAll, - queryKey: [REACT_QUERY_KEY.GET_ALL_LICENCES], - enabled: !!isLoggedIn, - }); - -type UseLicense = UseQueryResult< - SuccessResponse | ErrorResponse, - unknown ->; - -export default useLicense; diff --git a/frontend/src/pages/Support/Support.tsx b/frontend/src/pages/Support/Support.tsx index edc9066d2f..fe6517b70e 100644 --- a/frontend/src/pages/Support/Support.tsx +++ b/frontend/src/pages/Support/Support.tsx @@ -1,9 +1,8 @@ import './Support.styles.scss'; import { Button, Card, Modal, Typography } from 'antd'; -import updateCreditCardApi from 'api/billing/checkout'; import logEvent from 'api/common/logEvent'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; +import updateCreditCardApi from 'api/v1/checkout/create'; import { FeatureKeys } from 'constants/features'; import { useNotifications } from 'hooks/useNotifications'; import { @@ -18,8 +17,9 @@ import { useAppContext } from 'providers/App/App'; import { useEffect, useState } from 'react'; import { useMutation } from 'react-query'; import { useHistory, useLocation } from 'react-router-dom'; -import { ErrorResponse, SuccessResponse } from 'types/api'; +import { SuccessResponseV2 } from 'types/api'; import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; +import APIError from 'types/api/error'; const { Title, Text } = Typography; @@ -109,20 +109,21 @@ export default function Support(): JSX.Element { !isPremiumChatSupportEnabled && !trialInfo?.trialConvertedToSubscription; const handleBillingOnSuccess = ( - data: ErrorResponse | SuccessResponse, + data: SuccessResponseV2, ): void => { - if (data?.payload?.redirectURL) { + if (data?.data?.redirectURL) { const newTab = document.createElement('a'); - newTab.href = data.payload.redirectURL; + newTab.href = data.data.redirectURL; newTab.target = '_blank'; newTab.rel = 'noopener noreferrer'; newTab.click(); } }; - const handleBillingOnError = (): void => { + const handleBillingOnError = (error: APIError): void => { notifications.error({ - message: SOMETHING_WENT_WRONG, + message: error.getErrorCode(), + description: error.getErrorMessage(), }); }; diff --git a/frontend/src/pages/WorkspaceAccessRestricted/WorkspaceAccessRestricted.tsx b/frontend/src/pages/WorkspaceAccessRestricted/WorkspaceAccessRestricted.tsx index b7e0abd3ee..a251e4c034 100644 --- a/frontend/src/pages/WorkspaceAccessRestricted/WorkspaceAccessRestricted.tsx +++ b/frontend/src/pages/WorkspaceAccessRestricted/WorkspaceAccessRestricted.tsx @@ -8,24 +8,24 @@ import { useEffect } from 'react'; import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive'; function WorkspaceAccessRestricted(): JSX.Element { - const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext(); + const { activeLicense, isFetchingActiveLicense } = useAppContext(); useEffect(() => { - if (!isFetchingActiveLicenseV3) { - const isTerminated = activeLicenseV3?.state === LicenseState.TERMINATED; - const isExpired = activeLicenseV3?.state === LicenseState.EXPIRED; - const isCancelled = activeLicenseV3?.state === LicenseState.CANCELLED; + if (!isFetchingActiveLicense) { + const isTerminated = activeLicense?.state === LicenseState.TERMINATED; + const isExpired = activeLicense?.state === LicenseState.EXPIRED; + const isCancelled = activeLicense?.state === LicenseState.CANCELLED; const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled; if ( !isWorkspaceAccessRestricted || - activeLicenseV3.platform === LicensePlatform.SELF_HOSTED + activeLicense.platform === LicensePlatform.SELF_HOSTED ) { history.push(ROUTES.HOME); } } - }, [isFetchingActiveLicenseV3, activeLicenseV3]); + }, [isFetchingActiveLicense, activeLicense]); return (
@@ -44,7 +44,7 @@ function WorkspaceAccessRestricted(): JSX.Element { width="65%" >
- {isFetchingActiveLicenseV3 || !activeLicenseV3 ? ( + {isFetchingActiveLicense || !activeLicense ? ( ) : ( <> @@ -55,7 +55,7 @@ function WorkspaceAccessRestricted(): JSX.Element { level={4} className="workspace-access-restricted__details" > - {activeLicenseV3.state === LicenseState.TERMINATED && ( + {activeLicense.state === LicenseState.TERMINATED && ( <> Your SigNoz license is terminated, please contact support at{' '} @@ -64,7 +64,7 @@ function WorkspaceAccessRestricted(): JSX.Element { for a new deployment )} - {activeLicenseV3.state === LicenseState.EXPIRED && ( + {activeLicense.state === LicenseState.EXPIRED && ( <> Your SigNoz license is expired, please contact support at{' '} @@ -81,7 +81,7 @@ function WorkspaceAccessRestricted(): JSX.Element { . )} - {activeLicenseV3.state === LicenseState.CANCELLED && ( + {activeLicense.state === LicenseState.CANCELLED && ( <> Your SigNoz license is cancelled, please contact support at{' '} diff --git a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx index c0cd0877ba..dc680983ce 100644 --- a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx +++ b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx @@ -16,8 +16,8 @@ import { Tabs, Typography, } from 'antd'; -import updateCreditCardApi from 'api/billing/checkout'; import logEvent from 'api/common/logEvent'; +import updateCreditCardApi from 'api/v1/checkout/create'; import ROUTES from 'constants/routes'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; @@ -26,6 +26,7 @@ import { useAppContext } from 'providers/App/App'; import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; +import APIError from 'types/api/error'; import { LicensePlatform } from 'types/api/licensesV3/getActive'; import { getFormattedDate } from 'utils/timeUtils'; @@ -41,9 +42,9 @@ import { export default function WorkspaceBlocked(): JSX.Element { const { user, - isFetchingActiveLicenseV3, + isFetchingActiveLicense, trialInfo, - activeLicenseV3, + activeLicense, } = useAppContext(); const isAdmin = user.role === 'ADMIN'; const { notifications } = useNotifications(); @@ -70,37 +71,38 @@ export default function WorkspaceBlocked(): JSX.Element { }; useEffect(() => { - if (!isFetchingActiveLicenseV3) { + if (!isFetchingActiveLicense) { const shouldBlockWorkspace = trialInfo?.workSpaceBlock; if ( !shouldBlockWorkspace || - activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED + activeLicense?.platform === LicensePlatform.SELF_HOSTED ) { history.push(ROUTES.HOME); } } }, [ - isFetchingActiveLicenseV3, + isFetchingActiveLicense, trialInfo?.workSpaceBlock, - activeLicenseV3?.platform, + activeLicense?.platform, ]); const { mutate: updateCreditCard, isLoading } = useMutation( updateCreditCardApi, { onSuccess: (data) => { - if (data.payload?.redirectURL) { + if (data.data?.redirectURL) { const newTab = document.createElement('a'); - newTab.href = data.payload.redirectURL; + newTab.href = data.data.redirectURL; newTab.target = '_blank'; newTab.rel = 'noopener noreferrer'; newTab.click(); } }, - onError: () => + onError: (error: APIError) => notifications.error({ - message: t('somethingWentWrong'), + message: error.getErrorCode(), + description: error.getErrorMessage(), }), }, ); @@ -320,7 +322,7 @@ export default function WorkspaceBlocked(): JSX.Element { width="65%" >
- {isFetchingActiveLicenseV3 || !trialInfo ? ( + {isFetchingActiveLicense || !trialInfo ? ( ) : ( <> diff --git a/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx b/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx index 4671527b10..3633eb7135 100644 --- a/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx +++ b/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx @@ -10,7 +10,7 @@ import { Space, Typography, } from 'antd'; -import manageCreditCardApi from 'api/billing/manage'; +import manageCreditCardApi from 'api/v1/portal/create'; import ROUTES from 'constants/routes'; import dayjs from 'dayjs'; import { useNotifications } from 'hooks/useNotifications'; @@ -19,6 +19,7 @@ import { useAppContext } from 'providers/App/App'; import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; +import APIError from 'types/api/error'; import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive'; import { getFormattedDateWithMinutes } from 'utils/timeUtils'; @@ -26,7 +27,7 @@ function WorkspaceSuspended(): JSX.Element { const { user } = useAppContext(); const isAdmin = user.role === 'ADMIN'; const { notifications } = useNotifications(); - const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext(); + const { activeLicense, isFetchingActiveLicense } = useAppContext(); const { t } = useTranslation(['failedPayment']); @@ -34,17 +35,18 @@ function WorkspaceSuspended(): JSX.Element { manageCreditCardApi, { onSuccess: (data) => { - if (data.payload?.redirectURL) { + if (data.data?.redirectURL) { const newTab = document.createElement('a'); - newTab.href = data.payload.redirectURL; + newTab.href = data.data.redirectURL; newTab.target = '_blank'; newTab.rel = 'noopener noreferrer'; newTab.click(); } }, - onError: () => + onError: (error: APIError) => notifications.error({ - message: t('somethingWentWrong'), + message: error.getErrorCode(), + description: error.getErrorMessage(), }), }, ); @@ -56,18 +58,18 @@ function WorkspaceSuspended(): JSX.Element { }, [manageCreditCard]); useEffect(() => { - if (!isFetchingActiveLicenseV3) { + if (!isFetchingActiveLicense) { const shouldSuspendWorkspace = - activeLicenseV3?.state === LicenseState.DEFAULTED; + activeLicense?.state === LicenseState.DEFAULTED; if ( !shouldSuspendWorkspace || - activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED + activeLicense?.platform === LicensePlatform.SELF_HOSTED ) { history.push(ROUTES.HOME); } } - }, [isFetchingActiveLicenseV3, activeLicenseV3]); + }, [isFetchingActiveLicense, activeLicense]); return (
- {isFetchingActiveLicenseV3 || !activeLicenseV3 ? ( + {isFetchingActiveLicense || !activeLicense ? ( ) : ( <> @@ -115,7 +117,7 @@ function WorkspaceSuspended(): JSX.Element { {t('yourDataIsSafe')}{' '} {getFormattedDateWithMinutes( - dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() || + dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(), )} {' '} diff --git a/frontend/src/providers/App/App.tsx b/frontend/src/providers/App/App.tsx index 58d4c682f7..8f84ee0184 100644 --- a/frontend/src/providers/App/App.tsx +++ b/frontend/src/providers/App/App.tsx @@ -6,7 +6,6 @@ import dayjs from 'dayjs'; import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3'; import useGetFeatureFlag from 'hooks/useGetFeatureFlag'; import { useGlobalEventListener } from 'hooks/useGlobalEventListener'; -import useLicense from 'hooks/useLicense'; import useGetUser from 'hooks/user/useGetUser'; import { createContext, @@ -19,11 +18,10 @@ import { } from 'react'; import { useQuery } from 'react-query'; import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags'; -import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll'; import { LicensePlatform, + LicenseResModel, LicenseState, - LicenseV3ResModel, TrialInfo, } from 'types/api/licensesV3/getActive'; import { Organization } from 'types/api/user/getOrganization'; @@ -38,14 +36,10 @@ export const AppContext = createContext(undefined); 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 const [user, setUser] = useState(() => getUserDefaults()); - const [licenses, setLicenses] = useState(null); - const [ - activeLicenseV3, - setActiveLicenseV3, - ] = useState(null); - + const [activeLicense, setActiveLicense] = useState( + null, + ); const [trialInfo, setTrialInfo] = useState(null); - const [featureFlags, setFeatureFlags] = useState(null); const [orgPreferences, setOrgPreferences] = useState( null, @@ -103,59 +97,40 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element { } }, [userData, isFetchingUser]); - // fetcher for licenses v2 - // license will be fetched if we are in logged in state - const { - data: licenseData, - isFetching: isFetchingLicenses, - error: licensesFetchError, - refetch: licensesRefetch, - } = useLicense(isLoggedIn); - useEffect(() => { - if (!isFetchingLicenses && licenseData && licenseData.payload) { - setLicenses(licenseData.payload); - } - }, [licenseData, isFetchingLicenses]); - // fetcher for licenses v3 const { - data: activeLicenseV3Data, - isFetching: isFetchingActiveLicenseV3, - error: activeLicenseV3FetchError, + data: activeLicenseData, + isFetching: isFetchingActiveLicense, + error: activeLicenseFetchError, + refetch: activeLicenseRefetch, } = useActiveLicenseV3(isLoggedIn); useEffect(() => { - if ( - !isFetchingActiveLicenseV3 && - activeLicenseV3Data && - activeLicenseV3Data.payload - ) { - setActiveLicenseV3(activeLicenseV3Data.payload); + if (!isFetchingActiveLicense && activeLicenseData && activeLicenseData.data) { + setActiveLicense(activeLicenseData.data); const isOnTrial = dayjs( - activeLicenseV3Data.payload.free_until || Date.now(), + activeLicenseData.data.free_until || Date.now(), ).isAfter(dayjs()); const trialInfo: TrialInfo = { - trialStart: activeLicenseV3Data.payload.valid_from, - trialEnd: dayjs( - activeLicenseV3Data.payload.free_until || Date.now(), - ).unix(), + trialStart: activeLicenseData.data.valid_from, + trialEnd: dayjs(activeLicenseData.data.free_until || Date.now()).unix(), onTrial: isOnTrial, workSpaceBlock: - activeLicenseV3Data.payload.state === LicenseState.EVALUATION_EXPIRED && - activeLicenseV3Data.payload.platform === LicensePlatform.CLOUD, + activeLicenseData.data.state === LicenseState.EVALUATION_EXPIRED && + activeLicenseData.data.platform === LicensePlatform.CLOUD, trialConvertedToSubscription: - activeLicenseV3Data.payload.state !== LicenseState.ISSUED && - activeLicenseV3Data.payload.state !== LicenseState.EVALUATING && - activeLicenseV3Data.payload.state !== LicenseState.EVALUATION_EXPIRED, + activeLicenseData.data.state !== LicenseState.ISSUED && + activeLicenseData.data.state !== LicenseState.EVALUATING && + activeLicenseData.data.state !== LicenseState.EVALUATION_EXPIRED, gracePeriodEnd: dayjs( - activeLicenseV3Data.payload.event_queue.scheduled_at || Date.now(), + activeLicenseData.data.event_queue.scheduled_at || Date.now(), ).unix(), }; setTrialInfo(trialInfo); } - }, [activeLicenseV3Data, isFetchingActiveLicenseV3]); + }, [activeLicenseData, isFetchingActiveLicense]); // fetcher for feature flags const { @@ -242,9 +217,8 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element { useGlobalEventListener('LOGOUT', () => { setIsLoggedIn(false); setUser(getUserDefaults()); - setActiveLicenseV3(null); + setActiveLicense(null); setTrialInfo(null); - setLicenses(null); setFeatureFlags(null); setOrgPreferences(null); setOrg(null); @@ -254,46 +228,40 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element { const value: IAppContext = useMemo( () => ({ user, - licenses, - activeLicenseV3, + activeLicense, featureFlags, trialInfo, orgPreferences, isLoggedIn, org, isFetchingUser, - isFetchingLicenses, - isFetchingActiveLicenseV3, + isFetchingActiveLicense, isFetchingFeatureFlags, isFetchingOrgPreferences, userFetchError, - licensesFetchError, - activeLicenseV3FetchError, + activeLicenseFetchError, featureFlagsFetchError, orgPreferencesFetchError, - licensesRefetch, + activeLicenseRefetch, updateUser, updateOrgPreferences, updateOrg, }), [ trialInfo, - activeLicenseV3, - activeLicenseV3FetchError, + activeLicense, + activeLicenseFetchError, featureFlags, featureFlagsFetchError, - isFetchingActiveLicenseV3, + isFetchingActiveLicense, isFetchingFeatureFlags, - isFetchingLicenses, isFetchingOrgPreferences, isFetchingUser, isLoggedIn, - licenses, - licensesFetchError, - licensesRefetch, org, orgPreferences, orgPreferencesFetchError, + activeLicenseRefetch, updateOrg, user, userFetchError, diff --git a/frontend/src/providers/App/types.ts b/frontend/src/providers/App/types.ts index 8c9d2117dc..4dd5b31bf9 100644 --- a/frontend/src/providers/App/types.ts +++ b/frontend/src/providers/App/types.ts @@ -1,30 +1,27 @@ +import APIError from 'types/api/error'; import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags'; -import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll'; -import { LicenseV3ResModel, TrialInfo } from 'types/api/licensesV3/getActive'; +import { LicenseResModel, TrialInfo } from 'types/api/licensesV3/getActive'; import { Organization } from 'types/api/user/getOrganization'; import { UserResponse as User } from 'types/api/user/getUser'; import { OrgPreference } from 'types/reducer/app'; export interface IAppContext { user: IUser; - licenses: LicensesResModel | null; - activeLicenseV3: LicenseV3ResModel | null; + activeLicense: LicenseResModel | null; trialInfo: TrialInfo | null; featureFlags: FeatureFlags[] | null; orgPreferences: OrgPreference[] | null; isLoggedIn: boolean; org: Organization[] | null; isFetchingUser: boolean; - isFetchingLicenses: boolean; - isFetchingActiveLicenseV3: boolean; + isFetchingActiveLicense: boolean; isFetchingFeatureFlags: boolean; isFetchingOrgPreferences: boolean; userFetchError: unknown; - licensesFetchError: unknown; - activeLicenseV3FetchError: unknown; + activeLicenseFetchError: APIError | null; featureFlagsFetchError: unknown; orgPreferencesFetchError: unknown; - licensesRefetch: () => void; + activeLicenseRefetch: () => void; updateUser: (user: IUser) => void; updateOrgPreferences: (orgPreferences: OrgPreference[]) => void; updateOrg(orgId: string, updatedOrgName: string): void; diff --git a/frontend/src/tests/test-utils.tsx b/frontend/src/tests/test-utils.tsx index 71f253afe5..cdbe9bdd6d 100644 --- a/frontend/src/tests/test-utils.tsx +++ b/frontend/src/tests/test-utils.tsx @@ -105,7 +105,8 @@ export function getAppContextMock( appContextOverrides?: Partial, ): IAppContext { return { - activeLicenseV3: { + activeLicense: { + key: 'test-key', event_queue: { created_at: '0', event: LicenseEvent.NO_EVENT, @@ -138,8 +139,8 @@ export function getAppContextMock( trialConvertedToSubscription: false, gracePeriodEnd: -1, }, - isFetchingActiveLicenseV3: false, - activeLicenseV3FetchError: null, + isFetchingActiveLicense: false, + activeLicenseFetchError: null, user: { accessJwt: 'some-token', refreshJwt: 'some-refresh-token', @@ -160,20 +161,6 @@ export function getAppContextMock( ], isFetchingUser: false, userFetchError: null, - licenses: { - licenses: [ - { - key: 'does-not-matter', - isCurrent: true, - planKey: 'ENTERPRISE_PLAN', - ValidFrom: new Date(), - ValidUntil: new Date(), - status: 'VALID', - }, - ], - }, - isFetchingLicenses: false, - licensesFetchError: null, featureFlags: [ { name: FeatureKeys.SSO, @@ -246,7 +233,7 @@ export function getAppContextMock( updateUser: jest.fn(), updateOrg: jest.fn(), updateOrgPreferences: jest.fn(), - licensesRefetch: jest.fn(), + activeLicenseRefetch: jest.fn(), ...appContextOverrides, }; } diff --git a/frontend/src/types/api/billing/checkout.ts b/frontend/src/types/api/billing/checkout.ts index 78523376f0..4b1a2311ca 100644 --- a/frontend/src/types/api/billing/checkout.ts +++ b/frontend/src/types/api/billing/checkout.ts @@ -5,3 +5,8 @@ export interface CheckoutSuccessPayloadProps { export interface CheckoutRequestPayloadProps { url: string; } + +export interface PayloadProps { + data: CheckoutSuccessPayloadProps; + status: string; +} diff --git a/frontend/src/types/api/licenses/getAll.ts b/frontend/src/types/api/licenses/getAll.ts deleted file mode 100644 index 58996cf36e..0000000000 --- a/frontend/src/types/api/licenses/getAll.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { License } from './def'; - -export type PayloadProps = { - licenses: License[]; -}; diff --git a/frontend/src/types/api/licensesV3/getActive.ts b/frontend/src/types/api/licensesV3/getActive.ts index b073438bad..a26d766064 100644 --- a/frontend/src/types/api/licensesV3/getActive.ts +++ b/frontend/src/types/api/licensesV3/getActive.ts @@ -30,7 +30,7 @@ export const LicensePlanKey = { BASIC: 'BASIC', }; -export type LicenseV3EventQueueResModel = { +export type LicenseEventQueueResModel = { event: LicenseEvent; status: string; scheduled_at: string; @@ -38,10 +38,11 @@ export type LicenseV3EventQueueResModel = { updated_at: string; }; -export type LicenseV3ResModel = { +export type LicenseResModel = { + key: string; status: LicenseStatus; state: LicenseState; - event_queue: LicenseV3EventQueueResModel; + event_queue: LicenseEventQueueResModel; platform: LicensePlatform; created_at: string; plan: { @@ -67,3 +68,8 @@ export type TrialInfo = { trialConvertedToSubscription: boolean; gracePeriodEnd: number; }; + +export interface PayloadProps { + data: LicenseEventQueueResModel; + status: string; +} diff --git a/pkg/licensing/config.go b/pkg/licensing/config.go new file mode 100644 index 0000000000..a88480d867 --- /dev/null +++ b/pkg/licensing/config.go @@ -0,0 +1,18 @@ +package licensing + +import ( + "time" + + "github.com/SigNoz/signoz/pkg/factory" +) + +var _ factory.Config = (*Config)(nil) + +type Config struct { + PollInterval time.Duration `mapstructure:"poll_interval"` + FailureThreshold int `mapstructure:"failure_threshold"` +} + +func (c Config) Validate() error { + return nil +} diff --git a/pkg/licensing/licensing.go b/pkg/licensing/licensing.go new file mode 100644 index 0000000000..0e0196650f --- /dev/null +++ b/pkg/licensing/licensing.go @@ -0,0 +1,55 @@ +package licensing + +import ( + "context" + "net/http" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/types/featuretypes" + "github.com/SigNoz/signoz/pkg/types/licensetypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +var ( + ErrCodeUnsupported = errors.MustNewCode("licensing_unsupported") + ErrCodeFeatureUnavailable = errors.MustNewCode("feature_unavailable") +) + +type Licensing interface { + factory.Service + + // Validate validates the license with the upstream server + Validate(ctx context.Context) error + // Activate validates and enables the license + Activate(ctx context.Context, organizationID valuer.UUID, key string) error + // GetActive fetches the current active license in org + GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) + // Refresh refreshes the license state from upstream server + Refresh(ctx context.Context, organizationID valuer.UUID) error + // Checkout creates a checkout session via upstream server and returns the redirection link + Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) + // Portal creates a portal session via upstream server and return the redirection link + Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) + + // feature surrogate + // CheckFeature checks if the feature is active or not + CheckFeature(ctx context.Context, key string) error + // GetFeatureFlags fetches all the defined feature flags + GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) + // GetFeatureFlags fetches all the defined feature flags + GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) + // InitFeatures initialises the feature flags + InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error + // UpdateFeatureFlag updates the feature flag + UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error +} + +type API interface { + Activate(http.ResponseWriter, *http.Request) + Refresh(http.ResponseWriter, *http.Request) + GetActive(http.ResponseWriter, *http.Request) + + Checkout(http.ResponseWriter, *http.Request) + Portal(http.ResponseWriter, *http.Request) +} diff --git a/pkg/licensing/nooplicensing/api.go b/pkg/licensing/nooplicensing/api.go new file mode 100644 index 0000000000..e484376fd5 --- /dev/null +++ b/pkg/licensing/nooplicensing/api.go @@ -0,0 +1,35 @@ +package nooplicensing + +import ( + "net/http" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/licensing" +) + +type noopLicensingAPI struct{} + +func NewLicenseAPI() licensing.API { + return &noopLicensingAPI{} +} + +func (api *noopLicensingAPI) Activate(rw http.ResponseWriter, r *http.Request) { + render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented")) +} + +func (api *noopLicensingAPI) GetActive(rw http.ResponseWriter, r *http.Request) { + render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented")) +} + +func (api *noopLicensingAPI) Refresh(rw http.ResponseWriter, r *http.Request) { + render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented")) +} + +func (api *noopLicensingAPI) Checkout(rw http.ResponseWriter, r *http.Request) { + render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented")) +} + +func (api *noopLicensingAPI) Portal(rw http.ResponseWriter, r *http.Request) { + render.Error(rw, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "not implemented")) +} diff --git a/pkg/licensing/nooplicensing/provider.go b/pkg/licensing/nooplicensing/provider.go new file mode 100644 index 0000000000..0e509615f2 --- /dev/null +++ b/pkg/licensing/nooplicensing/provider.go @@ -0,0 +1,99 @@ +package nooplicensing + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/licensing" + "github.com/SigNoz/signoz/pkg/types/featuretypes" + "github.com/SigNoz/signoz/pkg/types/licensetypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type noopLicensing struct { + stopChan chan struct{} +} + +func NewFactory() factory.ProviderFactory[licensing.Licensing, licensing.Config] { + return factory.NewProviderFactory(factory.MustNewName("noop"), func(ctx context.Context, providerSettings factory.ProviderSettings, config licensing.Config) (licensing.Licensing, error) { + return New(ctx, providerSettings, config) + }) +} + +func New(_ context.Context, _ factory.ProviderSettings, _ licensing.Config) (licensing.Licensing, error) { + return &noopLicensing{stopChan: make(chan struct{})}, nil +} + +func (provider *noopLicensing) Start(context.Context) error { + <-provider.stopChan + return nil + +} + +func (provider *noopLicensing) Stop(context.Context) error { + close(provider.stopChan) + return nil +} + +func (provider *noopLicensing) Activate(ctx context.Context, organizationID valuer.UUID, key string) error { + return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching license is not supported") +} + +func (provider *noopLicensing) Validate(ctx context.Context) error { + return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "validating license is not supported") +} + +func (provider *noopLicensing) Refresh(ctx context.Context, organizationID valuer.UUID) error { + return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "refreshing license is not supported") +} + +func (provider *noopLicensing) Checkout(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) { + return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "checkout session is not supported") +} + +func (provider *noopLicensing) Portal(ctx context.Context, organizationID valuer.UUID, postableSubscription *licensetypes.PostableSubscription) (*licensetypes.GettableSubscription, error) { + return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "portal session is not supported") +} + +func (provider *noopLicensing) GetActive(ctx context.Context, organizationID valuer.UUID) (*licensetypes.License, error) { + return nil, errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "fetching active license is not supported") +} + +func (provider *noopLicensing) CheckFeature(ctx context.Context, key string) error { + feature, err := provider.GetFeatureFlag(ctx, key) + if err != nil { + return err + } + + if feature.Active { + return nil + } + + return errors.Newf(errors.TypeNotFound, licensing.ErrCodeFeatureUnavailable, "feature unavailable: %s", key) +} + +func (provider *noopLicensing) GetFeatureFlag(ctx context.Context, key string) (*featuretypes.GettableFeature, error) { + features, err := provider.GetFeatureFlags(ctx) + if err != nil { + return nil, err + } + for _, feature := range features { + if feature.Name == key { + return feature, nil + } + } + return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no feature available with given key: %s", key) +} + +func (provider *noopLicensing) GetFeatureFlags(ctx context.Context) ([]*featuretypes.GettableFeature, error) { + return licensetypes.DefaultFeatureSet, nil +} + +func (provider *noopLicensing) InitFeatures(ctx context.Context, features []*featuretypes.GettableFeature) error { + return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "init features is not supported") +} + +func (provider *noopLicensing) UpdateFeatureFlag(ctx context.Context, feature *featuretypes.GettableFeature) error { + return errors.New(errors.TypeUnsupported, licensing.ErrCodeUnsupported, "updating feature flag is not supported") +} diff --git a/pkg/modules/user/impluser/handler.go b/pkg/modules/user/impluser/handler.go index cfc867d87a..66d43e2b1d 100644 --- a/pkg/modules/user/impluser/handler.go +++ b/pkg/modules/user/impluser/handler.go @@ -92,7 +92,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) { return } - _, err = h.module.CreateBulkInvite(ctx, claims.OrgID, claims.UserID, &types.PostableBulkInviteRequest{ + invites, err := h.module.CreateBulkInvite(ctx, claims.OrgID, claims.UserID, &types.PostableBulkInviteRequest{ Invites: []types.PostableInvite{req}, }) if err != nil { @@ -100,7 +100,7 @@ func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) { return } - render.Success(rw, http.StatusCreated, nil) + render.Success(rw, http.StatusCreated, invites[0]) } func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) { diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index 15a1882e1c..4caad03633 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -23,6 +23,7 @@ import ( errorsV2 "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/http/middleware" "github.com/SigNoz/signoz/pkg/http/render" + "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/quickfilter" "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations/services" "github.com/SigNoz/signoz/pkg/query-service/app/integrations" @@ -58,6 +59,7 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/postprocess" "github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/featuretypes" "github.com/SigNoz/signoz/pkg/types/pipelinetypes" ruletypes "github.com/SigNoz/signoz/pkg/types/ruletypes" @@ -89,7 +91,6 @@ func NewRouter() *mux.Router { type APIHandler struct { reader interfaces.Reader ruleManager *rules.Manager - featureFlags interfaces.FeatureLookup querier interfaces.Querier querierV2 interfaces.Querier queryBuilder *queryBuilder.QueryBuilder @@ -136,6 +137,8 @@ type APIHandler struct { AlertmanagerAPI *alertmanager.API + LicensingAPI licensing.API + FieldsAPI *fields.API Signoz *signoz.SigNoz @@ -155,9 +158,6 @@ type APIHandlerOpts struct { // rule manager handles rule crud operations RuleManager *rules.Manager - // feature flags querier - FeatureFlags interfaces.FeatureLookup - // Integrations IntegrationsController *integrations.Controller @@ -177,6 +177,8 @@ type APIHandlerOpts struct { AlertmanagerAPI *alertmanager.API + LicensingAPI licensing.API + FieldsAPI *fields.API Signoz *signoz.SigNoz @@ -224,7 +226,6 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { preferSpanMetrics: opts.PreferSpanMetrics, temporalityMap: make(map[string]map[v3.Temporality]bool), ruleManager: opts.RuleManager, - featureFlags: opts.FeatureFlags, IntegrationsController: opts.IntegrationsController, CloudIntegrationsController: opts.CloudIntegrationsController, LogsParsingPipelineController: opts.LogsParsingPipelineController, @@ -244,6 +245,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) { JWT: opts.JWT, SummaryService: summaryService, AlertmanagerAPI: opts.AlertmanagerAPI, + LicensingAPI: opts.LicensingAPI, Signoz: opts.Signoz, FieldsAPI: opts.FieldsAPI, QuickFilters: opts.QuickFilters, @@ -607,7 +609,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) { render.Success(rw, http.StatusOK, []any{}) })).Methods(http.MethodGet) router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(func(rw http.ResponseWriter, req *http.Request) { - render.Error(rw, errorsV2.New(errorsV2.TypeUnsupported, errorsV2.CodeUnsupported, "not implemented")) + aH.LicensingAPI.Activate(rw, req) })).Methods(http.MethodGet) } @@ -1979,15 +1981,14 @@ func (aH *APIHandler) getVersion(w http.ResponseWriter, r *http.Request) { } func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { - featureSet, err := aH.FF().GetFeatureFlags() + featureSet, err := aH.Signoz.Licensing.GetFeatureFlags(r.Context()) if err != nil { aH.HandleError(w, err, http.StatusInternalServerError) return } if aH.preferSpanMetrics { - for idx := range featureSet { - feature := &featureSet[idx] - if feature.Name == model.UseSpanMetrics { + for idx, feature := range featureSet { + if feature.Name == featuretypes.UseSpanMetrics { featureSet[idx].Active = true } } @@ -1995,12 +1996,8 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) { aH.Respond(w, featureSet) } -func (aH *APIHandler) FF() interfaces.FeatureLookup { - return aH.featureFlags -} - -func (aH *APIHandler) CheckFeature(f string) bool { - err := aH.FF().CheckFeature(f) +func (aH *APIHandler) CheckFeature(ctx context.Context, key string) bool { + err := aH.Signoz.Licensing.CheckFeature(ctx, key) return err == nil } diff --git a/pkg/query-service/app/queryBuilder/query_builder.go b/pkg/query-service/app/queryBuilder/query_builder.go index f49a046937..2a9aa2a5e5 100644 --- a/pkg/query-service/app/queryBuilder/query_builder.go +++ b/pkg/query-service/app/queryBuilder/query_builder.go @@ -8,7 +8,6 @@ import ( "github.com/SigNoz/signoz/pkg/cache" metricsV3 "github.com/SigNoz/signoz/pkg/query-service/app/metrics/v3" "github.com/SigNoz/signoz/pkg/query-service/constants" - "github.com/SigNoz/signoz/pkg/query-service/interfaces" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" "go.uber.org/zap" ) @@ -46,8 +45,7 @@ type prepareLogsQueryFunc func(start, end int64, queryType v3.QueryType, panelTy type prepareMetricQueryFunc func(start, end int64, queryType v3.QueryType, panelType v3.PanelType, bq *v3.BuilderQuery, options metricsV3.Options) (string, error) type QueryBuilder struct { - options QueryBuilderOptions - featureFlags interfaces.FeatureLookup + options QueryBuilderOptions } type QueryBuilderOptions struct { diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 0ccadb1e8c..08a26c8799 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -14,6 +14,7 @@ import ( "github.com/SigNoz/signoz/pkg/alertmanager" "github.com/SigNoz/signoz/pkg/apis/fields" "github.com/SigNoz/signoz/pkg/http/middleware" + "github.com/SigNoz/signoz/pkg/licensing/nooplicensing" "github.com/SigNoz/signoz/pkg/modules/quickfilter" quickfilterscore "github.com/SigNoz/signoz/pkg/modules/quickfilter/core" "github.com/SigNoz/signoz/pkg/prometheus" @@ -34,7 +35,6 @@ import ( "github.com/SigNoz/signoz/pkg/cache" "github.com/SigNoz/signoz/pkg/query-service/constants" - "github.com/SigNoz/signoz/pkg/query-service/featureManager" "github.com/SigNoz/signoz/pkg/query-service/healthcheck" "github.com/SigNoz/signoz/pkg/query-service/interfaces" "github.com/SigNoz/signoz/pkg/query-service/rules" @@ -81,8 +81,6 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status { // NewServer creates and initializes Server func NewServer(serverOptions *ServerOptions) (*Server, error) { - // initiate feature manager - fm := featureManager.StartManager() fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail) if err != nil { @@ -146,13 +144,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { Reader: reader, PreferSpanMetrics: serverOptions.PreferSpanMetrics, RuleManager: rm, - FeatureFlags: fm, IntegrationsController: integrationsController, CloudIntegrationsController: cloudIntegrationsController, LogsParsingPipelineController: logParsingPipelineController, FluxInterval: fluxInterval, JWT: serverOptions.Jwt, AlertmanagerAPI: alertmanager.NewAPI(serverOptions.SigNoz.Alertmanager), + LicensingAPI: nooplicensing.NewLicenseAPI(), FieldsAPI: fields.NewAPI(serverOptions.SigNoz.TelemetryStore), Signoz: serverOptions.SigNoz, QuickFilters: quickFilter, diff --git a/pkg/query-service/constants/constants.go b/pkg/query-service/constants/constants.go index 21850a53ac..d1446bbe12 100644 --- a/pkg/query-service/constants/constants.go +++ b/pkg/query-service/constants/constants.go @@ -9,6 +9,7 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/model" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" + "github.com/SigNoz/signoz/pkg/types/featuretypes" ) const ( @@ -65,9 +66,9 @@ func UseMetricsPreAggregation() bool { var KafkaSpanEval = GetOrDefaultEnv("KAFKA_SPAN_EVAL", "false") -var DEFAULT_FEATURE_SET = model.FeatureSet{ - model.Feature{ - Name: model.UseSpanMetrics, +var DEFAULT_FEATURE_SET = []*featuretypes.GettableFeature{ + &featuretypes.GettableFeature{ + Name: featuretypes.UseSpanMetrics, Active: false, Usage: 0, UsageLimit: -1, diff --git a/pkg/query-service/featureManager/manager.go b/pkg/query-service/featureManager/manager.go deleted file mode 100644 index 7805fe6191..0000000000 --- a/pkg/query-service/featureManager/manager.go +++ /dev/null @@ -1,60 +0,0 @@ -package featureManager - -import ( - "github.com/SigNoz/signoz/pkg/query-service/constants" - "github.com/SigNoz/signoz/pkg/query-service/model" - "go.uber.org/zap" -) - -type FeatureManager struct { -} - -func StartManager() *FeatureManager { - fM := &FeatureManager{} - return fM -} - -// CheckFeature will be internally used by backend routines -// for feature gating -func (fm *FeatureManager) CheckFeature(featureKey string) error { - - feature, err := fm.GetFeatureFlag(featureKey) - if err != nil { - return err - } - - if feature.Active { - return nil - } - - return model.ErrFeatureUnavailable{Key: featureKey} -} - -// GetFeatureFlags returns current features -func (fm *FeatureManager) GetFeatureFlags() (model.FeatureSet, error) { - features := constants.DEFAULT_FEATURE_SET - return features, nil -} - -func (fm *FeatureManager) InitFeatures(req model.FeatureSet) error { - zap.L().Error("InitFeatures not implemented in OSS") - return nil -} - -func (fm *FeatureManager) UpdateFeatureFlag(req model.Feature) error { - zap.L().Error("UpdateFeatureFlag not implemented in OSS") - return nil -} - -func (fm *FeatureManager) GetFeatureFlag(key string) (model.Feature, error) { - features, err := fm.GetFeatureFlags() - if err != nil { - return model.Feature{}, err - } - for _, feature := range features { - if feature.Name == key { - return feature, nil - } - } - return model.Feature{}, model.ErrFeatureUnavailable{Key: key} -} diff --git a/pkg/query-service/interfaces/featureLookup.go b/pkg/query-service/interfaces/featureLookup.go deleted file mode 100644 index e2ecbcc3bb..0000000000 --- a/pkg/query-service/interfaces/featureLookup.go +++ /dev/null @@ -1,13 +0,0 @@ -package interfaces - -import ( - "github.com/SigNoz/signoz/pkg/query-service/model" -) - -type FeatureLookup interface { - CheckFeature(f string) error - GetFeatureFlags() (model.FeatureSet, error) - GetFeatureFlag(f string) (model.Feature, error) - UpdateFeatureFlag(features model.Feature) error - InitFeatures(features model.FeatureSet) error -} diff --git a/pkg/query-service/main.go b/pkg/query-service/main.go index 7c4ba52dbb..89644b8518 100644 --- a/pkg/query-service/main.go +++ b/pkg/query-service/main.go @@ -11,6 +11,8 @@ import ( "github.com/SigNoz/signoz/pkg/config/fileprovider" "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/licensing" + "github.com/SigNoz/signoz/pkg/licensing/nooplicensing" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/modules/user/impluser" "github.com/SigNoz/signoz/pkg/query-service/app" @@ -120,6 +122,10 @@ func main() { config, zeus.Config{}, noopzeus.NewProviderFactory(), + licensing.Config{}, + func(_ sqlstore.SQLStore, _ zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config] { + return nooplicensing.NewFactory() + }, signoz.NewEmailingProviderFactories(), signoz.NewCacheProviderFactories(), signoz.NewWebProviderFactories(), diff --git a/pkg/query-service/model/featureSet.go b/pkg/query-service/model/featureSet.go deleted file mode 100644 index 4646d030f6..0000000000 --- a/pkg/query-service/model/featureSet.go +++ /dev/null @@ -1,38 +0,0 @@ -package model - -type FeatureSet []Feature -type Feature struct { - Name string `db:"name" json:"name"` - Active bool `db:"active" json:"active"` - Usage int64 `db:"usage" json:"usage"` - UsageLimit int64 `db:"usage_limit" json:"usage_limit"` - Route string `db:"route" json:"route"` -} - -const UseSpanMetrics = "USE_SPAN_METRICS" -const AnomalyDetection = "ANOMALY_DETECTION" -const TraceFunnels = "TRACE_FUNNELS" - -var BasicPlan = FeatureSet{ - Feature{ - Name: UseSpanMetrics, - Active: false, - Usage: 0, - UsageLimit: -1, - Route: "", - }, - Feature{ - Name: AnomalyDetection, - Active: false, - Usage: 0, - UsageLimit: -1, - Route: "", - }, - Feature{ - Name: TraceFunnels, - Active: false, - Usage: 0, - UsageLimit: -1, - Route: "", - }, -} diff --git a/pkg/query-service/tests/integration/filter_suggestions_test.go b/pkg/query-service/tests/integration/filter_suggestions_test.go index 781e453cdf..b0e02b1ec4 100644 --- a/pkg/query-service/tests/integration/filter_suggestions_test.go +++ b/pkg/query-service/tests/integration/filter_suggestions_test.go @@ -24,7 +24,6 @@ import ( "github.com/SigNoz/signoz/pkg/modules/user/impluser" "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/query-service/constants" - "github.com/SigNoz/signoz/pkg/query-service/featureManager" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" "github.com/SigNoz/signoz/pkg/query-service/utils" "github.com/SigNoz/signoz/pkg/signoz" @@ -304,7 +303,6 @@ func (tb *FilterSuggestionsTestBed) GetQBFilterSuggestionsForLogs( func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed { testDB := utils.NewQueryServiceDBForTests(t) - fm := featureManager.StartManager() reader, mockClickhouse := NewMockClickhouseReader(t, testDB) mockClickhouse.MatchExpectationsInOrder(false) @@ -317,9 +315,8 @@ func NewFilterSuggestionsTestBed(t *testing.T) *FilterSuggestionsTestBed { quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB))) apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{ - Reader: reader, - FeatureFlags: fm, - JWT: jwt, + Reader: reader, + JWT: jwt, Signoz: &signoz.SigNoz{ Modules: modules, Handlers: signoz.NewHandlers(modules, userHandler), diff --git a/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go b/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go index 0d3a93ac1d..76ea8f590f 100644 --- a/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go +++ b/pkg/query-service/tests/integration/signoz_cloud_integrations_test.go @@ -24,7 +24,6 @@ import ( "github.com/SigNoz/signoz/pkg/instrumentation/instrumentationtest" "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations" - "github.com/SigNoz/signoz/pkg/query-service/featureManager" "github.com/SigNoz/signoz/pkg/query-service/utils" "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types" @@ -366,7 +365,6 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI t.Fatalf("could not create cloud integrations controller: %v", err) } - fm := featureManager.StartManager() reader, mockClickhouse := NewMockClickhouseReader(t, testDB) mockClickhouse.MatchExpectationsInOrder(false) @@ -382,7 +380,6 @@ func NewCloudIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *CloudI apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{ Reader: reader, CloudIntegrationsController: controller, - FeatureFlags: fm, JWT: jwt, Signoz: &signoz.SigNoz{ Modules: modules, diff --git a/pkg/query-service/tests/integration/signoz_integrations_test.go b/pkg/query-service/tests/integration/signoz_integrations_test.go index 4111d6df42..8751ef5c2a 100644 --- a/pkg/query-service/tests/integration/signoz_integrations_test.go +++ b/pkg/query-service/tests/integration/signoz_integrations_test.go @@ -22,7 +22,6 @@ import ( "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations" "github.com/SigNoz/signoz/pkg/query-service/app/integrations" - "github.com/SigNoz/signoz/pkg/query-service/featureManager" "github.com/SigNoz/signoz/pkg/query-service/model" v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3" "github.com/SigNoz/signoz/pkg/query-service/utils" @@ -567,7 +566,6 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration t.Fatalf("could not create integrations controller: %v", err) } - fm := featureManager.StartManager() reader, mockClickhouse := NewMockClickhouseReader(t, testDB) mockClickhouse.MatchExpectationsInOrder(false) @@ -587,9 +585,9 @@ func NewIntegrationsTestBed(t *testing.T, testDB sqlstore.SQLStore) *Integration quickFilterModule := quickfilter.NewAPI(quickfilterscore.NewQuickFilters(quickfilterscore.NewStore(testDB))) apiHandler, err := app.NewAPIHandler(app.APIHandlerOpts{ - Reader: reader, - IntegrationsController: controller, - FeatureFlags: fm, + Reader: reader, + IntegrationsController: controller, + JWT: jwt, CloudIntegrationsController: cloudIntegrationsController, Signoz: &signoz.SigNoz{ diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index a1aed180c9..d5363edd68 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -80,6 +80,7 @@ func NewSQLMigrationProviderFactories(sqlstore sqlstore.SQLStore) factory.NamedM sqlmigration.NewCreateQuickFiltersFactory(sqlstore), sqlmigration.NewUpdateQuickFiltersFactory(sqlstore), sqlmigration.NewAuthRefactorFactory(sqlstore), + sqlmigration.NewUpdateLicenseFactory(sqlstore), sqlmigration.NewMigratePATToFactorAPIKey(sqlstore), ) } diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index d72a1c18cd..555b03a4d3 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -8,6 +8,7 @@ import ( "github.com/SigNoz/signoz/pkg/emailing" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/instrumentation" + "github.com/SigNoz/signoz/pkg/licensing" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/prometheus" "github.com/SigNoz/signoz/pkg/sqlmigration" @@ -30,6 +31,7 @@ type SigNoz struct { Prometheus prometheus.Prometheus Alertmanager alertmanager.Alertmanager Zeus zeus.Zeus + Licensing licensing.Licensing Emailing emailing.Emailing Modules Modules Handlers Handlers @@ -40,6 +42,8 @@ func New( config Config, zeusConfig zeus.Config, zeusProviderFactory factory.ProviderFactory[zeus.Zeus, zeus.Config], + licenseConfig licensing.Config, + licenseProviderFactoryCb func(sqlstore.SQLStore, zeus.Zeus) factory.ProviderFactory[licensing.Licensing, licensing.Config], emailingProviderFactories factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]], cacheProviderFactories factory.NamedMap[factory.ProviderFactory[cache.Cache, cache.Config]], webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]], @@ -171,6 +175,16 @@ func New( return nil, err } + licensingProviderFactory := licenseProviderFactoryCb(sqlstore, zeus) + licensing, err := licensingProviderFactory.New( + ctx, + providerSettings, + licenseConfig, + ) + if err != nil { + return nil, err + } + userModule := userModuleFactory(sqlstore, emailing, providerSettings) userHandler := userHandlerFactory(userModule) @@ -184,6 +198,7 @@ func New( instrumentation.Logger(), factory.NewNamedService(factory.MustNewName("instrumentation"), instrumentation), factory.NewNamedService(factory.MustNewName("alertmanager"), alertmanager), + factory.NewNamedService(factory.MustNewName("licensing"), licensing), ) if err != nil { return nil, err @@ -199,6 +214,7 @@ func New( Prometheus: prometheus, Alertmanager: alertmanager, Zeus: zeus, + Licensing: licensing, Emailing: emailing, Modules: modules, Handlers: handlers, diff --git a/pkg/sqlmigration/034_update_license.go b/pkg/sqlmigration/034_update_license.go new file mode 100644 index 0000000000..0be8ab82bd --- /dev/null +++ b/pkg/sqlmigration/034_update_license.go @@ -0,0 +1,149 @@ +package sqlmigration + +import ( + "context" + "database/sql" + "encoding/json" + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/factory" + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" + "github.com/uptrace/bun/migrate" +) + +type updateLicense struct { + store sqlstore.SQLStore +} + +type existingLicense34 struct { + bun.BaseModel `bun:"table:licenses_v3"` + + ID string `bun:"id,pk,type:text"` + Key string `bun:"key,type:text,notnull,unique"` + Data string `bun:"data,type:text"` +} + +type newLicense34 struct { + bun.BaseModel `bun:"table:license"` + + types.Identifiable + types.TimeAuditable + Key string `bun:"key,type:text,notnull,unique"` + Data map[string]any `bun:"data,type:text"` + LastValidatedAt time.Time `bun:"last_validated_at,notnull"` + OrgID string `bun:"org_id,type:text,notnull" json:"orgID"` +} + +func NewUpdateLicenseFactory(store sqlstore.SQLStore) factory.ProviderFactory[SQLMigration, Config] { + return factory.NewProviderFactory(factory.MustNewName("update_license"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) { + return newUpdateLicense(ctx, ps, c, store) + }) +} + +func newUpdateLicense(_ context.Context, _ factory.ProviderSettings, _ Config, store sqlstore.SQLStore) (SQLMigration, error) { + return &updateLicense{store: store}, nil +} + +func (migration *updateLicense) Register(migrations *migrate.Migrations) error { + if err := migrations.Register(migration.Up, migration.Down); err != nil { + return err + } + + return nil +} + +func (migration *updateLicense) Up(ctx context.Context, db *bun.DB) error { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return err + } + + defer func() { + _ = tx.Rollback() + }() + + err = migration.store.Dialect().RenameTableAndModifyModel(ctx, tx, new(existingLicense34), new(newLicense34), []string{OrgReference}, func(ctx context.Context) error { + existingLicenses := make([]*existingLicense34, 0) + err = tx.NewSelect().Model(&existingLicenses).Scan(ctx) + if err != nil { + if err != sql.ErrNoRows { + return err + } + } + + if err == nil && len(existingLicenses) > 0 { + var orgID string + err := migration. + store. + BunDB(). + NewSelect(). + Model((*types.Organization)(nil)). + Column("id"). + Scan(ctx, &orgID) + if err != nil { + if err != sql.ErrNoRows { + return err + } + } + if err == nil { + newLicenses, err := migration.CopyExistingLicensesToNewLicenses(existingLicenses, orgID) + if err != nil { + return err + } + _, err = tx. + NewInsert(). + Model(&newLicenses). + Exec(ctx) + if err != nil { + return err + } + } + return nil + } + return nil + }) + + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + +func (migration *updateLicense) Down(context.Context, *bun.DB) error { + return nil +} + +func (migration *updateLicense) CopyExistingLicensesToNewLicenses(existingLicenses []*existingLicense34, orgID string) ([]*newLicense34, error) { + newLicenses := make([]*newLicense34, len(existingLicenses)) + for idx, existingLicense := range existingLicenses { + licenseID, err := valuer.NewUUID(existingLicense.ID) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "license id is not a valid UUID: %s", existingLicense.ID) + } + licenseData := map[string]any{} + err = json.Unmarshal([]byte(existingLicense.Data), &licenseData) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "unable to unmarshal license data in map[string]any") + } + newLicenses[idx] = &newLicense34{ + Identifiable: types.Identifiable{ + ID: licenseID, + }, + TimeAuditable: types.TimeAuditable{ + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + Key: existingLicense.Key, + Data: licenseData, + LastValidatedAt: time.Now(), + OrgID: orgID, + } + } + return newLicenses, nil +} diff --git a/pkg/types/featuretypes/feature.go b/pkg/types/featuretypes/feature.go new file mode 100644 index 0000000000..964cd4a15c --- /dev/null +++ b/pkg/types/featuretypes/feature.go @@ -0,0 +1,28 @@ +package featuretypes + +import "github.com/uptrace/bun" + +type FeatureSet []*GettableFeature +type GettableFeature struct { + Name string `db:"name" json:"name"` + Active bool `db:"active" json:"active"` + Usage int64 `db:"usage" json:"usage"` + UsageLimit int64 `db:"usage_limit" json:"usage_limit"` + Route string `db:"route" json:"route"` +} + +type StorableFeature struct { + bun.BaseModel `bun:"table:feature_status"` + + Name string `bun:"name,pk,type:text" json:"name"` + Active bool `bun:"active" json:"active"` + Usage int `bun:"usage,default:0" json:"usage"` + UsageLimit int `bun:"usage_limit,default:0" json:"usage_limit"` + Route string `bun:"route,type:text" json:"route"` +} + +func NewStorableFeature() {} + +const UseSpanMetrics = "USE_SPAN_METRICS" +const AnomalyDetection = "ANOMALY_DETECTION" +const TraceFunnels = "TRACE_FUNNELS" diff --git a/pkg/types/invite.go b/pkg/types/invite.go index 40fea73d27..8de9ede349 100644 --- a/pkg/types/invite.go +++ b/pkg/types/invite.go @@ -85,3 +85,7 @@ type PostableInvite struct { type PostableBulkInviteRequest struct { Invites []PostableInvite `json:"invites"` } + +type GettableCreateInviteResponse struct { + InviteToken string `json:"token"` +} diff --git a/pkg/types/license.go b/pkg/types/license.go deleted file mode 100644 index fb9fd7e6d0..0000000000 --- a/pkg/types/license.go +++ /dev/null @@ -1,46 +0,0 @@ -package types - -import ( - "time" - - "github.com/uptrace/bun" -) - -type License struct { - bun.BaseModel `bun:"table:licenses"` - - Key string `bun:"key,pk,type:text"` - CreatedAt time.Time `bun:"createdAt,default:current_timestamp"` - UpdatedAt time.Time `bun:"updatedAt,default:current_timestamp"` - PlanDetails string `bun:"planDetails,type:text"` - ActivationID string `bun:"activationId,type:text"` - ValidationMessage string `bun:"validationMessage,type:text"` - LastValidated time.Time `bun:"lastValidated,default:current_timestamp"` -} - -type Site struct { - bun.BaseModel `bun:"table:sites"` - - UUID string `bun:"uuid,pk,type:text"` - Alias string `bun:"alias,type:varchar(180),default:'PROD'"` - URL string `bun:"url,type:varchar(300)"` - CreatedAt time.Time `bun:"createdAt,default:current_timestamp"` -} - -type FeatureStatus struct { - bun.BaseModel `bun:"table:feature_status"` - - Name string `bun:"name,pk,type:text" json:"name"` - Active bool `bun:"active" json:"active"` - Usage int `bun:"usage,default:0" json:"usage"` - UsageLimit int `bun:"usage_limit,default:0" json:"usage_limit"` - Route string `bun:"route,type:text" json:"route"` -} - -type LicenseV3 struct { - bun.BaseModel `bun:"table:licenses_v3"` - - ID string `bun:"id,pk,type:text"` - Key string `bun:"key,type:text,notnull,unique"` - Data string `bun:"data,type:text"` -} diff --git a/pkg/types/licensetypes/license.go b/pkg/types/licensetypes/license.go new file mode 100644 index 0000000000..994b2c7b63 --- /dev/null +++ b/pkg/types/licensetypes/license.go @@ -0,0 +1,389 @@ +package licensetypes + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/featuretypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" +) + +type StorableLicense struct { + bun.BaseModel `bun:"table:license"` + + types.Identifiable + types.TimeAuditable + Key string `bun:"key,type:text,notnull,unique"` + Data map[string]any `bun:"data,type:text"` + LastValidatedAt time.Time `bun:"last_validated_at,notnull"` + OrgID valuer.UUID `bun:"org_id,type:text,notnull" json:"orgID"` +} + +// this data excludes ID and Key +type License struct { + ID valuer.UUID + Key string + Data map[string]interface{} + PlanName string + Features []*featuretypes.GettableFeature + Status string + ValidFrom int64 + ValidUntil int64 + CreatedAt time.Time + UpdatedAt time.Time + LastValidatedAt time.Time + OrganizationID valuer.UUID +} + +type GettableLicense map[string]any + +type PostableLicense struct { + Key string `json:"key"` +} + +func NewStorableLicense(ID valuer.UUID, key string, data map[string]any, createdAt, updatedAt, lastValidatedAt time.Time, organizationID valuer.UUID) *StorableLicense { + return &StorableLicense{ + Identifiable: types.Identifiable{ + ID: ID, + }, + TimeAuditable: types.TimeAuditable{ + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }, + Key: key, + Data: data, + LastValidatedAt: lastValidatedAt, + OrgID: organizationID, + } +} + +func NewStorableLicenseFromLicense(license *License) *StorableLicense { + return &StorableLicense{ + Identifiable: types.Identifiable{ + ID: license.ID, + }, + TimeAuditable: types.TimeAuditable{ + CreatedAt: license.CreatedAt, + UpdatedAt: license.UpdatedAt, + }, + Key: license.Key, + Data: license.Data, + LastValidatedAt: license.LastValidatedAt, + OrgID: license.OrganizationID, + } +} + +func GetActiveLicenseFromStorableLicenses(storableLicenses []*StorableLicense, organizationID valuer.UUID) (*License, error) { + var activeLicense *License + for _, storableLicense := range storableLicenses { + license, err := NewLicenseFromStorableLicense(storableLicense) + if err != nil { + return nil, err + } + + if license.Status != "VALID" { + continue + } + if activeLicense == nil && + (license.ValidFrom != 0) && + (license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) { + activeLicense = license + } + if activeLicense != nil && + license.ValidFrom > activeLicense.ValidFrom && + (license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) { + activeLicense = license + } + } + + if activeLicense == nil { + return nil, errors.Newf(errors.TypeNotFound, errors.CodeNotFound, "no active license found for the organization %s", organizationID.StringValue()) + } + + return activeLicense, nil +} + +func extractKeyFromMapStringInterface[T any](data map[string]interface{}, key string) (T, error) { + var zeroValue T + if val, ok := data[key]; ok { + if value, ok := val.(T); ok { + return value, nil + } + return zeroValue, fmt.Errorf("%s key is not a valid %s", key, reflect.TypeOf(zeroValue)) + } + return zeroValue, fmt.Errorf("%s key is missing", key) +} + +func NewLicense(data []byte, organizationID valuer.UUID) (*License, error) { + licenseData := map[string]any{} + err := json.Unmarshal(data, &licenseData) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to unmarshal license data") + } + + var features []*featuretypes.GettableFeature + + // extract id from data + licenseIDStr, err := extractKeyFromMapStringInterface[string](licenseData, "id") + if err != nil { + return nil, err + } + licenseID, err := valuer.NewUUID(licenseIDStr) + if err != nil { + return nil, err + } + delete(licenseData, "id") + + // extract key from data + licenseKey, err := extractKeyFromMapStringInterface[string](licenseData, "key") + if err != nil { + return nil, err + } + delete(licenseData, "key") + + // extract status from data + status, err := extractKeyFromMapStringInterface[string](licenseData, "status") + if err != nil { + return nil, err + } + + planMap, err := extractKeyFromMapStringInterface[map[string]any](licenseData, "plan") + if err != nil { + return nil, err + } + + planName, err := extractKeyFromMapStringInterface[string](planMap, "name") + if err != nil { + return nil, err + } + // if license status is invalid then default it to basic + if status == LicenseStatusInvalid { + planName = PlanNameBasic + } + + featuresFromZeus := make([]*featuretypes.GettableFeature, 0) + if _features, ok := licenseData["features"]; ok { + featuresData, err := json.Marshal(_features) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal features data") + } + + if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal features data") + } + } + + switch planName { + case PlanNameEnterprise: + features = append(features, EnterprisePlan...) + case PlanNameBasic: + features = append(features, BasicPlan...) + default: + features = append(features, BasicPlan...) + } + + if len(featuresFromZeus) > 0 { + for _, feature := range featuresFromZeus { + exists := false + for i, existingFeature := range features { + if existingFeature.Name == feature.Name { + features[i] = feature // Replace existing feature + exists = true + break + } + } + if !exists { + features = append(features, feature) // Append if it doesn't exist + } + } + } + licenseData["features"] = features + + _validFrom, err := extractKeyFromMapStringInterface[float64](licenseData, "valid_from") + if err != nil { + _validFrom = 0 + } + validFrom := int64(_validFrom) + + _validUntil, err := extractKeyFromMapStringInterface[float64](licenseData, "valid_until") + if err != nil { + _validUntil = 0 + } + validUntil := int64(_validUntil) + + return &License{ + ID: licenseID, + Key: licenseKey, + Data: licenseData, + PlanName: planName, + Features: features, + ValidFrom: validFrom, + ValidUntil: validUntil, + Status: status, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + LastValidatedAt: time.Now(), + OrganizationID: organizationID, + }, nil + +} + +func NewLicenseFromStorableLicense(storableLicense *StorableLicense) (*License, error) { + var features []*featuretypes.GettableFeature + // extract status from data + status, err := extractKeyFromMapStringInterface[string](storableLicense.Data, "status") + if err != nil { + return nil, err + } + + planMap, err := extractKeyFromMapStringInterface[map[string]any](storableLicense.Data, "plan") + if err != nil { + return nil, err + } + + planName, err := extractKeyFromMapStringInterface[string](planMap, "name") + if err != nil { + return nil, err + } + // if license status is invalid then default it to basic + if status == LicenseStatusInvalid { + planName = PlanNameBasic + } + + featuresFromZeus := make([]*featuretypes.GettableFeature, 0) + if _features, ok := storableLicense.Data["features"]; ok { + featuresData, err := json.Marshal(_features) + if err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal features data") + } + + if err := json.Unmarshal(featuresData, &featuresFromZeus); err != nil { + return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal features data") + } + } + + switch planName { + case PlanNameEnterprise: + features = append(features, EnterprisePlan...) + case PlanNameBasic: + features = append(features, BasicPlan...) + default: + features = append(features, BasicPlan...) + } + + if len(featuresFromZeus) > 0 { + for _, feature := range featuresFromZeus { + exists := false + for i, existingFeature := range features { + if existingFeature.Name == feature.Name { + features[i] = feature // Replace existing feature + exists = true + break + } + } + if !exists { + features = append(features, feature) // Append if it doesn't exist + } + } + } + storableLicense.Data["features"] = features + + _validFrom, err := extractKeyFromMapStringInterface[float64](storableLicense.Data, "valid_from") + if err != nil { + _validFrom = 0 + } + validFrom := int64(_validFrom) + + _validUntil, err := extractKeyFromMapStringInterface[float64](storableLicense.Data, "valid_until") + if err != nil { + _validUntil = 0 + } + validUntil := int64(_validUntil) + + return &License{ + ID: storableLicense.ID, + Key: storableLicense.Key, + Data: storableLicense.Data, + PlanName: planName, + Features: features, + ValidFrom: validFrom, + ValidUntil: validUntil, + Status: status, + CreatedAt: storableLicense.CreatedAt, + UpdatedAt: storableLicense.UpdatedAt, + LastValidatedAt: storableLicense.LastValidatedAt, + OrganizationID: storableLicense.OrgID, + }, nil + +} + +func (license *License) Update(data []byte) error { + updatedLicense, err := NewLicense(data, license.OrganizationID) + if err != nil { + return err + } + + currentTime := time.Now() + license.Data = updatedLicense.Data + license.Features = updatedLicense.Features + license.ID = updatedLicense.ID + license.Key = updatedLicense.Key + license.PlanName = updatedLicense.PlanName + license.Status = updatedLicense.Status + license.ValidFrom = updatedLicense.ValidFrom + license.ValidUntil = updatedLicense.ValidUntil + license.UpdatedAt = currentTime + license.LastValidatedAt = currentTime + + return nil +} + +func NewGettableLicense(data map[string]any, key string) *GettableLicense { + gettableLicense := make(GettableLicense) + for k, v := range data { + gettableLicense[k] = v + } + gettableLicense["key"] = key + return &gettableLicense +} + +func (p *PostableLicense) UnmarshalJSON(data []byte) error { + var postableLicense struct { + Key string `json:"key"` + } + + err := json.Unmarshal(data, &postableLicense) + if err != nil { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal payload") + } + + if postableLicense.Key == "" { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "license key cannot be empty") + } + + p.Key = postableLicense.Key + return nil +} + +type Store interface { + Create(context.Context, *StorableLicense) error + Get(context.Context, valuer.UUID, valuer.UUID) (*StorableLicense, error) + GetAll(context.Context, valuer.UUID) ([]*StorableLicense, error) + Update(context.Context, valuer.UUID, *StorableLicense) error + + // feature surrogate + InitFeatures(context.Context, []*featuretypes.StorableFeature) error + CreateFeature(context.Context, *featuretypes.StorableFeature) error + GetFeature(context.Context, string) (*featuretypes.StorableFeature, error) + GetAllFeatures(context.Context) ([]*featuretypes.StorableFeature, error) + UpdateFeature(context.Context, *featuretypes.StorableFeature) error + + // ListOrganizations returns the list of orgs + ListOrganizations(context.Context) ([]valuer.UUID, error) +} diff --git a/pkg/types/licensetypes/license_test.go b/pkg/types/licensetypes/license_test.go new file mode 100644 index 0000000000..b2216accbb --- /dev/null +++ b/pkg/types/licensetypes/license_test.go @@ -0,0 +1,175 @@ +package licensetypes + +import ( + "testing" + "time" + + "github.com/SigNoz/signoz/pkg/types/featuretypes" + "github.com/SigNoz/signoz/pkg/valuer" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewLicenseV3(t *testing.T) { + testCases := []struct { + name string + data []byte + pass bool + expected *License + error error + }{ + { + name: "Error for missing license id", + data: []byte(`{}`), + pass: false, + error: errors.New("id key is missing"), + }, + { + name: "Error for license id not being a valid string", + data: []byte(`{"id": 10}`), + pass: false, + error: errors.New("id key is not a valid string"), + }, + { + name: "Error for missing license key", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e"}`), + pass: false, + error: errors.New("key key is missing"), + }, + { + name: "Error for invalid string license key", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":10}`), + pass: false, + error: errors.New("key key is not a valid string"), + }, + { + name: "Error for missing license status", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e", "key": "does-not-matter","category":"FREE"}`), + pass: false, + error: errors.New("status key is missing"), + }, + { + name: "Error for invalid string license status", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key": "does-not-matter", "category":"FREE", "status":10}`), + pass: false, + error: errors.New("status key is not a valid string"), + }, + { + name: "Error for missing license plan", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE"}`), + pass: false, + error: errors.New("plan key is missing"), + }, + { + name: "Error for invalid json license plan", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":10}`), + pass: false, + error: errors.New("plan key is not a valid map[string]interface {}"), + }, + { + name: "Error for invalid license plan", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{}}`), + pass: false, + error: errors.New("name key is missing"), + }, + { + name: "Parse the entire license properly", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`), + pass: true, + expected: &License{ + ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), + Key: "does-not-matter-key", + Data: map[string]interface{}{ + "plan": map[string]interface{}{ + "name": "ENTERPRISE", + }, + "category": "FREE", + "status": "ACTIVE", + "valid_from": float64(1730899309), + "valid_until": float64(-1), + }, + PlanName: PlanNameEnterprise, + ValidFrom: 1730899309, + ValidUntil: -1, + Status: "ACTIVE", + Features: make([]*featuretypes.GettableFeature, 0), + OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), + }, + }, + { + name: "Fallback to basic plan if license status is invalid", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"INVALID","plan":{"name":"ENTERPRISE"},"valid_from": 1730899309,"valid_until": -1}`), + pass: true, + expected: &License{ + ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), + Key: "does-not-matter-key", + Data: map[string]interface{}{ + "plan": map[string]interface{}{ + "name": "ENTERPRISE", + }, + "category": "FREE", + "status": "INVALID", + "valid_from": float64(1730899309), + "valid_until": float64(-1), + }, + PlanName: PlanNameBasic, + ValidFrom: 1730899309, + ValidUntil: -1, + Status: "INVALID", + Features: make([]*featuretypes.GettableFeature, 0), + OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), + }, + }, + { + name: "fallback states for validFrom and validUntil", + data: []byte(`{"id":"0196f794-ff30-7bee-a5f4-ef5ad315715e","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"ENTERPRISE"},"valid_from":1234.456,"valid_until":5678.567}`), + pass: true, + expected: &License{ + ID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), + Key: "does-not-matter-key", + Data: map[string]interface{}{ + "plan": map[string]interface{}{ + "name": "ENTERPRISE", + }, + "valid_from": 1234.456, + "valid_until": 5678.567, + "category": "FREE", + "status": "ACTIVE", + }, + PlanName: PlanNameEnterprise, + ValidFrom: 1234, + ValidUntil: 5678, + Status: "ACTIVE", + Features: make([]*featuretypes.GettableFeature, 0), + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + LastValidatedAt: time.Time{}, + OrganizationID: valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e"), + }, + }, + } + + for _, tc := range testCases { + license, err := NewLicense(tc.data, valuer.MustNewUUID("0196f794-ff30-7bee-a5f4-ef5ad315715e")) + if license != nil { + license.Features = make([]*featuretypes.GettableFeature, 0) + delete(license.Data, "features") + } + + if tc.pass { + require.NoError(t, err) + require.NotNil(t, license) + // as the new license will pick the time.Now() value. doesn't make sense to compare them + license.CreatedAt = time.Time{} + license.UpdatedAt = time.Time{} + license.LastValidatedAt = time.Time{} + assert.Equal(t, tc.expected, license) + } else { + require.Error(t, err) + assert.EqualError(t, err, tc.error.Error()) + require.Nil(t, license) + } + + } +} diff --git a/ee/query-service/model/plans.go b/pkg/types/licensetypes/plan.go similarity index 61% rename from ee/query-service/model/plans.go rename to pkg/types/licensetypes/plan.go index 2de2e7ccb8..4521018084 100644 --- a/ee/query-service/model/plans.go +++ b/pkg/types/licensetypes/plan.go @@ -1,8 +1,6 @@ -package model +package licensetypes -import ( - basemodel "github.com/SigNoz/signoz/pkg/query-service/model" -) +import "github.com/SigNoz/signoz/pkg/types/featuretypes" const SSO = "SSO" const Basic = "BASIC_PLAN" @@ -26,44 +24,44 @@ const ChatSupport = "CHAT_SUPPORT" const Gateway = "GATEWAY" const PremiumSupport = "PREMIUM_SUPPORT" -var BasicPlan = basemodel.FeatureSet{ - basemodel.Feature{ +var BasicPlan = featuretypes.FeatureSet{ + &featuretypes.GettableFeature{ Name: SSO, Active: false, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ - Name: basemodel.UseSpanMetrics, + &featuretypes.GettableFeature{ + Name: featuretypes.UseSpanMetrics, Active: false, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ + &featuretypes.GettableFeature{ Name: Gateway, Active: false, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ + &featuretypes.GettableFeature{ Name: PremiumSupport, Active: false, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ - Name: basemodel.AnomalyDetection, + &featuretypes.GettableFeature{ + Name: featuretypes.AnomalyDetection, Active: false, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ - Name: basemodel.TraceFunnels, + &featuretypes.GettableFeature{ + Name: featuretypes.TraceFunnels, Active: false, Usage: 0, UsageLimit: -1, @@ -71,58 +69,68 @@ var BasicPlan = basemodel.FeatureSet{ }, } -var EnterprisePlan = basemodel.FeatureSet{ - basemodel.Feature{ +var EnterprisePlan = featuretypes.FeatureSet{ + &featuretypes.GettableFeature{ Name: SSO, Active: true, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ - Name: basemodel.UseSpanMetrics, + &featuretypes.GettableFeature{ + Name: featuretypes.UseSpanMetrics, Active: false, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ + &featuretypes.GettableFeature{ Name: Onboarding, Active: true, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ + &featuretypes.GettableFeature{ Name: ChatSupport, Active: true, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ + &featuretypes.GettableFeature{ Name: Gateway, Active: true, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ + &featuretypes.GettableFeature{ Name: PremiumSupport, Active: true, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ - Name: basemodel.AnomalyDetection, + &featuretypes.GettableFeature{ + Name: featuretypes.AnomalyDetection, Active: true, Usage: 0, UsageLimit: -1, Route: "", }, - basemodel.Feature{ - Name: basemodel.TraceFunnels, + &featuretypes.GettableFeature{ + Name: featuretypes.TraceFunnels, + Active: false, + Usage: 0, + UsageLimit: -1, + Route: "", + }, +} + +var DefaultFeatureSet = featuretypes.FeatureSet{ + &featuretypes.GettableFeature{ + Name: featuretypes.UseSpanMetrics, Active: false, Usage: 0, UsageLimit: -1, diff --git a/pkg/types/licensetypes/subscription.go b/pkg/types/licensetypes/subscription.go new file mode 100644 index 0000000000..9065a8a465 --- /dev/null +++ b/pkg/types/licensetypes/subscription.go @@ -0,0 +1,33 @@ +package licensetypes + +import ( + "encoding/json" + + "github.com/SigNoz/signoz/pkg/errors" +) + +type GettableSubscription struct { + RedirectURL string `json:"redirectURL"` +} + +type PostableSubscription struct { + SuccessURL string `json:"url"` +} + +func (p *PostableSubscription) UnmarshalJSON(data []byte) error { + var postableSubscription struct { + SuccessURL string `json:"url"` + } + + err := json.Unmarshal(data, &postableSubscription) + if err != nil { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to unmarshal payload") + } + + if postableSubscription.SuccessURL == "" { + return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "success url cannot be empty") + } + + p.SuccessURL = postableSubscription.SuccessURL + return nil +} diff --git a/tests/integration/fixtures/http.py b/tests/integration/fixtures/http.py index fd8f798df4..2df0f1cc30 100644 --- a/tests/integration/fixtures/http.py +++ b/tests/integration/fixtures/http.py @@ -6,6 +6,7 @@ from testcontainers.core.container import Network from wiremock.client import ( Mapping, Mappings, + Requests, ) from wiremock.constants import Config from wiremock.testing.testcontainer import WireMockContainer @@ -78,3 +79,4 @@ def make_http_mocks(): yield _make_http_mocks Mappings.delete_all_mappings() + Requests.reset_request_journal() diff --git a/tests/integration/src/bootstrap/c_license.py b/tests/integration/src/bootstrap/c_license.py index 5a2647610f..6ed524d1d8 100644 --- a/tests/integration/src/bootstrap/c_license.py +++ b/tests/integration/src/bootstrap/c_license.py @@ -69,7 +69,7 @@ def test_apply_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None: timeout=5, ) - assert response.json()["count"] >= 1 + assert response.json()["count"] == 1 def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None: @@ -123,7 +123,7 @@ def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None cursor = signoz.sqlstore.conn.cursor() cursor.execute( - "SELECT data FROM licenses_v3 WHERE id='0196360e-90cd-7a74-8313-1aa815ce2a67'" + "SELECT data FROM license WHERE id='0196360e-90cd-7a74-8313-1aa815ce2a67'" ) record = cursor.fetchone()[0] assert json.loads(record)["valid_from"] == 1732146922 @@ -134,7 +134,7 @@ def test_refresh_license(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None timeout=5, ) - assert response.json()["count"] >= 1 + assert response.json()["count"] == 1 def test_license_checkout(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None: @@ -172,7 +172,7 @@ def test_license_checkout(signoz: SigNoz, make_http_mocks, get_jwt_token) -> Non timeout=5, ) - assert response.status_code == http.HTTPStatus.OK + assert response.status_code == http.HTTPStatus.CREATED assert response.json()["data"]["redirectURL"] == "https://signoz.checkout.com" response = requests.post( @@ -219,7 +219,7 @@ def test_license_portal(signoz: SigNoz, make_http_mocks, get_jwt_token) -> None: timeout=5, ) - assert response.status_code == http.HTTPStatus.OK + assert response.status_code == http.HTTPStatus.CREATED assert response.json()["data"]["redirectURL"] == "https://signoz.portal.com" response = requests.post(