diff --git a/server/action/admin/organisation/user/create.go b/server/action/admin/organisation/user/create.go new file mode 100644 index 00000000..b8cf837d --- /dev/null +++ b/server/action/admin/organisation/user/create.go @@ -0,0 +1,142 @@ +package user + +// userContext is imported from route.go + +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" + "github.com/go-chi/chi" +) + +type invites struct { + // flag to send email or not, default is true + SendEmail bool `json:"send_email"` + Users []invite `json:"users"` +} + +type invite struct { + FirstName string `gorm:"column:first_name" json:"first_name" validate:"required"` + LastName string `gorm:"column:last_name" json:"last_name"` + Email string `json:"email" validate:"required"` + Role string `json:"role" validate:"required"` +} + +// create - Create organisation user (admin) +// @Summary Create organisation user (admin) +// @Description Create organisation user (admin) +// @Tags AdminOrganisationUser +// @ID add-admin-organisation-user +// @Consume json +// @Produce json +// @Param X-KAVACH-MASTER-KEY header string true "Master Key" +// @Param organisation_id path string true "Organisation ID" +// @Param Invite body invites true "Invite Object" +// @Success 201 {array} userWithPermission +// @Failure 400 {array} string +// @Router /admin/organisations/{organisation_id}/users [post] +func create(w http.ResponseWriter, r *http.Request) { + 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 + } + + // FindOrCreate invitee + req := invites{SendEmail: false} + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DecodeError())) + return + } + for _, user := range req.Users { + validationError := validationx.Check(user) + if validationError != nil { + loggerx.Error(errors.New("validation error")) + errorx.Render(w, validationError) + return + } + } + + for _, user := range req.Users { + tx := model.DB.WithContext(context.WithValue(r.Context(), userContext, 0)).Begin() + invitee := model.User{ + Email: user.Email, + FirstName: user.FirstName, + LastName: user.LastName, + } + + err = tx.Where(&model.User{ + Email: invitee.Email, + }).First(&invitee).Error + + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + // Check if user already exists in organisation + var totPermissions int64 + permission := &model.OrganisationUser{} + permission.OrganisationID = uint(orgID) + permission.UserID = invitee.ID + + tx.Model(&model.OrganisationUser{}).Where(permission).Count(&totPermissions) + if totPermissions != 0 { + tx.Rollback() + loggerx.Error(errors.New("user already exists in organisation")) + continue + } + + // Create organisation user entry + orgUser := &model.OrganisationUser{ + UserID: invitee.ID, + OrganisationID: uint(orgID), + Role: user.Role, + } + + // Creating a relation tuple for users in keto api + tuple := &model.KetoRelationTupleWithSubjectID{ + KetoSubjectSet: model.KetoSubjectSet{ + Namespace: "organisations", + Object: fmt.Sprintf("org:%d", orgID), + Relation: user.Role, + }, + SubjectID: fmt.Sprintf("%d", invitee.ID), + } + + err = keto.CreateRelationTupleWithSubjectID(tuple) + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + + // Adding user to organisation + err = tx.Model(&model.OrganisationUser{}).Create(orgUser).Error + if err != nil { + loggerx.Error(err) + tx.Rollback() + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + tx.Commit() + } + renderx.JSON(w, http.StatusOK, nil) +} diff --git a/server/action/admin/organisation/user/delete.go b/server/action/admin/organisation/user/delete.go new file mode 100644 index 00000000..8ce6573d --- /dev/null +++ b/server/action/admin/organisation/user/delete.go @@ -0,0 +1,118 @@ +package user + +import ( + "errors" + "fmt" + "net/http" + "strconv" + + keto "github.com/factly/kavach-server/util/keto/relationTuple" + "github.com/factly/kavach-server/util/user" + + "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" +) + +// delete - Delete organisation user by id (admin) +// @Summary Delete a organisation user (admin) +// @Description Delete organisation user by ID (admin) +// @Tags AdminOrganisationUser +// @ID delete-admin-organisation-user-by-id +// @Param X-KAVACH-MASTER-KEY header string true "Master Key" +// @Param organisation_id path string true "Organisation ID" +// @Param user_id path string true "User ID" +// @Success 200 +// @Router /admin/organisations/{organisation_id}/users/{user_id} [delete] +func delete(w http.ResponseWriter, r *http.Request) { + /* Check if record exist */ + 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 + } + + userID := chi.URLParam(r, "user_id") + uID, err := strconv.Atoi(userID) + + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InvalidID())) + return + } + + result := &model.OrganisationUser{} + + // Check if record exist + err = model.DB.Where(&model.OrganisationUser{ + OrganisationID: uint(orgID), + UserID: uint(uID), + }).First(&result).Error + + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.RecordNotFound())) + return + } + + // Check if the user to delete is not last owner of organisation + var totalOwners int64 + model.DB.Model(&model.OrganisationUser{}).Where(&model.OrganisationUser{ + Role: "owner", + OrganisationID: uint(orgID), + }).Count(&totalOwners) + + if result.Role == "owner" && totalOwners < 2 { + loggerx.Error(errors.New("cannot delete last user of organisation")) + errorx.Render(w, errorx.Parser(errorx.CannotSaveChanges())) + return + } + err = user.DeleteUserFromOrganisationRoles(uint(orgID), result.UserID) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + err = user.DeleteUserFromApplications(uint(orgID), result.UserID) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + err = keto.DeleteRelationTuplesOfSubjectIDInNamespace(namespace, userID, fmt.Sprintf("org:%d", orgID)) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + err = keto.DeleteRelationTuplesOfSubjectIDInNamespace("applications", userID, fmt.Sprintf("org:%d", orgID)) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + err = keto.DeleteRelationTuplesOfSubjectIDInNamespace("spaces", userID, fmt.Sprintf("org:%d", orgID)) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.InternalServerError())) + return + } + + deletePermission := &model.OrganisationUser{} + deletePermission.ID = result.ID + + /* DELETE */ + err = model.DB.Delete(&deletePermission).Error + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + renderx.JSON(w, http.StatusOK, nil) +} diff --git a/server/action/admin/organisation/user/list.go b/server/action/admin/organisation/user/list.go new file mode 100644 index 00000000..1f812041 --- /dev/null +++ b/server/action/admin/organisation/user/list.go @@ -0,0 +1,99 @@ +package user + +import ( + "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/go-chi/chi" +) + +// list - Get all organisations users (admin) +// @Summary Show all organisations users (admin) +// @Description Get all organisations users (admin) +// @Tags AdminOrganisationUser +// @ID get-all-admin-organisations-users +// @Produce json +// @Param X-KAVACH-MASTER-KEY header string true "Master Key" +// @Param organisation_id path string true "Organisation ID" +// @Success 200 {array} []userWithPermission +// @Router /admin/organisations/{organisation_id}/users [get] +func list(w http.ResponseWriter, r *http.Request) { + 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 + } + + role := r.URL.Query().Get("role") + + var userIDs []string + + if role == "owner" || role == "member" { + userIDs, err = keto.ListSubjectsByObjectID(namespace, role, fmt.Sprintf("org:%d", orgID)) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.RecordNotFound())) + return + } + } else { + userIDs, err = keto.ListSubjectsByObjectID(namespace, "", fmt.Sprintf("org:%d", orgID)) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.RecordNotFound())) + return + } + } + + result := make([]userWithPermission, 0) + for _, userID := range userIDs { + var user userWithPermission + + uID, err := strconv.Atoi(userID) + if err != nil { + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DecodeError())) + return + } + + tx := model.DB.Begin() + var userModel model.User + err = tx.Model(&model.User{}).Where(&model.User{ + Base: model.Base{ + ID: uint(uID), + }, + }).Preload("Medium").First(&userModel).Error + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + var userPermissions model.OrganisationUser + err = tx.Model(&model.OrganisationUser{}).Where(&model.OrganisationUser{ + OrganisationID: uint(orgID), + UserID: uint(uID), + }).First(&userPermissions).Error + if err != nil { + tx.Rollback() + loggerx.Error(err) + errorx.Render(w, errorx.Parser(errorx.DBError())) + return + } + + user.User = userModel + user.Permission = userPermissions + result = append(result, user) + tx.Commit() + } + + renderx.JSON(w, http.StatusOK, result) +} diff --git a/server/action/admin/organisation/user/route.go b/server/action/admin/organisation/user/route.go new file mode 100644 index 00000000..17829cd9 --- /dev/null +++ b/server/action/admin/organisation/user/route.go @@ -0,0 +1,28 @@ +package user + +import ( + "github.com/factly/kavach-server/model" + "github.com/go-chi/chi" +) + +type userWithPermission struct { + model.User + Permission model.OrganisationUser `json:"permission"` +} + +const namespace string = "organisations" + +var userContext model.ContextKey = "organisation_user" + +// Router organisation user routes for admin +func Router() chi.Router { + r := chi.NewRouter() + + r.Get("/", list) + r.Post("/", create) + r.Route("/{user_id}", func(r chi.Router) { + r.Delete("/", delete) + }) + + return r +}