package service import ( "sort" "time" "voltaserve/cache" "voltaserve/config" "voltaserve/errorpkg" "voltaserve/guard" "voltaserve/helper" "voltaserve/model" "voltaserve/repo" "voltaserve/search" "github.com/gofiber/fiber/v2/log" ) type Organization struct { ID string `json:"id"` Name string `json:"name"` Image *string `json:"image,omitempty"` Permission string `json:"permission"` CreateTime string `json:"createTime"` UpdateTime *string `json:"updateTime,omitempty"` } type OrganizationList struct { Data []*Organization `json:"data"` TotalPages uint `json:"totalPages"` TotalElements uint `json:"totalElements"` Page uint `json:"page"` Size uint `json:"size"` } type OrganizationCreateOptions struct { Name string `json:"name" validate:"required,max=255"` Image *string `json:"image"` } type OrganizationListOptions struct { Query string Page uint Size uint SortBy string SortOrder string } type OrganizationUpdateNameOptions struct { Name string `json:"name" validate:"required,max=255"` } type OrganizationUpdateImageOptions struct { Image string `json:"image" validate:"required,base64"` } type OrganizationRemoveMemberOptions struct { UserID string `json:"userId" validate:"required"` } type OrganizationService struct { orgRepo repo.OrganizationRepo orgCache *cache.OrganizationCache orgGuard *guard.OrganizationGuard orgMapper *organizationMapper orgSearch *search.OrganizationSearch userRepo repo.UserRepo userSearch *search.UserSearch userMapper *userMapper groupRepo repo.GroupRepo groupService *GroupService groupMapper *groupMapper config config.Config } func NewOrganizationService() *OrganizationService { return &OrganizationService{ orgRepo: repo.NewOrganizationRepo(), orgCache: cache.NewOrganizationCache(), orgGuard: guard.NewOrganizationGuard(), orgSearch: search.NewOrganizationSearch(), orgMapper: newOrganizationMapper(), userRepo: repo.NewUserRepo(), userSearch: search.NewUserSearch(), groupRepo: repo.NewGroupRepo(), groupService: NewGroupService(), groupMapper: newGroupMapper(), userMapper: newUserMapper(), config: config.GetConfig(), } } func (svc *OrganizationService) Create(opts OrganizationCreateOptions, userID string) (*Organization, error) { org, err := svc.orgRepo.Insert(repo.OrganizationInsertOptions{ ID: helper.NewID(), Name: opts.Name, }) if err != nil { return nil, err } if err := svc.orgRepo.GrantUserPermission(org.GetID(), userID, model.PermissionOwner); err != nil { return nil, err } org, err = svc.orgRepo.Find(org.GetID()) if err != nil { return nil, err } if err := svc.orgSearch.Index([]model.Organization{org}); err != nil { return nil, err } if err := svc.orgCache.Set(org); err != nil { return nil, nil } res, err := svc.orgMapper.mapOne(org, userID) if err != nil { return nil, err } return res, nil } func (svc *OrganizationService) Find(id string, userID string) (*Organization, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } org, err := svc.orgCache.Get(id) if err != nil { return nil, err } if err := svc.orgGuard.Authorize(user, org, model.PermissionViewer); err != nil { return nil, err } res, err := svc.orgMapper.mapOne(org, userID) if err != nil { return nil, err } return res, nil } func (svc *OrganizationService) List(opts OrganizationListOptions, userID string) (*OrganizationList, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } var authorized []model.Organization if opts.Query == "" { ids, err := svc.orgRepo.GetIDs() if err != nil { return nil, err } authorized, err = svc.doAuthorizationByIDs(ids, user) if err != nil { return nil, err } } else { orgs, err := svc.orgSearch.Query(opts.Query) if err != nil { return nil, err } authorized, err = svc.doAuthorization(orgs, user) if err != nil { return nil, err } } if opts.SortBy == "" { opts.SortBy = SortByDateCreated } if opts.SortOrder == "" { opts.SortOrder = SortOrderAsc } sorted := svc.doSorting(authorized, opts.SortBy, opts.SortOrder) paged, totalElements, totalPages := svc.doPagination(sorted, opts.Page, opts.Size) mapped, err := svc.orgMapper.mapMany(paged, userID) if err != nil { return nil, err } return &OrganizationList{ Data: mapped, TotalPages: totalPages, TotalElements: totalElements, Page: opts.Page, Size: uint(len(mapped)), }, nil } func (svc *OrganizationService) UpdateName(id string, name string, userID string) (*Organization, error) { user, err := svc.userRepo.Find(userID) if err != nil { return nil, err } org, err := svc.orgCache.Get(id) if err != nil { return nil, err } if err := svc.orgGuard.Authorize(user, org, model.PermissionEditor); err != nil { return nil, err } org.SetName(name) if err := svc.orgRepo.Save(org); err != nil { return nil, err } if err := svc.orgSearch.Update([]model.Organization{org}); err != nil { return nil, err } err = svc.orgCache.Set(org) if err != nil { return nil, err } res, err := svc.orgMapper.mapOne(org, userID) if err != nil { return nil, err } return res, nil } func (svc *OrganizationService) Delete(id string, userID string) error { user, err := svc.userRepo.Find(userID) if err != nil { return err } org, err := svc.orgCache.Get(id) if err != nil { return err } if err := svc.orgGuard.Authorize(user, org, model.PermissionOwner); err != nil { return err } if err := svc.orgRepo.Delete(id); err != nil { return err } if err := svc.orgCache.Delete(org.GetID()); err != nil { return err } if err := svc.orgSearch.Delete([]string{org.GetID()}); err != nil { return err } return nil } func (svc *OrganizationService) RemoveMember(id string, memberID string, userID string) error { user, err := svc.userRepo.Find(userID) if err != nil { return err } member, err := svc.userRepo.Find(memberID) if err != nil { return err } org, err := svc.orgCache.Get(id) if err != nil { return err } /* Make sure member is not the last remaining owner of the organization */ ownerCount, err := svc.orgRepo.GetOwnerCount(org.GetID()) if err != nil { return err } if svc.orgGuard.IsAuthorized(member, org, model.PermissionOwner) && ownerCount == 1 { return errorpkg.NewCannotRemoveLastRemainingOwnerOfOrganizationError(org.GetID()) } if userID != member.GetID() { if err := svc.orgGuard.Authorize(user, org, model.PermissionOwner); err != nil { return err } } /* Remove member from all groups belonging to this organization */ groupsIDs, err := svc.groupRepo.GetIDsForOrganization(org.GetID()) if err != nil { return err } for _, groupID := range groupsIDs { if err := svc.groupService.RemoveMemberUnauthorized(groupID, member.GetID()); err != nil { log.Error(err) } } if err := svc.orgRepo.RevokeUserPermission(id, member.GetID()); err != nil { return err } if err := svc.orgRepo.RemoveMember(id, member.GetID()); err != nil { return err } if _, err := svc.orgCache.Refresh(org.GetID()); err != nil { return err } return nil } func (svc *OrganizationService) doAuthorization(data []model.Organization, user model.User) ([]model.Organization, error) { var res []model.Organization for _, o := range data { if svc.orgGuard.IsAuthorized(user, o, model.PermissionViewer) { res = append(res, o) } } return res, nil } func (svc *OrganizationService) doAuthorizationByIDs(ids []string, user model.User) ([]model.Organization, error) { var res []model.Organization for _, id := range ids { var o model.Organization o, err := svc.orgCache.Get(id) if err != nil { return nil, err } if svc.orgGuard.IsAuthorized(user, o, model.PermissionViewer) { res = append(res, o) } } return res, nil } func (svc *OrganizationService) doSorting(data []model.Organization, sortBy string, sortOrder string) []model.Organization { if sortBy == SortByName { sort.Slice(data, func(i, j int) bool { if sortOrder == SortOrderDesc { return data[i].GetName() > data[j].GetName() } else { return data[i].GetName() < data[j].GetName() } }) return data } else if sortBy == SortByDateCreated { sort.Slice(data, func(i, j int) bool { a, _ := time.Parse(time.RFC3339, data[i].GetCreateTime()) b, _ := time.Parse(time.RFC3339, data[j].GetCreateTime()) if sortOrder == SortOrderDesc { return a.UnixMilli() > b.UnixMilli() } else { return a.UnixMilli() < b.UnixMilli() } }) return data } else if sortBy == SortByDateModified { sort.Slice(data, func(i, j int) bool { if data[i].GetUpdateTime() != nil && data[j].GetUpdateTime() != nil { a, _ := time.Parse(time.RFC3339, *data[i].GetUpdateTime()) b, _ := time.Parse(time.RFC3339, *data[j].GetUpdateTime()) if sortOrder == SortOrderDesc { return a.UnixMilli() > b.UnixMilli() } else { return a.UnixMilli() < b.UnixMilli() } } else { return false } }) return data } return data } func (svc *OrganizationService) doPagination(data []model.Organization, page, size uint) ([]model.Organization, uint, uint) { totalElements := uint(len(data)) totalPages := (totalElements + size - 1) / size if page > totalPages { return nil, totalElements, totalPages } startIndex := (page - 1) * size endIndex := startIndex + size if endIndex > totalElements { endIndex = totalElements } pageData := data[startIndex:endIndex] return pageData, totalElements, totalPages } type organizationMapper struct { groupCache *cache.GroupCache } func newOrganizationMapper() *organizationMapper { return &organizationMapper{ groupCache: cache.NewGroupCache(), } } func (mp *organizationMapper) mapOne(m model.Organization, userID string) (*Organization, error) { res := &Organization{ ID: m.GetID(), Name: m.GetName(), CreateTime: m.GetCreateTime(), UpdateTime: m.GetUpdateTime(), } res.Permission = "" for _, p := range m.GetUserPermissions() { if p.GetUserID() == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) { res.Permission = p.GetValue() } } for _, p := range m.GetGroupPermissions() { g, err := mp.groupCache.Get(p.GetGroupID()) if err != nil { return nil, err } for _, u := range g.GetUsers() { if u == userID && model.GetPermissionWeight(p.GetValue()) > model.GetPermissionWeight(res.Permission) { res.Permission = p.GetValue() } } } return res, nil } func (mp *organizationMapper) mapMany(orgs []model.Organization, userID string) ([]*Organization, error) { res := make([]*Organization, 0) for _, f := range orgs { v, err := mp.mapOne(f, userID) if err != nil { return nil, err } res = append(res, v) } return res, nil }