package service import ( "fmt" "regexp" "spend-sparrow/db" "spend-sparrow/log" "spend-sparrow/types" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" ) var ( safeInputRegex = regexp.MustCompile(`^[a-zA-Z0-9äöüß -]+$`) accountMetric = promauto.NewCounterVec( prometheus.CounterOpts{ Name: "spendsparrow_account_total", Help: "The total of account operations", }, []string{"operation"}, ) ) type Account interface { Add(user *types.User, name string) (*types.Account, error) Update(user *types.User, id uuid.UUID, name string) (*types.Account, error) Get(user *types.User, id uuid.UUID) (*types.Account, error) GetAll(user *types.User) ([]*types.Account, error) Delete(user *types.User, id uuid.UUID) error } type AccountImpl struct { db db.Account clock Clock random Random settings *types.Settings } func NewAccountImpl(db db.Account, random Random, clock Clock, settings *types.Settings) Account { return AccountImpl{ db: db, clock: clock, random: NewRandomImpl(), settings: settings, } } func (s AccountImpl) Add(user *types.User, name string) (*types.Account, error) { accountMetric.WithLabelValues("add").Inc() if user == nil { return nil, ErrUnauthorized } newId, err := s.random.UUID() if err != nil { return nil, types.ErrInternal } err = s.validateAccount(name) if err != nil { return nil, err } account := &types.Account{ Id: newId, UserId: user.Id, Name: name, CurrentBalance: 0, LastTransaction: nil, OinkBalance: 0, CreatedAt: s.clock.Now(), CreatedBy: user.Id, UpdatedAt: nil, UpdatedBy: nil, } err = s.db.Insert(user.Id, account) if err != nil { return nil, types.ErrInternal } savedAccount, err := s.db.Get(user.Id, newId) if err != nil { log.Error("account %v not found after insert: %v", newId, err) return nil, types.ErrInternal } return savedAccount, nil } func (s AccountImpl) Update(user *types.User, id uuid.UUID, name string) (*types.Account, error) { accountMetric.WithLabelValues("update").Inc() if user == nil { return nil, ErrUnauthorized } err := s.validateAccount(name) if err != nil { return nil, err } account, err := s.db.Get(user.Id, id) if err != nil { if err == db.ErrNotFound { return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest) } return nil, types.ErrInternal } timestamp := s.clock.Now() account.Name = name account.UpdatedAt = ×tamp account.UpdatedBy = &user.Id err = s.db.Update(user.Id, account) if err != nil { return nil, types.ErrInternal } return account, nil } func (s AccountImpl) Get(user *types.User, id uuid.UUID) (*types.Account, error) { accountMetric.WithLabelValues("get").Inc() if user == nil { return nil, ErrUnauthorized } account, err := s.db.Get(user.Id, id) if err != nil { if err == db.ErrNotFound { return nil, fmt.Errorf("account %v not found: %w", id, ErrBadRequest) } return nil, types.ErrInternal } return account, nil } func (s AccountImpl) GetAll(user *types.User) ([]*types.Account, error) { accountMetric.WithLabelValues("get_all").Inc() if user == nil { return nil, ErrUnauthorized } accounts, err := s.db.GetAll(user.Id) if err != nil { return nil, types.ErrInternal } return accounts, nil } func (s AccountImpl) Delete(user *types.User, id uuid.UUID) error { accountMetric.WithLabelValues("delete").Inc() if user == nil { return ErrUnauthorized } account, err := s.db.Get(user.Id, id) if err != nil { if err == db.ErrNotFound { return fmt.Errorf("account %v not found: %w", id, ErrBadRequest) } return types.ErrInternal } if account.UserId != user.Id { return types.ErrUnauthorized } err = s.db.Delete(user.Id, account.Id) if err != nil { return types.ErrInternal } return nil } func (s AccountImpl) validateAccount(name string) error { if name == "" { return fmt.Errorf("field \"name\" needs to be set: %w", ErrBadRequest) } else if !safeInputRegex.MatchString(name) { return fmt.Errorf("use only letters, dashes and spaces for \"name\": %w", ErrBadRequest) } else { return nil } }