eintopf.info@v0.13.16/service/invitation/invitation.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 invitation
    17  
    18  import (
    19  	"context"
    20  	"crypto/sha256"
    21  	"encoding/hex"
    22  	"fmt"
    23  	"strconv"
    24  	"time"
    25  
    26  	"eintopf.info/internal/xerror"
    27  	"eintopf.info/service/auth"
    28  	"eintopf.info/service/user"
    29  )
    30  
    31  // Service defines an invitation services, that can handle crud operations on
    32  // the Invitation model
    33  // -go:generate go run github.com/petergtz/pegomock/pegomock generate eintopf.info/service/invitation Service --output=../../internal/mock/invitation_service.go --package=mock --mock-name=InvitationService
    34  type Service interface {
    35  	Storer
    36  	Invite(ctx context.Context) (token string, err error)
    37  	UseInvite(ctx context.Context, token string, user *user.NewUser) error
    38  }
    39  
    40  // Invitation is an invitation model.
    41  type Invitation struct {
    42  	ID        string    `json:"id" db:"id"`
    43  	Token     string    `json:"token" db:"token"`
    44  	CreatedAt time.Time `json:"createdAt" db:"created_at"`
    45  	CreatedBy string    `json:"createdBy" db:"created_by"`
    46  	UsedBy    string    `json:"usedBy" db:"used_by"`
    47  }
    48  
    49  // NewInvitation is the model used to create new invitations by the storer.
    50  type NewInvitation struct {
    51  	Token     string    `json:"token"`
    52  	CreatedAt time.Time `json:"createdAt"`
    53  	CreatedBy string    `json:"createdBy"`
    54  }
    55  
    56  // InvitationFromNewInvitation converts a NewInvitation to an Invitation with a
    57  // given id.
    58  func InvitationFromNewInvitation(newInvitation *NewInvitation, id string) *Invitation {
    59  	return &Invitation{
    60  		ID:        id,
    61  		Token:     newInvitation.Token,
    62  		CreatedAt: newInvitation.CreatedAt,
    63  		CreatedBy: newInvitation.CreatedBy,
    64  	}
    65  }
    66  
    67  // Storer defines an interface for crud operations on the invitation model.
    68  type Storer interface {
    69  	Create(ctx context.Context, invitation *NewInvitation) (*Invitation, error)
    70  	Update(ctx context.Context, invitation *Invitation) (*Invitation, error)
    71  	Delete(ctx context.Context, id string) error
    72  	FindByID(ctx context.Context, id string) (*Invitation, error)
    73  	Find(ctx context.Context, params *FindParams) ([]Invitation, int, error)
    74  }
    75  
    76  // SortOrder defines the order of sorting.
    77  type SortOrder string
    78  
    79  // Possible values for SortOrder.
    80  const (
    81  	OrderAsc  = SortOrder("ASC")
    82  	OrderDesc = SortOrder("DESC")
    83  )
    84  
    85  // FindParams defines parameters used by the Find method.
    86  type FindParams struct {
    87  	Offset int64
    88  	Limit  int64
    89  
    90  	Sort  string
    91  	Order SortOrder
    92  
    93  	Filters *FindFilters
    94  }
    95  
    96  // FindFilters defines the possible filters for the find method.
    97  type FindFilters struct {
    98  	ID        *string
    99  	Token     *string
   100  	CreatedAt *time.Time
   101  	CreatedBy *string
   102  	UsedBy    *string
   103  }
   104  
   105  type service struct {
   106  	store       Storer
   107  	userService user.Service
   108  }
   109  
   110  func NewService(store Storer, userService user.Service) Service {
   111  	return &service{store: store, userService: userService}
   112  }
   113  
   114  func (s *service) Create(ctx context.Context, invitation *NewInvitation) (*Invitation, error) {
   115  	return s.store.Create(ctx, invitation)
   116  }
   117  func (s *service) Update(ctx context.Context, invitation *Invitation) (*Invitation, error) {
   118  
   119  	return s.store.Update(ctx, invitation)
   120  }
   121  func (s *service) Delete(ctx context.Context, id string) error {
   122  	invitation, err := s.store.FindByID(ctx, id)
   123  	if err != nil {
   124  		return err
   125  	}
   126  	if invitation.UsedBy != "" {
   127  		return fmt.Errorf("can not delete used invitation")
   128  	}
   129  	return s.store.Delete(ctx, id)
   130  }
   131  func (s *service) FindByID(ctx context.Context, id string) (*Invitation, error) {
   132  
   133  	return s.store.FindByID(ctx, id)
   134  }
   135  func (s *service) Find(ctx context.Context, params *FindParams) ([]Invitation, int, error) {
   136  
   137  	return s.store.Find(ctx, params)
   138  }
   139  
   140  var ErrNoInvitationYet = fmt.Errorf("no invitation yet")
   141  var ErrInvitationLimit = fmt.Errorf("invitation limit")
   142  
   143  func (s *service) Invite(ctx context.Context) (token string, err error) {
   144  	role, err := auth.RoleFromContext(ctx)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  	userID, err := auth.UserIDFromContext(ctx)
   149  	if err != nil {
   150  		return "", fmt.Errorf("invalid inviter")
   151  	}
   152  
   153  	if role == auth.RoleNormal {
   154  		user, err := s.userService.FindByID(ctx, userID)
   155  		if err != nil {
   156  			return "", err
   157  		}
   158  		if time.Now().Sub(user.CreatedAt) < time.Hour*24*7 {
   159  			return "", ErrNoInvitationYet
   160  		}
   161  
   162  		invitations, _, err := s.store.Find(ctx, &FindParams{
   163  			Filters: &FindFilters{CreatedBy: &userID},
   164  		})
   165  		if err != nil {
   166  			return "", err
   167  		}
   168  
   169  		lastInvitationAt := time.Date(2000, 0, 0, 0, 0, 0, 0, time.UTC)
   170  		for _, invitation := range invitations {
   171  			if invitation.CreatedAt.After(lastInvitationAt) {
   172  				lastInvitationAt = invitation.CreatedAt
   173  			}
   174  		}
   175  		if time.Now().Sub(lastInvitationAt) < time.Hour*24*7 {
   176  			return "", ErrInvitationLimit
   177  		}
   178  	}
   179  
   180  	h := sha256.New()
   181  	h.Write([]byte(userID + strconv.FormatInt(time.Now().Unix(), 10)))
   182  	sum := h.Sum(nil)
   183  	dst := make([]byte, hex.EncodedLen(len(sum)))
   184  	hex.Encode(dst, sum)
   185  
   186  	invitation := &NewInvitation{
   187  		Token:     string(dst),
   188  		CreatedAt: time.Now(),
   189  		CreatedBy: userID,
   190  	}
   191  	_, err = s.store.Create(auth.ContextWithRole(ctx, auth.RoleInternal), invitation)
   192  	if err != nil {
   193  		return "", err
   194  	}
   195  
   196  	return invitation.Token, nil
   197  }
   198  
   199  var ErrInvalidInvitationToken = xerror.BadInputError{Err: fmt.Errorf("invalid invitation token")}
   200  var ErrInvitationTokenAlreadyUsed = xerror.BadInputError{Err: fmt.Errorf("invitation token already used")}
   201  
   202  func (s *service) UseInvite(ctx context.Context, token string, user *user.NewUser) error {
   203  	invitations, _, err := s.store.Find(auth.ContextWithRole(ctx, auth.RoleInternal), &FindParams{
   204  		Filters: &FindFilters{Token: &token},
   205  	})
   206  	if err != nil || len(invitations) != 1 {
   207  		return ErrInvalidInvitationToken
   208  	}
   209  	invitation := invitations[0]
   210  	if invitation.UsedBy != "" {
   211  		return ErrInvitationTokenAlreadyUsed
   212  	}
   213  	user.Role = auth.RoleNormal
   214  
   215  	newUser, err := s.userService.Create(auth.ContextWithRole(ctx, auth.RoleInternal), user)
   216  	if err != nil {
   217  		return err
   218  	}
   219  
   220  	invitation.UsedBy = newUser.ID
   221  
   222  	_, err = s.store.Update(auth.ContextWithRole(ctx, auth.RoleInternal), &invitation)
   223  	if err != nil {
   224  		return err
   225  	}
   226  	return nil
   227  }