diff --git a/Wire.go b/Wire.go index 3e76c85272..98c2bca2ea 100644 --- a/Wire.go +++ b/Wire.go @@ -729,6 +729,14 @@ func InitializeApp() (*App, error) { app.NewPipelineStatusTimelineServiceImpl, wire.Bind(new(app.PipelineStatusTimelineService), new(*app.PipelineStatusTimelineServiceImpl)), + router.NewUserAttributesRouterImpl, + wire.Bind(new(router.UserAttributesRouter), new(*router.UserAttributesRouterImpl)), + restHandler.NewUserAttributesRestHandlerImpl, + wire.Bind(new(restHandler.UserAttributesRestHandler), new(*restHandler.UserAttributesRestHandlerImpl)), + attributes.NewUserAttributesServiceImpl, + wire.Bind(new(attributes.UserAttributesService), new(*attributes.UserAttributesServiceImpl)), + repository.NewUserAttributesRepositoryImpl, + wire.Bind(new(repository.UserAttributesRepository), new(*repository.UserAttributesRepositoryImpl)), pipelineConfig.NewPipelineStatusTimelineRepositoryImpl, wire.Bind(new(pipelineConfig.PipelineStatusTimelineRepository), new(*pipelineConfig.PipelineStatusTimelineRepositoryImpl)), ) diff --git a/api/restHandler/TelemetryRestHandler.go b/api/restHandler/TelemetryRestHandler.go index 1c8d807c9c..a0b9ca3209 100644 --- a/api/restHandler/TelemetryRestHandler.go +++ b/api/restHandler/TelemetryRestHandler.go @@ -18,24 +18,36 @@ package restHandler import ( + "encoding/json" + "errors" "github.com/devtron-labs/devtron/api/restHandler/common" "github.com/devtron-labs/devtron/client/telemetry" + "github.com/devtron-labs/devtron/pkg/user" + "github.com/devtron-labs/devtron/pkg/user/casbin" "go.uber.org/zap" "net/http" ) type TelemetryRestHandler interface { GetTelemetryMetaInfo(w http.ResponseWriter, r *http.Request) + SendTelemetryData(w http.ResponseWriter, r *http.Request) } type TelemetryRestHandlerImpl struct { logger *zap.SugaredLogger telemetryEventClient telemetry.TelemetryEventClient + enforcer casbin.Enforcer + userService user.UserService +} + +type TelemetryGenericEvent struct { + eventType string + eventPayload map[string]interface{} } func NewTelemetryRestHandlerImpl(logger *zap.SugaredLogger, - telemetryEventClient telemetry.TelemetryEventClient) *TelemetryRestHandlerImpl { - handler := &TelemetryRestHandlerImpl{logger: logger, telemetryEventClient: telemetryEventClient} + telemetryEventClient telemetry.TelemetryEventClient, enforcer casbin.Enforcer, userService user.UserService) *TelemetryRestHandlerImpl { + handler := &TelemetryRestHandlerImpl{logger: logger, telemetryEventClient: telemetryEventClient, enforcer: enforcer, userService: userService} return handler } @@ -48,3 +60,36 @@ func (handler TelemetryRestHandlerImpl) GetTelemetryMetaInfo(w http.ResponseWrit } common.WriteJsonResp(w, nil, res, http.StatusOK) } + +func (handler TelemetryRestHandlerImpl) SendTelemetryData(w http.ResponseWriter, r *http.Request) { + userId, err := handler.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + decoder := json.NewDecoder(r.Body) + var payload map[string]interface{} + err = decoder.Decode(&payload) + if err != nil { + handler.logger.Errorw("request err, SendTelemetryData", "err", err, "payload", payload) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + token := r.Header.Get("token") + if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + + eventType := payload["eventType"] + eventTypeString := eventType.(string) + err = handler.telemetryEventClient.SendGenericTelemetryEvent(eventTypeString, payload) + + if err != nil { + handler.logger.Errorw("service err, SendTelemetryData", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, "success", http.StatusOK) + +} diff --git a/api/restHandler/UserAttributesRestHandler.go b/api/restHandler/UserAttributesRestHandler.go new file mode 100644 index 0000000000..0348ec599e --- /dev/null +++ b/api/restHandler/UserAttributesRestHandler.go @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package restHandler + +import ( + "encoding/json" + "errors" + "github.com/devtron-labs/devtron/api/restHandler/common" + "github.com/devtron-labs/devtron/pkg/attributes" + "github.com/devtron-labs/devtron/pkg/user" + "github.com/devtron-labs/devtron/pkg/user/casbin" + "github.com/gorilla/mux" + "go.uber.org/zap" + "net/http" +) + +type UserAttributesRestHandler interface { + AddUserAttributes(w http.ResponseWriter, r *http.Request) + UpdateUserAttributes(w http.ResponseWriter, r *http.Request) + GetUserAttribute(w http.ResponseWriter, r *http.Request) +} + +type UserAttributesRestHandlerImpl struct { + logger *zap.SugaredLogger + enforcer casbin.Enforcer + userService user.UserService + userAttributesService attributes.UserAttributesService +} + +func NewUserAttributesRestHandlerImpl(logger *zap.SugaredLogger, enforcer casbin.Enforcer, + userService user.UserService, userAttributesService attributes.UserAttributesService) *UserAttributesRestHandlerImpl { + userAuthHandler := &UserAttributesRestHandlerImpl{ + logger: logger, + enforcer: enforcer, + userService: userService, + userAttributesService: userAttributesService, + } + return userAuthHandler +} + +func (handler *UserAttributesRestHandlerImpl) AddUserAttributes(w http.ResponseWriter, r *http.Request) { + userId, err := handler.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + decoder := json.NewDecoder(r.Body) + var dto attributes.UserAttributesDto + err = decoder.Decode(&dto) + if err != nil { + handler.logger.Errorw("request err, AddUserAttributes", "err", err, "payload", dto) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + dto.UserId = userId + token := r.Header.Get("token") + //if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionCreate, "*"); !ok { + // common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + // return + //} + emailId, err := handler.userService.GetEmailFromToken(token) + if err != nil { + handler.logger.Errorw("request err, UpdateUserAttributes", "err", err, "payload", dto) + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + dto.EmailId = emailId + + handler.logger.Infow("request payload, AddUserAttributes", "payload", dto) + resp, err := handler.userAttributesService.AddUserAttributes(&dto) + if err != nil { + handler.logger.Errorw("service err, AddUserAttributes", "err", err, "payload", dto) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, resp, http.StatusOK) +} + +// @Summary update user attributes +// @version 1.0 +// @produce application/json +// @Param payload body attributes.UserAttributesDto true "Input key" +// @Success 200 {object} attributes.UserAttributesDto +// @Router /orchestrator/attributes/user/update [POST] +func (handler *UserAttributesRestHandlerImpl) UpdateUserAttributes(w http.ResponseWriter, r *http.Request) { + userId, err := handler.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + + decoder := json.NewDecoder(r.Body) + var dto attributes.UserAttributesDto + err = decoder.Decode(&dto) + if err != nil { + handler.logger.Errorw("request err, UpdateUserAttributes", "err", err, "payload", dto) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + dto.UserId = userId + token := r.Header.Get("token") + //if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok { + // common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + // return + //} + + emailId, err := handler.userService.GetEmailFromToken(token) + if err != nil { + handler.logger.Errorw("request err, UpdateUserAttributes", "err", err, "payload", dto) + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + dto.EmailId = emailId + + handler.logger.Infow("request payload, UpdateUserAttributes", "payload", dto) + resp, err := handler.userAttributesService.UpdateUserAttributes(&dto) + if err != nil { + handler.logger.Errorw("service err, UpdateUserAttributes", "err", err, "payload", dto) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, resp, http.StatusOK) +} + +// @Summary get user attributes +// @version 1.0 +// @produce application/json +// @Param name query string true "Input key" +// @Success 200 {object} attributes.UserAttributesDto +// @Router /orchestrator/attributes/user/get [GET] +func (handler *UserAttributesRestHandlerImpl) GetUserAttribute(w http.ResponseWriter, r *http.Request) { + userId, err := handler.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + key := vars["key"] + if key == "" { + handler.logger.Errorw("request err, GetUserAttribute", "err", err, "key", key) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + token := r.Header.Get("token") + //if ok := handler.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionGet, "*"); !ok { + // common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + // return + //} + + dto := attributes.UserAttributesDto{} + + emailId, err := handler.userService.GetEmailFromToken(token) + if err != nil { + handler.logger.Errorw("request err, UpdateUserAttributes", "err", err, "payload", dto) + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + dto.EmailId = emailId + dto.Key = key + + res, err := handler.userAttributesService.GetUserAttribute(&dto) + if err != nil { + handler.logger.Errorw("service err, GetAttributesById", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, nil, res, http.StatusOK) +} diff --git a/api/router/TelemetryRouter.go b/api/router/TelemetryRouter.go index fdc0c168c4..c5ede43061 100644 --- a/api/router/TelemetryRouter.go +++ b/api/router/TelemetryRouter.go @@ -24,7 +24,7 @@ import ( ) type TelemetryRouter interface { - initTelemetryRouter(router *mux.Router) + InitTelemetryRouter(router *mux.Router) } type TelemetryRouterImpl struct { @@ -39,7 +39,10 @@ func NewTelemetryRouterImpl(logger *zap.SugaredLogger, handler restHandler.Telem return router } -func (router TelemetryRouterImpl) initTelemetryRouter(telemetryRouter *mux.Router) { +func (router TelemetryRouterImpl) InitTelemetryRouter(telemetryRouter *mux.Router) { telemetryRouter.Path("/meta"). HandlerFunc(router.handler.GetTelemetryMetaInfo).Methods("GET") + telemetryRouter.Path("/event"). + HandlerFunc(router.handler.SendTelemetryData).Methods("POST") + } diff --git a/api/router/UserAttributesRouter.go b/api/router/UserAttributesRouter.go new file mode 100644 index 0000000000..07ab87a1b6 --- /dev/null +++ b/api/router/UserAttributesRouter.go @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package router + +import ( + user "github.com/devtron-labs/devtron/api/restHandler" + "github.com/gorilla/mux" +) + +type UserAttributesRouter interface { + InitUserAttributesRouter(helmRouter *mux.Router) +} + +type UserAttributesRouterImpl struct { + userAttributesRestHandler user.UserAttributesRestHandler +} + +func NewUserAttributesRouterImpl(userAttributesRestHandler user.UserAttributesRestHandler) *UserAttributesRouterImpl { + router := &UserAttributesRouterImpl{ + userAttributesRestHandler: userAttributesRestHandler, + } + return router +} + +func (router UserAttributesRouterImpl) InitUserAttributesRouter(attributesRouter *mux.Router) { + attributesRouter.Path("/update"). + HandlerFunc(router.userAttributesRestHandler.UpdateUserAttributes).Methods("POST") + attributesRouter.Path("/get"). + HandlerFunc(router.userAttributesRestHandler.GetUserAttribute).Queries("key", "{key}").Methods("GET") +} diff --git a/api/router/router.go b/api/router/router.go index bdbd8b19e8..b9e719af59 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -88,6 +88,7 @@ type MuxRouter struct { gitOpsConfigRouter GitOpsConfigRouter dashboardRouter dashboard.DashboardRouter attributesRouter AttributesRouter + userAttributesRouter UserAttributesRouter commonRouter CommonRouter grafanaRouter GrafanaRouter ssoLoginRouter sso.SsoLoginRouter @@ -129,7 +130,7 @@ func NewMuxRouter(logger *zap.SugaredLogger, HelmRouter HelmRouter, PipelineConf ChartRefRouter ChartRefRouter, ConfigMapRouter ConfigMapRouter, AppStoreRouter appStore.AppStoreRouter, chartRepositoryRouter chartRepo.ChartRepositoryRouter, ReleaseMetricsRouter ReleaseMetricsRouter, deploymentGroupRouter DeploymentGroupRouter, batchOperationRouter BatchOperationRouter, chartGroupRouter ChartGroupRouter, testSuitRouter TestSuitRouter, imageScanRouter ImageScanRouter, - policyRouter PolicyRouter, gitOpsConfigRouter GitOpsConfigRouter, dashboardRouter dashboard.DashboardRouter, attributesRouter AttributesRouter, + policyRouter PolicyRouter, gitOpsConfigRouter GitOpsConfigRouter, dashboardRouter dashboard.DashboardRouter, attributesRouter AttributesRouter, userAttributesRouter UserAttributesRouter, commonRouter CommonRouter, grafanaRouter GrafanaRouter, ssoLoginRouter sso.SsoLoginRouter, telemetryRouter TelemetryRouter, telemetryWatcher telemetry.TelemetryEventClient, bulkUpdateRouter BulkUpdateRouter, webhookListenerRouter WebhookListenerRouter, appLabelsRouter AppLabelRouter, coreAppRouter CoreAppRouter, helmAppRouter client.HelmAppRouter, k8sApplicationRouter k8s.K8sApplicationRouter, pProfRouter PProfRouter, deploymentConfigRouter deployment.DeploymentConfigRouter, dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter, @@ -175,6 +176,7 @@ func NewMuxRouter(logger *zap.SugaredLogger, HelmRouter HelmRouter, PipelineConf policyRouter: policyRouter, gitOpsConfigRouter: gitOpsConfigRouter, attributesRouter: attributesRouter, + userAttributesRouter: userAttributesRouter, dashboardRouter: dashboardRouter, commonRouter: commonRouter, grafanaRouter: grafanaRouter, @@ -322,6 +324,9 @@ func (r MuxRouter) Init() { attributeRouter := r.Router.PathPrefix("/orchestrator/attributes").Subrouter() r.attributesRouter.initAttributesRouter(attributeRouter) + userAttributeRouter := r.Router.PathPrefix("/orchestrator/attributes/user").Subrouter() + r.userAttributesRouter.InitUserAttributesRouter(userAttributeRouter) + dashboardRouter := r.Router.PathPrefix("/dashboard").Subrouter() r.dashboardRouter.InitDashboardRouter(dashboardRouter) @@ -339,7 +344,7 @@ func (r MuxRouter) Init() { r.ssoLoginRouter.InitSsoLoginRouter(ssoLoginRouter) telemetryRouter := r.Router.PathPrefix("/orchestrator/telemetry").Subrouter() - r.telemetryRouter.initTelemetryRouter(telemetryRouter) + r.telemetryRouter.InitTelemetryRouter(telemetryRouter) bulkUpdateRouter := r.Router.PathPrefix("/orchestrator/batch").Subrouter() r.bulkUpdateRouter.initBulkUpdateRouter(bulkUpdateRouter) diff --git a/client/telemetry/TelemetryEventClient.go b/client/telemetry/TelemetryEventClient.go index 7039ecf3c3..0345d5d418 100644 --- a/client/telemetry/TelemetryEventClient.go +++ b/client/telemetry/TelemetryEventClient.go @@ -45,6 +45,7 @@ type TelemetryEventClient interface { SendTelemetryInstallEventEA() (*TelemetryEventType, error) SendTelemetryDashboardAccessEvent() error SendTelemetryDashboardLoggedInEvent() error + SendGenericTelemetryEvent(eventType string, prop map[string]interface{}) error } func NewTelemetryEventClientImpl(logger *zap.SugaredLogger, client *http.Client, clusterService cluster.ClusterService, @@ -205,6 +206,25 @@ func (impl *TelemetryEventClientImpl) SummaryEventForTelemetryEA() { } func (impl *TelemetryEventClientImpl) EnqueuePostHog(ucid string, eventType TelemetryEventType, prop map[string]interface{}) error { + return impl.EnqueueGenericPostHogEvent(ucid, string(eventType), prop) +} + +func (impl *TelemetryEventClientImpl) SendGenericTelemetryEvent(eventType string, prop map[string]interface{}) error { + ucid, err := impl.getUCID() + if err != nil { + impl.logger.Errorw("exception caught inside telemetry generic event", "err", err) + return nil + } + + if IsOptOut { + impl.logger.Warnw("client is opt-out for telemetry, there will be no events capture", "ucid", ucid) + return nil + } + + return impl.EnqueueGenericPostHogEvent(ucid, eventType, prop) +} + +func (impl *TelemetryEventClientImpl) EnqueueGenericPostHogEvent(ucid string, eventType string, prop map[string]interface{}) error { if impl.PosthogClient.Client == nil { impl.logger.Warn("no posthog client found, creating new") client, err := impl.retryPosthogClient(PosthogApiKey, PosthogEndpoint) @@ -215,11 +235,11 @@ func (impl *TelemetryEventClientImpl) EnqueuePostHog(ucid string, eventType Tele if impl.PosthogClient.Client != nil { err := impl.PosthogClient.Client.Enqueue(posthog.Capture{ DistinctId: ucid, - Event: string(eventType), + Event: eventType, Properties: prop, }) if err != nil { - impl.logger.Errorw("SummaryEventForTelemetry, failed to push event", "error", err) + impl.logger.Errorw("EnqueueGenericPostHogEvent, failed to push event", "error", err) return err } } diff --git a/cmd/external-app/router.go b/cmd/external-app/router.go index 9d799ad0c1..ddfad7a296 100644 --- a/cmd/external-app/router.go +++ b/cmd/external-app/router.go @@ -13,6 +13,7 @@ import ( client "github.com/devtron-labs/devtron/api/helm-app" "github.com/devtron-labs/devtron/api/module" "github.com/devtron-labs/devtron/api/restHandler/common" + "github.com/devtron-labs/devtron/api/router" "github.com/devtron-labs/devtron/api/server" "github.com/devtron-labs/devtron/api/sso" "github.com/devtron-labs/devtron/api/team" @@ -50,6 +51,8 @@ type MuxRouter struct { apiTokenRouter apiToken.ApiTokenRouter k8sCapacityRouter k8s.K8sCapacityRouter webhookHelmRouter webhookHelm.WebhookHelmRouter + userAttributesRouter router.UserAttributesRouter + telemetryRouter router.TelemetryRouter } func NewMuxRouter( @@ -74,6 +77,8 @@ func NewMuxRouter( serverRouter server.ServerRouter, apiTokenRouter apiToken.ApiTokenRouter, k8sCapacityRouter k8s.K8sCapacityRouter, webhookHelmRouter webhookHelm.WebhookHelmRouter, + userAttributesRouter router.UserAttributesRouter, + telemetryRouter router.TelemetryRouter, ) *MuxRouter { r := &MuxRouter{ Router: mux.NewRouter(), @@ -99,6 +104,8 @@ func NewMuxRouter( apiTokenRouter: apiTokenRouter, k8sCapacityRouter: k8sCapacityRouter, webhookHelmRouter: webhookHelmRouter, + userAttributesRouter: userAttributesRouter, + telemetryRouter: telemetryRouter, } return r } @@ -206,4 +213,10 @@ func (r *MuxRouter) Init() { // webhook helm app router webhookHelmRouter := r.Router.PathPrefix("/orchestrator/webhook/helm").Subrouter() r.webhookHelmRouter.InitWebhookHelmRouter(webhookHelmRouter) + + userAttributeRouter := r.Router.PathPrefix("/orchestrator/attributes/user").Subrouter() + r.userAttributesRouter.InitUserAttributesRouter(userAttributeRouter) + + telemetryRouter := r.Router.PathPrefix("/orchestrator/telemetry").Subrouter() + r.telemetryRouter.InitTelemetryRouter(telemetryRouter) } diff --git a/cmd/external-app/wire.go b/cmd/external-app/wire.go index b16d775cca..65106c2517 100644 --- a/cmd/external-app/wire.go +++ b/cmd/external-app/wire.go @@ -16,6 +16,8 @@ import ( "github.com/devtron-labs/devtron/api/externalLink" client "github.com/devtron-labs/devtron/api/helm-app" "github.com/devtron-labs/devtron/api/module" + "github.com/devtron-labs/devtron/api/restHandler" + "github.com/devtron-labs/devtron/api/router" "github.com/devtron-labs/devtron/api/server" "github.com/devtron-labs/devtron/api/sso" "github.com/devtron-labs/devtron/api/team" @@ -107,6 +109,11 @@ func InitializeApp() (*App, error) { wire.Value(chartRepoRepository.RefChartDir("scripts/devtron-reference-helm-charts")), + router.NewTelemetryRouterImpl, + wire.Bind(new(router.TelemetryRouter), new(*router.TelemetryRouterImpl)), + restHandler.NewTelemetryRestHandlerImpl, + wire.Bind(new(restHandler.TelemetryRestHandler), new(*restHandler.TelemetryRestHandlerImpl)), + //needed for sending events dashboardEvent.NewDashboardTelemetryRestHandlerImpl, wire.Bind(new(dashboardEvent.DashboardTelemetryRestHandler), new(*dashboardEvent.DashboardTelemetryRestHandlerImpl)), @@ -120,6 +127,15 @@ func InitializeApp() (*App, error) { //binding argoUserService to helm via dummy implementation(HelmUserServiceImpl) argo.NewHelmUserServiceImpl, wire.Bind(new(argo.ArgoUserService), new(*argo.HelmUserServiceImpl)), + + router.NewUserAttributesRouterImpl, + wire.Bind(new(router.UserAttributesRouter), new(*router.UserAttributesRouterImpl)), + restHandler.NewUserAttributesRestHandlerImpl, + wire.Bind(new(restHandler.UserAttributesRestHandler), new(*restHandler.UserAttributesRestHandlerImpl)), + attributes.NewUserAttributesServiceImpl, + wire.Bind(new(attributes.UserAttributesService), new(*attributes.UserAttributesServiceImpl)), + repository.NewUserAttributesRepositoryImpl, + wire.Bind(new(repository.UserAttributesRepository), new(*repository.UserAttributesRepositoryImpl)), ) return &App{}, nil } diff --git a/cmd/external-app/wire_gen.go b/cmd/external-app/wire_gen.go index 66ed6539d5..1c1f12ceee 100644 --- a/cmd/external-app/wire_gen.go +++ b/cmd/external-app/wire_gen.go @@ -21,6 +21,8 @@ import ( externalLink2 "github.com/devtron-labs/devtron/api/externalLink" client2 "github.com/devtron-labs/devtron/api/helm-app" module2 "github.com/devtron-labs/devtron/api/module" + "github.com/devtron-labs/devtron/api/restHandler" + "github.com/devtron-labs/devtron/api/router" server2 "github.com/devtron-labs/devtron/api/server" sso2 "github.com/devtron-labs/devtron/api/sso" team2 "github.com/devtron-labs/devtron/api/team" @@ -271,7 +273,13 @@ func InitializeApp() (*App, error) { webhookHelmServiceImpl := webhookHelm.NewWebhookHelmServiceImpl(sugaredLogger, helmAppServiceImpl, clusterServiceImpl, chartRepositoryServiceImpl, attributesServiceImpl) webhookHelmRestHandlerImpl := webhookHelm2.NewWebhookHelmRestHandlerImpl(sugaredLogger, webhookHelmServiceImpl, userServiceImpl, enforcerImpl, validate) webhookHelmRouterImpl := webhookHelm2.NewWebhookHelmRouterImpl(webhookHelmRestHandlerImpl) - muxRouter := NewMuxRouter(sugaredLogger, ssoLoginRouterImpl, teamRouterImpl, userAuthRouterImpl, userRouterImpl, clusterRouterImpl, dashboardRouterImpl, helmAppRouterImpl, environmentRouterImpl, k8sApplicationRouterImpl, chartRepositoryRouterImpl, appStoreDiscoverRouterImpl, appStoreValuesRouterImpl, appStoreDeploymentRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, moduleRouterImpl, serverRouterImpl, apiTokenRouterImpl, k8sCapacityRouterImpl, webhookHelmRouterImpl) + userAttributesRepositoryImpl := repository4.NewUserAttributesRepositoryImpl(db) + userAttributesServiceImpl := attributes.NewUserAttributesServiceImpl(sugaredLogger, userAttributesRepositoryImpl) + userAttributesRestHandlerImpl := restHandler.NewUserAttributesRestHandlerImpl(sugaredLogger, enforcerImpl, userServiceImpl, userAttributesServiceImpl) + userAttributesRouterImpl := router.NewUserAttributesRouterImpl(userAttributesRestHandlerImpl) + telemetryRestHandlerImpl := restHandler.NewTelemetryRestHandlerImpl(sugaredLogger, telemetryEventClientImpl, enforcerImpl, userServiceImpl) + telemetryRouterImpl := router.NewTelemetryRouterImpl(sugaredLogger, telemetryRestHandlerImpl) + muxRouter := NewMuxRouter(sugaredLogger, ssoLoginRouterImpl, teamRouterImpl, userAuthRouterImpl, userRouterImpl, clusterRouterImpl, dashboardRouterImpl, helmAppRouterImpl, environmentRouterImpl, k8sApplicationRouterImpl, chartRepositoryRouterImpl, appStoreDiscoverRouterImpl, appStoreValuesRouterImpl, appStoreDeploymentRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, moduleRouterImpl, serverRouterImpl, apiTokenRouterImpl, k8sCapacityRouterImpl, webhookHelmRouterImpl, userAttributesRouterImpl, telemetryRouterImpl) mainApp := NewApp(db, sessionManager, muxRouter, telemetryEventClientImpl, sugaredLogger) return mainApp, nil } diff --git a/internal/sql/repository/UserAttributesRepository.go b/internal/sql/repository/UserAttributesRepository.go new file mode 100644 index 0000000000..b7b7976dc3 --- /dev/null +++ b/internal/sql/repository/UserAttributesRepository.go @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package repository + +import ( + "encoding/json" + "github.com/devtron-labs/devtron/pkg/sql" + "github.com/go-pg/pg" + "time" +) + +type UserAttributes struct { + tableName struct{} `sql:"user_attributes" pg:",discard_unknown_columns"` + EmailId string `sql:"email_id,pk"` + UserData string `sql:"user_data,notnull"` + sql.AuditLog +} + +type UserAttributesDao struct { + EmailId string `json:"emailId"` + Key string `json:"key"` + Value string `json:"value"` + UserId int32 `json:"-"` +} + +type UserAttributesRepository interface { + GetConnection() (dbConnection *pg.DB) + AddUserAttribute(attrDto *UserAttributesDao) (*UserAttributesDao, error) + UpdateDataValByKey(attrDto *UserAttributesDao) error + GetDataValueByKey(attrDto *UserAttributesDao) (string, error) +} + +type UserAttributesRepositoryImpl struct { + dbConnection *pg.DB +} + +func NewUserAttributesRepositoryImpl(dbConnection *pg.DB) *UserAttributesRepositoryImpl { + return &UserAttributesRepositoryImpl{dbConnection: dbConnection} +} + +func (impl *UserAttributesRepositoryImpl) GetConnection() (dbConnection *pg.DB) { + return impl.dbConnection +} + +func (repo UserAttributesRepositoryImpl) AddUserAttribute(attrDto *UserAttributesDao) (*UserAttributesDao, error) { + userDataMap := make(map[string]interface{}) + userDataMap[attrDto.Key] = attrDto.Value + userAttr := UserAttributes{} + userAttr.EmailId = attrDto.EmailId + userDataJson, err := json.Marshal(userDataMap) + if err != nil { + return nil, err + } + userAttr.UserData = string(userDataJson) + userAttr.CreatedBy = attrDto.UserId + userAttr.UpdatedBy = attrDto.UserId + userAttr.CreatedOn = time.Now() + userAttr.UpdatedOn = time.Now() + + err = repo.dbConnection.Insert(&userAttr) + if err != nil { + return nil, err + } + + return attrDto, nil +} + +func (repo UserAttributesRepositoryImpl) UpdateDataValByKey(attrDto *UserAttributesDao) error { + var userAttr = &UserAttributes{} + keyValMap := make(map[string]string) + keyValMap[attrDto.Key] = attrDto.Value + updatedValJson, err := json.Marshal(keyValMap) + if err != nil { + return err + } + query := "update user_attributes SET user_data = user_data::jsonb - ? || ? where email_id = ?" + + _, err = repo.dbConnection. + Query(userAttr, query, attrDto.Key, string(updatedValJson), attrDto.EmailId) + return err +} + +func (repo UserAttributesRepositoryImpl) GetDataValueByKey(attrDto *UserAttributesDao) (string, error) { + model := &UserAttributes{} + err := repo.dbConnection.Model(model).Where("email_id = ?", attrDto.EmailId). + Select() + if err != nil { + return "", err + } + data := model.UserData + var jsonMap map[string]interface{} + err = json.Unmarshal([]byte(data), &jsonMap) + if err != nil { + return "", err + } + dataVal := jsonMap[attrDto.Key] + var response = "" + if dataVal != nil { + response = dataVal.(string) + } + return response, err +} diff --git a/pkg/attributes/UserAttributesService.go b/pkg/attributes/UserAttributesService.go new file mode 100644 index 0000000000..70d1cf85f4 --- /dev/null +++ b/pkg/attributes/UserAttributesService.go @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package attributes + +import ( + "errors" + "github.com/devtron-labs/devtron/internal/sql/repository" + "github.com/go-pg/pg" + "go.uber.org/zap" +) + +type UserAttributesService interface { + AddUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) + UpdateUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) + GetUserAttribute(request *UserAttributesDto) (*UserAttributesDto, error) +} + +type UserAttributesServiceImpl struct { + logger *zap.SugaredLogger + attributesRepository repository.UserAttributesRepository +} + +type UserAttributesDto struct { + EmailId string `json:"emailId"` + Key string `json:"key"` + Value string `json:"value"` + UserId int32 `json:"-"` +} + +func NewUserAttributesServiceImpl(logger *zap.SugaredLogger, + attributesRepository repository.UserAttributesRepository) *UserAttributesServiceImpl { + serviceImpl := &UserAttributesServiceImpl{ + logger: logger, + attributesRepository: attributesRepository, + } + return serviceImpl +} + +func (impl UserAttributesServiceImpl) AddUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) { + dao := &repository.UserAttributesDao{ + EmailId: request.EmailId, + Key: request.Key, + Value: request.Value, + UserId: request.UserId, + } + _, err := impl.attributesRepository.AddUserAttribute(dao) + if err != nil { + impl.logger.Errorw("error in creating new user attributes for req", "req", request, "error", err) + return nil, errors.New("error occurred while creating user attributes") + } + return request, nil +} + +func (impl UserAttributesServiceImpl) UpdateUserAttributes(request *UserAttributesDto) (*UserAttributesDto, error) { + + userAttribute, err := impl.GetUserAttribute(request) + if err != nil { + impl.logger.Errorw("error while getting user attributes during update request", "req", request, "error", err) + return nil, errors.New("error occurred while updating user attributes") + } + if userAttribute == nil { + impl.logger.Info("not data found for request, so going to add instead of update", "req", request) + attributes, err := impl.AddUserAttributes(request) + if err != nil { + impl.logger.Errorw("error in adding new user attributes", "req", request, "error", err) + return nil, errors.New("error occurred while updating user attributes") + } + return attributes, nil + } + dao := &repository.UserAttributesDao{ + EmailId: request.EmailId, + Key: request.Key, + Value: request.Value, + UserId: request.UserId, + } + err = impl.attributesRepository.UpdateDataValByKey(dao) + if err != nil { + impl.logger.Errorw("error in update new attributes", "req", request, "error", err) + return nil, errors.New("error occurred while updating user attributes") + } + return request, nil +} + +func (impl UserAttributesServiceImpl) GetUserAttribute(request *UserAttributesDto) (*UserAttributesDto, error) { + + dao := &repository.UserAttributesDao{ + EmailId: request.EmailId, + Key: request.Key, + Value: request.Value, + UserId: request.UserId, + } + modelValue, err := impl.attributesRepository.GetDataValueByKey(dao) + if err == pg.ErrNoRows { + return nil, nil + } + if err != nil { + impl.logger.Errorw("error in fetching user attributes", "req", request, "error", err) + return nil, errors.New("error occurred while getting user attributes") + } + resAttrDto := &UserAttributesDto{ + EmailId: request.EmailId, + Key: request.Key, + Value: modelValue, + } + return resAttrDto, nil +} diff --git a/scripts/sql/73_insert_user_attributes.down.sql b/scripts/sql/73_insert_user_attributes.down.sql new file mode 100644 index 0000000000..5eccfd5d6a --- /dev/null +++ b/scripts/sql/73_insert_user_attributes.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS "public"."user_attributes"; \ No newline at end of file diff --git a/scripts/sql/73_insert_user_attributes.up.sql b/scripts/sql/73_insert_user_attributes.up.sql new file mode 100644 index 0000000000..4bb4a151cf --- /dev/null +++ b/scripts/sql/73_insert_user_attributes.up.sql @@ -0,0 +1,16 @@ +-- +-- Name: user_attributes; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE IF NOT EXISTS public.user_attributes ( + email_id varchar(500) NOT NULL, + user_data json NOT NULL, + created_on timestamp with time zone, + updated_on timestamp with time zone, + created_by integer, + updated_by integer, + PRIMARY KEY ("email_id") +); + + +ALTER TABLE public.user_attributes OWNER TO postgres; \ No newline at end of file diff --git a/wire_gen.go b/wire_gen.go index f5aaef8504..5201295bc6 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -520,6 +520,10 @@ func InitializeApp() (*App, error) { dashboardRouterImpl := dashboard.NewDashboardRouterImpl(sugaredLogger, dashboardConfig) attributesRestHandlerImpl := restHandler.NewAttributesRestHandlerImpl(sugaredLogger, enforcerImpl, userServiceImpl, attributesServiceImpl) attributesRouterImpl := router.NewAttributesRouterImpl(attributesRestHandlerImpl) + userAttributesRepositoryImpl := repository.NewUserAttributesRepositoryImpl(db) + userAttributesServiceImpl := attributes.NewUserAttributesServiceImpl(sugaredLogger, userAttributesRepositoryImpl) + userAttributesRestHandlerImpl := restHandler.NewUserAttributesRestHandlerImpl(sugaredLogger, enforcerImpl, userServiceImpl, userAttributesServiceImpl) + userAttributesRouterImpl := router.NewUserAttributesRouterImpl(userAttributesRestHandlerImpl) commonRestHanlderImpl := restHandler.NewCommonRestHanlderImpl(sugaredLogger, gitOpsConfigServiceImpl, userServiceImpl, validate, enforcerImpl, commonServiceImpl) commonRouterImpl := router.NewCommonRouterImpl(commonRestHanlderImpl) grafanaConfig, err := grafana.GetConfig() @@ -539,7 +543,7 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - telemetryRestHandlerImpl := restHandler.NewTelemetryRestHandlerImpl(sugaredLogger, telemetryEventClientImplExtended) + telemetryRestHandlerImpl := restHandler.NewTelemetryRestHandlerImpl(sugaredLogger, telemetryEventClientImplExtended, enforcerImpl, userServiceImpl) telemetryRouterImpl := router.NewTelemetryRouterImpl(sugaredLogger, telemetryRestHandlerImpl) bulkUpdateRepositoryImpl := bulkUpdate.NewBulkUpdateRepository(db, sugaredLogger) bulkUpdateServiceImpl := pipeline.NewBulkUpdateServiceImpl(bulkUpdateRepositoryImpl, chartRepositoryImpl, sugaredLogger, chartTemplateServiceImpl, chartRepoRepositoryImpl, defaultChart, utilMergeUtil, repositoryServiceClientImpl, chartRefRepositoryImpl, envConfigOverrideRepositoryImpl, pipelineConfigRepositoryImpl, configMapRepositoryImpl, environmentRepositoryImpl, pipelineRepositoryImpl, appLevelMetricsRepositoryImpl, envLevelAppMetricsRepositoryImpl, httpClient, appRepositoryImpl, deploymentTemplateHistoryServiceImpl, configMapHistoryServiceImpl) @@ -617,7 +621,7 @@ func InitializeApp() (*App, error) { webhookHelmServiceImpl := webhookHelm.NewWebhookHelmServiceImpl(sugaredLogger, helmAppServiceImpl, clusterServiceImplExtended, chartRepositoryServiceImpl, attributesServiceImpl) webhookHelmRestHandlerImpl := webhookHelm2.NewWebhookHelmRestHandlerImpl(sugaredLogger, webhookHelmServiceImpl, userServiceImpl, enforcerImpl, validate) webhookHelmRouterImpl := webhookHelm2.NewWebhookHelmRouterImpl(webhookHelmRestHandlerImpl) - muxRouter := router.NewMuxRouter(sugaredLogger, helmRouterImpl, pipelineConfigRouterImpl, migrateDbRouterImpl, appListingRouterImpl, environmentRouterImpl, clusterRouterImpl, webhookRouterImpl, userAuthRouterImpl, applicationRouterImpl, cdRouterImpl, projectManagementRouterImpl, gitProviderRouterImpl, gitHostRouterImpl, dockerRegRouterImpl, notificationRouterImpl, teamRouterImpl, gitWebhookHandlerImpl, workflowStatusUpdateHandlerImpl, applicationStatusUpdateHandlerImpl, ciEventHandlerImpl, pubSubClient, userRouterImpl, chartRefRouterImpl, configMapRouterImpl, appStoreRouterImpl, chartRepositoryRouterImpl, releaseMetricsRouterImpl, deploymentGroupRouterImpl, batchOperationRouterImpl, chartGroupRouterImpl, testSuitRouterImpl, imageScanRouterImpl, policyRouterImpl, gitOpsConfigRouterImpl, dashboardRouterImpl, attributesRouterImpl, commonRouterImpl, grafanaRouterImpl, ssoLoginRouterImpl, telemetryRouterImpl, telemetryEventClientImplExtended, bulkUpdateRouterImpl, webhookListenerRouterImpl, appLabelRouterImpl, coreAppRouterImpl, helmAppRouterImpl, k8sApplicationRouterImpl, pProfRouterImpl, deploymentConfigRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, globalPluginRouterImpl, moduleRouterImpl, serverRouterImpl, apiTokenRouterImpl, cdApplicationStatusUpdateHandlerImpl, k8sCapacityRouterImpl, webhookHelmRouterImpl) + muxRouter := router.NewMuxRouter(sugaredLogger, helmRouterImpl, pipelineConfigRouterImpl, migrateDbRouterImpl, appListingRouterImpl, environmentRouterImpl, clusterRouterImpl, webhookRouterImpl, userAuthRouterImpl, applicationRouterImpl, cdRouterImpl, projectManagementRouterImpl, gitProviderRouterImpl, gitHostRouterImpl, dockerRegRouterImpl, notificationRouterImpl, teamRouterImpl, gitWebhookHandlerImpl, workflowStatusUpdateHandlerImpl, applicationStatusUpdateHandlerImpl, ciEventHandlerImpl, pubSubClient, userRouterImpl, chartRefRouterImpl, configMapRouterImpl, appStoreRouterImpl, chartRepositoryRouterImpl, releaseMetricsRouterImpl, deploymentGroupRouterImpl, batchOperationRouterImpl, chartGroupRouterImpl, testSuitRouterImpl, imageScanRouterImpl, policyRouterImpl, gitOpsConfigRouterImpl, dashboardRouterImpl, attributesRouterImpl, userAttributesRouterImpl, commonRouterImpl, grafanaRouterImpl, ssoLoginRouterImpl, telemetryRouterImpl, telemetryEventClientImplExtended, bulkUpdateRouterImpl, webhookListenerRouterImpl, appLabelRouterImpl, coreAppRouterImpl, helmAppRouterImpl, k8sApplicationRouterImpl, pProfRouterImpl, deploymentConfigRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, globalPluginRouterImpl, moduleRouterImpl, serverRouterImpl, apiTokenRouterImpl, cdApplicationStatusUpdateHandlerImpl, k8sCapacityRouterImpl, webhookHelmRouterImpl) mainApp := NewApp(muxRouter, sugaredLogger, sseSSE, versionServiceImpl, syncedEnforcer, db, pubSubClient, sessionManager) return mainApp, nil }