From 9d290fdec5ee9303435f8d2eca37aa9e4a28c4ee Mon Sep 17 00:00:00 2001 From: shreeharsha-factly Date: Thu, 20 Apr 2023 13:15:27 +0530 Subject: [PATCH 1/3] admin endpoint to create user --- server/action/admin/user/create.go | 103 +++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 server/action/admin/user/create.go diff --git a/server/action/admin/user/create.go b/server/action/admin/user/create.go new file mode 100644 index 00000000..320cb30c --- /dev/null +++ b/server/action/admin/user/create.go @@ -0,0 +1,103 @@ +package user + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + "github.com/factly/kavach-server/model" + "github.com/factly/x/errorx" + "github.com/factly/x/loggerx" + "github.com/factly/x/renderx" + "github.com/factly/x/slugx" + "github.com/spf13/viper" +) + +// create organisation +func create(w http.ResponseWriter, r *http.Request) { + user := user{} + + err := json.NewDecoder(r.Body).Decode(&user) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DecodeError())) + return + } + + displayName := "" + if user.DisplayName == "" { + displayName = user.FirstName + if user.LastName != "" { + displayName = fmt.Sprint(displayName, " ", user.LastName) + } + } + + identity := map[string]interface{}{ + "traits": map[string]interface{}{ + "email": user.Email, + "name": map[string]interface{}{ + "first": user.FirstName, + "last": user.LastName, + }, + }, + "schema_id": "default", + "credentials": map[string]interface{}{ + "password": map[string]interface{}{ + "config": map[string]interface{}{ + "password": user.Password, + }, + }, + }, + } + + buf := new(bytes.Buffer) + err = json.NewEncoder(buf).Encode(&identity) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DecodeError())) + return + } + + req, err := http.NewRequest("POST", viper.GetString("kratos_admin_url")+"/admin/identities", buf) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + client := &http.Client{} + response, err := client.Do(req) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + + responseBody := make(map[string]interface{}) + + err = json.NewDecoder(response.Body).Decode(&responseBody) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DecodeError())) + return + } + + result := model.User{ + Email: user.Email, + KID: fmt.Sprint(responseBody["id"]), + FirstName: user.FirstName, + LastName: user.LastName, + DisplayName: displayName, + Slug: slugx.Make(fmt.Sprint(user.FirstName, " ", user.LastName)), + } + + // check whether user exists + err = model.DB.Model(&model.User{}).Create(&result).Error + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + renderx.JSON(w, http.StatusOK, result) +} From 48aaabdd6d5dc77ba60d5ca9aa580f87fa14d79e Mon Sep 17 00:00:00 2001 From: shreeharsha-factly Date: Thu, 20 Apr 2023 13:16:24 +0530 Subject: [PATCH 2/3] admin endpoint to create and update organisation --- server/action/admin/organisation/create.go | 130 +++++++++++++++++++++ server/action/admin/organisation/update.go | 104 +++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 server/action/admin/organisation/create.go create mode 100644 server/action/admin/organisation/update.go diff --git a/server/action/admin/organisation/create.go b/server/action/admin/organisation/create.go new file mode 100644 index 00000000..9f916300 --- /dev/null +++ b/server/action/admin/organisation/create.go @@ -0,0 +1,130 @@ +package organisation + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + + "github.com/factly/kavach-server/model" + keto "github.com/factly/kavach-server/util/keto/relationTuple" + "github.com/factly/x/errorx" + "github.com/factly/x/loggerx" + "github.com/factly/x/renderx" + "github.com/factly/x/validationx" +) + +type organisation struct { + Title string `json:"title" validate:"required"` + Slug string `json:"slug"` + Description string `json:"description"` + FeaturedMediumID uint `json:"featured_medium_id"` + IsIndividual bool `json:"is_individual"` + UserID uint `json:"user_id"` +} + +// create - Create organisation +// @Summary Create organisation +// @Description Create organisation +// @Tags Organisation +// @ID add-organisation +// @Consume json +// @Produce json +// @Param X-User header string true "User ID" +// @Param Organisation body organisation true "Organisation Object" +// @Success 201 {object} orgWithRole +// @Failure 400 {array} string +// @Router /organisations [post] +func create(w http.ResponseWriter, r *http.Request) { + userID, err := strconv.Atoi(r.Header.Get("X-User")) + if err != nil { + errorx.Render(w, errorx.Parser(errorx.InvalidID())) + return + } + + org := &organisation{} + + err = json.NewDecoder(r.Body).Decode(&org) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DecodeError())) + return + } + + validationError := validationx.Check(org) + if validationError != nil { + loggerx.Error(errors.New("validation error")) + errorx.Render(w, validationError) + return + } + + mediumID := &org.FeaturedMediumID + if org.FeaturedMediumID == 0 { + mediumID = nil + } + + tx := model.DB.WithContext(context.WithValue(r.Context(), userContext, userID)).Begin() + + organisation := &model.Organisation{ + Title: org.Title, + Slug: org.Slug, + Description: org.Description, + FeaturedMediumID: mediumID, + IsIndividual: org.IsIndividual, + } + + err = tx.Model(&model.Organisation{}).Create(&organisation).Error + + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + tx.Model(&model.Organisation{}).Preload("Medium").First(&organisation) + + permission := model.OrganisationUser{} + permission.OrganisationID = uint(organisation.ID) + permission.UserID = uint(org.UserID) + permission.Role = "owner" + + err = tx.Model(&model.OrganisationUser{}).Create(&permission).Error + + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + // creating the organisation-role: owner, on the keto api + tuple := &model.KetoRelationTupleWithSubjectID{ + KetoSubjectSet: model.KetoSubjectSet{ + Namespace: "organisations", + Object: fmt.Sprintf("org:%d", organisation.ID), + Relation: "owner", + }, + SubjectID: fmt.Sprintf("%d", org.UserID), + } + + err = keto.CreateRelationTupleWithSubjectID(tuple) + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + + result := model.Organisation{} + + result = *organisation + + result.OrganisationUsers = []model.OrganisationUser{permission} + + tx.Commit() + + renderx.JSON(w, http.StatusCreated, result) +} diff --git a/server/action/admin/organisation/update.go b/server/action/admin/organisation/update.go new file mode 100644 index 00000000..859b9af1 --- /dev/null +++ b/server/action/admin/organisation/update.go @@ -0,0 +1,104 @@ +package organisation + +import ( + "context" + "encoding/json" + "net/http" + "strconv" + + "github.com/factly/kavach-server/model" + "github.com/factly/x/errorx" + "github.com/factly/x/loggerx" + "github.com/factly/x/renderx" + "github.com/go-chi/chi" +) + +func update(w http.ResponseWriter, r *http.Request) { + req := organisation{} + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DecodeError())) + return + } + + organisationID := chi.URLParam(r, "organisation_id") + orgID, err := strconv.Atoi(organisationID) + + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InvalidID())) + return + } + + hostID, err := strconv.Atoi(r.Header.Get("X-User")) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InvalidID())) + return + } + + organisation := &model.Organisation{} + organisation.ID = uint(orgID) + + // check record exists or not + err = model.DB.First(&organisation).Error + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.RecordNotFound())) + return + } + + tx := model.DB.WithContext(context.WithValue(r.Context(), userContext, hostID)).Begin() + + mediumID := &req.FeaturedMediumID + organisation.FeaturedMediumID = &req.FeaturedMediumID + if req.FeaturedMediumID == 0 { + err = tx.Model(&organisation).Updates(map[string]interface{}{"featured_medium_id": nil}).First(&organisation).Error + mediumID = nil + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + } + + // update + updateMap := map[string]interface{}{ + "updated_by_id": uint(hostID), + "title": req.Title, + "slug": req.Slug, + "description": req.Description, + "featured_medium_id": mediumID, + "is_individual": req.IsIndividual, + } + + err = tx.Model(&organisation).Updates(&updateMap).Preload("Medium").First(&organisation).Error + + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + // fetching permissions of the user + permission := &model.OrganisationUser{} + err = tx.Model(&model.OrganisationUser{}).Where(&model.OrganisationUser{ + OrganisationID: uint(orgID), + }).First(permission).Error + + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.RecordNotFound())) + return + } + result := model.Organisation{} + result = *organisation + result.OrganisationUsers = []model.OrganisationUser{*permission} + + tx.Commit() + + renderx.JSON(w, http.StatusOK, result) +} From aae91a7af4c9d066f10ca39fecec9651474da077 Mon Sep 17 00:00:00 2001 From: shreeharsha-factly Date: Thu, 20 Apr 2023 13:22:14 +0530 Subject: [PATCH 3/3] admin endpoint to fetch user and organisation list --- server/action/admin/organisation/details.go | 46 +++++++++++++++++++++ server/action/admin/organisation/list.go | 34 +++++++++++++++ server/action/admin/organisation/routes.go | 22 ++++++++++ server/action/admin/route.go | 38 +++++++++++++++++ server/action/admin/user/list.go | 35 ++++++++++++++++ server/action/admin/user/route.go | 23 +++++++++++ server/action/routes.go | 2 + 7 files changed, 200 insertions(+) create mode 100644 server/action/admin/organisation/details.go create mode 100644 server/action/admin/organisation/list.go create mode 100644 server/action/admin/organisation/routes.go create mode 100644 server/action/admin/route.go create mode 100644 server/action/admin/user/list.go create mode 100644 server/action/admin/user/route.go diff --git a/server/action/admin/organisation/details.go b/server/action/admin/organisation/details.go new file mode 100644 index 00000000..69bcacd7 --- /dev/null +++ b/server/action/admin/organisation/details.go @@ -0,0 +1,46 @@ +package organisation + +import ( + "net/http" + "strconv" + + "github.com/factly/kavach-server/model" + "github.com/factly/x/errorx" + "github.com/factly/x/loggerx" + "github.com/factly/x/renderx" + "github.com/go-chi/chi" +) + +// details - Get organisation by id +// @Summary Show a organisation by id +// @Description Get organisation by ID +// @Tags Organisation +// @ID get-organisation-by-id +// @Produce json +// @Param X-User header string true "User ID" +// @Param organisation_id path string true "Organisation ID" +// @Success 200 {object} orgWithRole +// @Router /organisations/{organisation_id} [get] +func details(w http.ResponseWriter, r *http.Request) { + organisationID := chi.URLParam(r, "organisation_id") + id, err := strconv.Atoi(organisationID) + + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InvalidID())) + return + } + + organisation := &model.Organisation{} + organisation.ID = uint(id) + + err = model.DB.Model(&model.Organisation{}).Preload("OrganisationUsers").Preload("OrganisationUsers.User").First(&organisation).Error + + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.RecordNotFound())) + return + } + + renderx.JSON(w, http.StatusOK, organisation) +} diff --git a/server/action/admin/organisation/list.go b/server/action/admin/organisation/list.go new file mode 100644 index 00000000..bed96abf --- /dev/null +++ b/server/action/admin/organisation/list.go @@ -0,0 +1,34 @@ +package organisation + +import ( + "net/http" + + "github.com/factly/kavach-server/model" + "github.com/factly/x/errorx" + "github.com/factly/x/loggerx" + "github.com/factly/x/paginationx" + "github.com/factly/x/renderx" +) + +func list(w http.ResponseWriter, r *http.Request) { + + orgIDs := r.URL.Query()["id"] + + offset, limit := paginationx.Parse(r.URL.Query()) + + if len(orgIDs) > 0 { + offset = 0 + limit = len(orgIDs) + } + + res := make([]model.Organisation, 0) + + err := model.DB.Model(&model.Organisation{}).Where(orgIDs).Offset(offset).Limit(limit).Find(&res).Error + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + renderx.JSON(w, http.StatusOK, res) +} diff --git a/server/action/admin/organisation/routes.go b/server/action/admin/organisation/routes.go new file mode 100644 index 00000000..6c665900 --- /dev/null +++ b/server/action/admin/organisation/routes.go @@ -0,0 +1,22 @@ +package organisation + +import ( + "github.com/factly/kavach-server/model" + "github.com/go-chi/chi" +) + +var userContext model.ContextKey = "organisation_user" + +// Router organisation +func Router() chi.Router { + r := chi.NewRouter() + + r.Post("/", create) + r.Get("/", list) + r.Route("/{organisation_id}", func(r chi.Router) { + r.Put("/", update) + r.Get("/", details) + }) + + return r +} diff --git a/server/action/admin/route.go b/server/action/admin/route.go new file mode 100644 index 00000000..afbf0f8f --- /dev/null +++ b/server/action/admin/route.go @@ -0,0 +1,38 @@ +package admin + +import ( + "log" + "net/http" + + "github.com/factly/kavach-server/action/admin/organisation" + "github.com/factly/kavach-server/action/admin/user" + "github.com/go-chi/chi" + "github.com/spf13/viper" +) + +// Router organisation +func AdminRouter() chi.Router { + r := chi.NewRouter() + + r.With(CheckMasterKey).Route("/", func(r chi.Router) { + r.Mount("/users", user.Router()) + r.Mount("/organisations", organisation.Router()) + }) + + return r +} + +// CheckMasterKey check X-User in header +func CheckMasterKey(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestMasterKey := r.Header.Get("X-KAVACH-MASTER-KEY") + + log.Println(requestMasterKey) + log.Println(viper.GetString("master_key")) + if requestMasterKey != viper.GetString("master_key") { + w.WriteHeader(http.StatusUnauthorized) + return + } + h.ServeHTTP(w, r) + }) +} diff --git a/server/action/admin/user/list.go b/server/action/admin/user/list.go new file mode 100644 index 00000000..156b4416 --- /dev/null +++ b/server/action/admin/user/list.go @@ -0,0 +1,35 @@ +package user + +import ( + "net/http" + + "github.com/factly/kavach-server/model" + "github.com/factly/x/errorx" + "github.com/factly/x/loggerx" + "github.com/factly/x/paginationx" + "github.com/factly/x/renderx" +) + +func list(w http.ResponseWriter, r *http.Request) { + + userIDs := r.URL.Query()["id"] + + offset, limit := paginationx.Parse(r.URL.Query()) + + if len(userIDs) > 0 { + offset = 0 + limit = len(userIDs) + } + + result := make([]model.User, 0) + + err := model.DB.Model(&model.User{}).Preload("Organisations").Where(userIDs).Offset(offset).Limit(limit).Find(&result).Error + if err != nil { + + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + renderx.JSON(w, http.StatusOK, result) +} diff --git a/server/action/admin/user/route.go b/server/action/admin/user/route.go new file mode 100644 index 00000000..f7877ee5 --- /dev/null +++ b/server/action/admin/user/route.go @@ -0,0 +1,23 @@ +package user + +import "github.com/go-chi/chi" + +// user - user payload +type user struct { + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + DisplayName string `json:"display_name"` + Gender string `json:"gender"` + Password string `json:"password"` +} + +// AdminRouter user +func Router() chi.Router { + r := chi.NewRouter() + + r.Post("/", create) + r.Get("/", list) + + return r +} diff --git a/server/action/routes.go b/server/action/routes.go index 26876484..c43bda55 100644 --- a/server/action/routes.go +++ b/server/action/routes.go @@ -6,6 +6,7 @@ import ( "github.com/factly/kavach-server/util/keto" "github.com/factly/x/healthx" + "github.com/factly/kavach-server/action/admin" "github.com/factly/kavach-server/action/medium" "github.com/factly/kavach-server/action/organisation" "github.com/factly/kavach-server/action/organisation/application/space/token" @@ -46,6 +47,7 @@ func RegisterRoutes() http.Handler { r.Mount("/util", util.Router()) r.Post("/spaces/{space_id}/validateToken", token.Validate) r.Mount("/sessions", sessions.Router()) + r.Mount("/admin", admin.AdminRouter()) sqlDB, _ := model.DB.DB() healthx.RegisterRoutes(r, healthx.ReadyCheckers{