eintopf.info@v0.13.16/service/group/group.go (about)

     1  // Copyright (C) 2022 The Eintopf authors
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    15  
    16  package group
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"eintopf.info/internal/crud"
    24  	"eintopf.info/internal/xerror"
    25  	"eintopf.info/service/auth"
    26  )
    27  
    28  // Group defines a group entity
    29  type NewGroup struct {
    30  	Published   bool     `json:"published"`
    31  	Name        string   `json:"name"`
    32  	Link        string   `json:"link"`
    33  	Email       string   `json:"email"`
    34  	Description string   `json:"description"`
    35  	Image       string   `json:"image"`
    36  	OwnedBy     []string `json:"ownedBy" db:"owned_by"`
    37  }
    38  
    39  // IsOwned returns true if the id is in the OwnedBy field.
    40  func (g *NewGroup) IsOwned(id string) bool {
    41  	owned := false
    42  	for _, owner := range g.OwnedBy {
    43  		if owner == id {
    44  			owned = true
    45  		}
    46  	}
    47  	return owned
    48  }
    49  
    50  // Group defines a group entity
    51  // It implements indexo.Coppyable.
    52  type Group struct {
    53  	ID          string   `json:"id" db:"id"`
    54  	Deactivated bool     `json:"deactivated" db:"deactivated"`
    55  	Published   bool     `json:"published" db:"published"`
    56  	Name        string   `json:"name" db:"name"`
    57  	Link        string   `json:"link" db:"link"`
    58  	Email       string   `json:"email" db:"email"`
    59  	Description string   `json:"description" db:"description"`
    60  	Image       string   `json:"image" db:"image"`
    61  	OwnedBy     []string `json:"ownedBy" db:"owned_by"`
    62  }
    63  
    64  func (g Group) Identifier() string { return g.ID }
    65  
    66  func GroupFromNewGroup(newGroup *NewGroup, id string) *Group {
    67  	return &Group{
    68  		ID:          id,
    69  		Deactivated: false,
    70  		Name:        newGroup.Name,
    71  		Link:        newGroup.Link,
    72  		Email:       newGroup.Email,
    73  		Description: newGroup.Description,
    74  		OwnedBy:     newGroup.OwnedBy,
    75  		Image:       newGroup.Image,
    76  		Published:   newGroup.Published,
    77  	}
    78  }
    79  
    80  // Indexable indicates wether the group should be added to the search index.
    81  func (g *Group) Indexable() bool {
    82  	return !g.Deactivated
    83  }
    84  
    85  // Listed indicates wether the group should be shown on the list page.
    86  func (g *Group) Listable() bool {
    87  	return g.Published
    88  }
    89  
    90  // IsOwned returns true if the id is in the OwnedBy field.
    91  func (g *Group) IsOwned(id string) bool {
    92  	owned := false
    93  	for _, owner := range g.OwnedBy {
    94  		if owner == id {
    95  			owned = true
    96  		}
    97  	}
    98  	return owned
    99  }
   100  
   101  // Service defines the crud service to manage groups.
   102  // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/group Service --output=../../internal/mock/group_service.go --package=mock --mock-name=GroupService
   103  type Service interface {
   104  	Storer
   105  }
   106  
   107  // SortOrder defines the order of sorting.
   108  type SortOrder string
   109  
   110  // Possible values for SortOrder.
   111  const (
   112  	OrderAsc  = SortOrder("ASC")
   113  	OrderDesc = SortOrder("DESC")
   114  )
   115  
   116  // FindFilters defines the possible filters for the find method.
   117  type FindFilters struct {
   118  	ID          *string  `json:"id"`
   119  	NotID       *string  `json:"not_id"`
   120  	Deactivated *bool    `json:"deactivated"`
   121  	Published   *bool    `json:"published"`
   122  	Name        *string  `json:"name"`
   123  	LikeName    *string  `json:"likeName"`
   124  	Link        *string  `json:"link"`
   125  	Email       *string  `json:"email"`
   126  	Description *string  `json:"description"`
   127  	OwnedBy     []string `json:"ownedBy"`
   128  }
   129  
   130  // Storer defines a service for CRUD operations on the event model.
   131  // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/group Storer --output=../../internal/mock/group_store.go --package=mock --mock-name=GroupStore
   132  type Storer interface {
   133  	Create(ctx context.Context, newGroup *NewGroup) (*Group, error)
   134  	Update(ctx context.Context, group *Group) (*Group, error)
   135  	Delete(ctx context.Context, id string) error
   136  	FindByID(ctx context.Context, id string) (*Group, error)
   137  	Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Group, int, error)
   138  }
   139  
   140  // NewService returns a new group service.
   141  func NewService(store Storer) *service {
   142  	return &service{store: store}
   143  }
   144  
   145  type service struct {
   146  	store Storer
   147  }
   148  
   149  var ErrNameAlreadyExists = xerror.BadInputError{Err: fmt.Errorf("name already exists")}
   150  
   151  // Create makes sure the loggedin user is in the owned field.
   152  func (s *service) Create(ctx context.Context, newGroup *NewGroup) (*Group, error) {
   153  	if s.groupNameExists(ctx, newGroup.Name, "") {
   154  		return nil, ErrNameAlreadyExists
   155  	}
   156  	id, err := auth.UserIDFromContext(ctx)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	if !newGroup.IsOwned(id) {
   161  		newGroup.OwnedBy = append(newGroup.OwnedBy, id)
   162  	}
   163  	newGroup.Name = strings.TrimSpace(newGroup.Name)
   164  	return s.store.Create(ctx, newGroup)
   165  }
   166  
   167  func (s *service) Update(ctx context.Context, group *Group) (*Group, error) {
   168  	if s.groupNameExists(ctx, group.Name, group.ID) {
   169  		return nil, ErrNameAlreadyExists
   170  	}
   171  	return s.store.Update(ctx, group)
   172  }
   173  
   174  func (s *service) Delete(ctx context.Context, id string) error {
   175  	return s.store.Delete(ctx, id)
   176  }
   177  
   178  func (s *service) FindByID(ctx context.Context, id string) (*Group, error) {
   179  	return s.store.FindByID(ctx, id)
   180  }
   181  
   182  // Find adds a special filter value "self" for the OwnedBy field. If it is set
   183  // the value gets replaced with the id of the loggedin user.
   184  func (s *service) Find(ctx context.Context, params *crud.FindParams[FindFilters]) ([]*Group, int, error) {
   185  	if params != nil && params.Filters != nil && params.Filters.OwnedBy != nil {
   186  		if len(params.Filters.OwnedBy) == 1 && params.Filters.OwnedBy[0] == "self" {
   187  			id, err := auth.UserIDFromContext(ctx)
   188  			if err == nil {
   189  				params.Filters.OwnedBy = []string{id}
   190  			}
   191  		}
   192  	}
   193  	return s.store.Find(ctx, params)
   194  }
   195  
   196  func (s *service) groupNameExists(ctx context.Context, name string, id string) bool {
   197  	existingGroups, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{
   198  		Filters: &FindFilters{Name: &name, NotID: &id},
   199  	})
   200  	if err != nil || len(existingGroups) > 0 {
   201  		return true
   202  	}
   203  	return false
   204  }
   205  
   206  func (s *service) GroupIDsByOwners(ctx context.Context, ownedBy []string) ([]string, error) {
   207  
   208  	groups, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &crud.FindParams[FindFilters]{
   209  		Filters: &FindFilters{OwnedBy: ownedBy},
   210  	})
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	ids := []string{}
   215  	for _, g := range groups {
   216  		ids = append(ids, g.ID)
   217  	}
   218  	return ids, nil
   219  }