2019-01-24 16:26:36 +00:00
|
|
|
// Copyright (C) 2019 Storj Labs, Inc.
|
2018-11-15 12:00:08 +00:00
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
2019-01-15 13:03:24 +00:00
|
|
|
package console
|
2018-11-14 10:50:15 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/subtle"
|
2020-03-10 23:01:19 +00:00
|
|
|
"database/sql"
|
2020-07-16 16:50:15 +01:00
|
|
|
"errors"
|
2020-01-03 14:21:05 +00:00
|
|
|
"fmt"
|
2020-11-05 16:16:55 +00:00
|
|
|
"net/mail"
|
2019-11-12 11:14:34 +00:00
|
|
|
"sort"
|
2018-11-14 10:50:15 +00:00
|
|
|
"time"
|
|
|
|
|
2020-10-09 14:40:12 +01:00
|
|
|
"github.com/spacemonkeygo/monkit/v3"
|
testplanet/satellite: reduce the number of places default values need to be configured
Satellites set their configuration values to default values using
cfgstruct, however, it turns out our tests don't test these values
at all! Instead, they have a completely separate definition system
that is easy to forget about.
As is to be expected, these values have drifted, and it appears
in a few cases test planet is testing unreasonable values that we
won't see in production, or perhaps worse, features enabled in
production were missed and weren't enabled in testplanet.
This change makes it so all values are configured the same,
systematic way, so it's easy to see when test values are different
than dev values or release values, and it's less hard to forget
to enable features in testplanet.
In terms of reviewing, this change should be actually fairly
easy to review, considering private/testplanet/satellite.go keeps
the current config system and the new one and confirms that they
result in identical configurations, so you can be certain that
nothing was missed and the config is all correct.
You can also check the config lock to see what actual config
values changed.
Change-Id: I6715d0794887f577e21742afcf56fd2b9d12170e
2021-05-31 22:15:00 +01:00
|
|
|
"github.com/spf13/pflag"
|
2021-06-22 01:09:56 +01:00
|
|
|
"github.com/stripe/stripe-go/v72"
|
2018-11-14 10:50:15 +00:00
|
|
|
"github.com/zeebo/errs"
|
2018-11-30 13:40:13 +00:00
|
|
|
"go.uber.org/zap"
|
2019-01-08 14:05:14 +00:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
2018-12-20 20:10:27 +00:00
|
|
|
|
2019-12-27 11:48:47 +00:00
|
|
|
"storj.io/common/macaroon"
|
2021-07-01 00:13:45 +01:00
|
|
|
"storj.io/common/memory"
|
2020-11-13 11:41:35 +00:00
|
|
|
"storj.io/common/storj"
|
2020-03-30 10:08:50 +01:00
|
|
|
"storj.io/common/uuid"
|
testplanet/satellite: reduce the number of places default values need to be configured
Satellites set their configuration values to default values using
cfgstruct, however, it turns out our tests don't test these values
at all! Instead, they have a completely separate definition system
that is easy to forget about.
As is to be expected, these values have drifted, and it appears
in a few cases test planet is testing unreasonable values that we
won't see in production, or perhaps worse, features enabled in
production were missed and weren't enabled in testplanet.
This change makes it so all values are configured the same,
systematic way, so it's easy to see when test values are different
than dev values or release values, and it's less hard to forget
to enable features in testplanet.
In terms of reviewing, this change should be actually fairly
easy to review, considering private/testplanet/satellite.go keeps
the current config system and the new one and confirms that they
result in identical configurations, so you can be certain that
nothing was missed and the config is all correct.
You can also check the config lock to see what actual config
values changed.
Change-Id: I6715d0794887f577e21742afcf56fd2b9d12170e
2021-05-31 22:15:00 +01:00
|
|
|
"storj.io/private/cfgstruct"
|
2019-11-15 14:27:44 +00:00
|
|
|
"storj.io/storj/satellite/accounting"
|
2021-04-08 18:34:23 +01:00
|
|
|
"storj.io/storj/satellite/analytics"
|
2019-01-15 13:03:24 +00:00
|
|
|
"storj.io/storj/satellite/console/consoleauth"
|
2019-10-17 15:42:18 +01:00
|
|
|
"storj.io/storj/satellite/payments"
|
2019-07-02 15:36:54 +01:00
|
|
|
"storj.io/storj/satellite/rewards"
|
2018-11-14 10:50:15 +00:00
|
|
|
)
|
|
|
|
|
2019-06-19 21:49:04 +01:00
|
|
|
var mon = monkit.Package()
|
2018-12-20 20:10:27 +00:00
|
|
|
|
2018-12-26 14:00:53 +00:00
|
|
|
const (
|
2019-11-15 14:27:44 +00:00
|
|
|
// maxLimit specifies the limit for all paged queries.
|
2019-01-30 15:04:40 +00:00
|
|
|
maxLimit = 50
|
|
|
|
tokenExpirationTime = 24 * time.Hour
|
2019-02-05 17:31:53 +00:00
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
// TestPasswordCost is the hashing complexity to use for testing.
|
2019-02-05 17:31:53 +00:00
|
|
|
TestPasswordCost = bcrypt.MinCost
|
2018-12-26 14:00:53 +00:00
|
|
|
)
|
2018-12-17 14:28:58 +00:00
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// Error messages.
|
2019-04-10 01:15:12 +01:00
|
|
|
const (
|
2019-04-10 20:16:10 +01:00
|
|
|
unauthorizedErrMsg = "You are not authorized to perform this action"
|
|
|
|
emailUsedErrMsg = "This email is already in use, try another"
|
|
|
|
passwordRecoveryTokenIsExpiredErrMsg = "Your password recovery link has expired, please request another one"
|
|
|
|
credentialsErrMsg = "Your email or password was incorrect, please try again"
|
|
|
|
passwordIncorrectErrMsg = "Your password needs at least %d characters long"
|
2019-09-04 16:02:39 +01:00
|
|
|
projectOwnerDeletionForbiddenErrMsg = "%s is a project owner and can not be deleted"
|
2019-10-10 14:28:35 +01:00
|
|
|
apiKeyWithNameExistsErrMsg = "An API Key with this name already exists in this project, please use a different name"
|
2021-03-23 20:23:27 +00:00
|
|
|
apiKeyWithNameDoesntExistErrMsg = "An API Key with this name doesn't exist in this project."
|
2019-05-24 17:51:27 +01:00
|
|
|
teamMemberDoesNotExistErrMsg = `There is no account on this Satellite for the user(s) you have entered.
|
2019-04-10 01:15:12 +01:00
|
|
|
Please add team members with active accounts`
|
|
|
|
|
2020-03-10 23:01:19 +00:00
|
|
|
usedRegTokenErrMsg = "This registration token has already been used"
|
|
|
|
projLimitErrMsg = "Sorry, project creation is limited for your account. Please contact support!"
|
2019-04-10 01:15:12 +01:00
|
|
|
)
|
|
|
|
|
2020-02-10 12:03:38 +00:00
|
|
|
var (
|
|
|
|
// Error describes internal console error.
|
2021-04-28 09:06:17 +01:00
|
|
|
Error = errs.Class("console service")
|
2019-09-04 16:02:39 +01:00
|
|
|
|
2020-02-10 12:03:38 +00:00
|
|
|
// ErrNoMembership is error type of not belonging to a specific project.
|
2021-04-28 09:06:17 +01:00
|
|
|
ErrNoMembership = errs.Class("no membership")
|
2019-10-17 15:42:18 +01:00
|
|
|
|
2020-02-10 12:03:38 +00:00
|
|
|
// ErrTokenExpiration is error type of token reached expiration time.
|
2021-04-28 09:06:17 +01:00
|
|
|
ErrTokenExpiration = errs.Class("token expiration")
|
2019-11-12 13:14:31 +00:00
|
|
|
|
2020-02-10 12:03:38 +00:00
|
|
|
// ErrProjLimit is error type of project limit.
|
2021-04-28 09:06:17 +01:00
|
|
|
ErrProjLimit = errs.Class("project limit")
|
2020-02-10 12:03:38 +00:00
|
|
|
|
2020-10-09 14:40:12 +01:00
|
|
|
// ErrUsage is error type of project usage.
|
2021-04-28 09:06:17 +01:00
|
|
|
ErrUsage = errs.Class("project usage")
|
2020-10-09 14:40:12 +01:00
|
|
|
|
2020-02-10 12:03:38 +00:00
|
|
|
// ErrEmailUsed is error type that occurs on repeating auth attempts with email.
|
|
|
|
ErrEmailUsed = errs.Class("email used")
|
2021-03-23 20:23:27 +00:00
|
|
|
|
|
|
|
// ErrNoAPIKey is error type that occurs when there is no api key found.
|
|
|
|
ErrNoAPIKey = errs.Class("no api key found")
|
2021-07-15 22:06:23 +01:00
|
|
|
|
|
|
|
// ErrRegToken describes registration token errors.
|
|
|
|
ErrRegToken = errs.Class("registration token")
|
|
|
|
|
|
|
|
// ErrRecaptcha describes reCAPTCHA validation errors.
|
|
|
|
ErrRecaptcha = errs.Class("recaptcha validation")
|
2020-02-10 12:03:38 +00:00
|
|
|
)
|
2019-12-09 13:20:44 +00:00
|
|
|
|
2020-12-05 16:01:42 +00:00
|
|
|
// Service is handling accounts related logic.
|
2019-09-10 14:24:16 +01:00
|
|
|
//
|
|
|
|
// architecture: Service
|
2018-11-14 10:50:15 +00:00
|
|
|
type Service struct {
|
|
|
|
Signer
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
log, auditLogger *zap.Logger
|
2019-11-15 14:27:44 +00:00
|
|
|
store DB
|
|
|
|
projectAccounting accounting.ProjectAccounting
|
2019-12-12 12:58:15 +00:00
|
|
|
projectUsage *accounting.Service
|
2020-10-09 14:40:12 +01:00
|
|
|
buckets Buckets
|
2019-11-15 14:27:44 +00:00
|
|
|
partners *rewards.PartnersService
|
|
|
|
accounts payments.Accounts
|
2021-06-25 12:17:55 +01:00
|
|
|
recaptchaHandler RecaptchaHandler
|
2021-04-08 18:34:23 +01:00
|
|
|
analytics *analytics.Service
|
2019-02-05 17:31:53 +00:00
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
config Config
|
2020-03-16 19:34:15 +00:00
|
|
|
|
|
|
|
minCoinPayment int64
|
2020-03-11 15:36:55 +00:00
|
|
|
}
|
|
|
|
|
testplanet/satellite: reduce the number of places default values need to be configured
Satellites set their configuration values to default values using
cfgstruct, however, it turns out our tests don't test these values
at all! Instead, they have a completely separate definition system
that is easy to forget about.
As is to be expected, these values have drifted, and it appears
in a few cases test planet is testing unreasonable values that we
won't see in production, or perhaps worse, features enabled in
production were missed and weren't enabled in testplanet.
This change makes it so all values are configured the same,
systematic way, so it's easy to see when test values are different
than dev values or release values, and it's less hard to forget
to enable features in testplanet.
In terms of reviewing, this change should be actually fairly
easy to review, considering private/testplanet/satellite.go keeps
the current config system and the new one and confirms that they
result in identical configurations, so you can be certain that
nothing was missed and the config is all correct.
You can also check the config lock to see what actual config
values changed.
Change-Id: I6715d0794887f577e21742afcf56fd2b9d12170e
2021-05-31 22:15:00 +01:00
|
|
|
func init() {
|
|
|
|
var c Config
|
|
|
|
cfgstruct.Bind(pflag.NewFlagSet("", pflag.PanicOnError), &c, cfgstruct.UseTestDefaults())
|
|
|
|
if c.PasswordCost != TestPasswordCost {
|
|
|
|
panic("invalid test constant defined in struct tag")
|
|
|
|
}
|
|
|
|
cfgstruct.Bind(pflag.NewFlagSet("", pflag.PanicOnError), &c, cfgstruct.UseReleaseDefaults())
|
|
|
|
if c.PasswordCost != 0 {
|
|
|
|
panic("invalid release constant defined in struct tag. should be 0 (=automatic)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// Config keeps track of core console service configuration parameters.
|
2020-03-11 15:36:55 +00:00
|
|
|
type Config struct {
|
testplanet/satellite: reduce the number of places default values need to be configured
Satellites set their configuration values to default values using
cfgstruct, however, it turns out our tests don't test these values
at all! Instead, they have a completely separate definition system
that is easy to forget about.
As is to be expected, these values have drifted, and it appears
in a few cases test planet is testing unreasonable values that we
won't see in production, or perhaps worse, features enabled in
production were missed and weren't enabled in testplanet.
This change makes it so all values are configured the same,
systematic way, so it's easy to see when test values are different
than dev values or release values, and it's less hard to forget
to enable features in testplanet.
In terms of reviewing, this change should be actually fairly
easy to review, considering private/testplanet/satellite.go keeps
the current config system and the new one and confirms that they
result in identical configurations, so you can be certain that
nothing was missed and the config is all correct.
You can also check the config lock to see what actual config
values changed.
Change-Id: I6715d0794887f577e21742afcf56fd2b9d12170e
2021-05-31 22:15:00 +01:00
|
|
|
PasswordCost int `help:"password hashing cost (0=automatic)" testDefault:"4" default:"0"`
|
2021-06-22 12:06:40 +01:00
|
|
|
OpenRegistrationEnabled bool `help:"enable open registration" default:"false" testDefault:"true"`
|
testplanet/satellite: reduce the number of places default values need to be configured
Satellites set their configuration values to default values using
cfgstruct, however, it turns out our tests don't test these values
at all! Instead, they have a completely separate definition system
that is easy to forget about.
As is to be expected, these values have drifted, and it appears
in a few cases test planet is testing unreasonable values that we
won't see in production, or perhaps worse, features enabled in
production were missed and weren't enabled in testplanet.
This change makes it so all values are configured the same,
systematic way, so it's easy to see when test values are different
than dev values or release values, and it's less hard to forget
to enable features in testplanet.
In terms of reviewing, this change should be actually fairly
easy to review, considering private/testplanet/satellite.go keeps
the current config system and the new one and confirms that they
result in identical configurations, so you can be certain that
nothing was missed and the config is all correct.
You can also check the config lock to see what actual config
values changed.
Change-Id: I6715d0794887f577e21742afcf56fd2b9d12170e
2021-05-31 22:15:00 +01:00
|
|
|
DefaultProjectLimit int `help:"default project limits for users" default:"3" testDefault:"5"`
|
2021-03-22 20:26:59 +00:00
|
|
|
UsageLimits UsageLimitsConfig
|
2021-06-25 12:17:55 +01:00
|
|
|
Recaptcha RecaptchaConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
// RecaptchaConfig contains configurations for the reCAPTCHA system.
|
|
|
|
type RecaptchaConfig struct {
|
|
|
|
Enabled bool `help:"whether or not reCAPTCHA is enabled for user registration" default:"false"`
|
|
|
|
SiteKey string `help:"reCAPTCHA site key"`
|
|
|
|
SecretKey string `help:"reCAPTCHA secret key"`
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// PaymentsService separates all payment related functionality.
|
2019-10-17 15:42:18 +01:00
|
|
|
type PaymentsService struct {
|
|
|
|
service *Service
|
|
|
|
}
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
// NewService returns new instance of Service.
|
2021-04-08 18:34:23 +01:00
|
|
|
func NewService(log *zap.Logger, signer Signer, store DB, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, analytics *analytics.Service, config Config, minCoinPayment int64) (*Service, error) {
|
2018-11-14 10:50:15 +00:00
|
|
|
if signer == nil {
|
|
|
|
return nil, errs.New("signer can't be nil")
|
|
|
|
}
|
|
|
|
if store == nil {
|
|
|
|
return nil, errs.New("store can't be nil")
|
|
|
|
}
|
2018-11-21 15:51:43 +00:00
|
|
|
if log == nil {
|
|
|
|
return nil, errs.New("log can't be nil")
|
|
|
|
}
|
2020-03-11 15:36:55 +00:00
|
|
|
if config.PasswordCost == 0 {
|
|
|
|
config.PasswordCost = bcrypt.DefaultCost
|
2019-02-05 17:31:53 +00:00
|
|
|
}
|
|
|
|
|
2019-03-02 15:22:20 +00:00
|
|
|
return &Service{
|
2019-11-15 14:27:44 +00:00
|
|
|
log: log,
|
2020-09-06 02:56:07 +01:00
|
|
|
auditLogger: log.Named("auditlog"),
|
2019-11-15 14:27:44 +00:00
|
|
|
Signer: signer,
|
|
|
|
store: store,
|
|
|
|
projectAccounting: projectAccounting,
|
2019-12-12 12:58:15 +00:00
|
|
|
projectUsage: projectUsage,
|
2020-10-09 14:40:12 +01:00
|
|
|
buckets: buckets,
|
2019-11-15 14:27:44 +00:00
|
|
|
partners: partners,
|
|
|
|
accounts: accounts,
|
2021-06-25 12:17:55 +01:00
|
|
|
recaptchaHandler: NewDefaultRecaptcha(config.Recaptcha.SecretKey),
|
2021-04-08 18:34:23 +01:00
|
|
|
analytics: analytics,
|
2020-03-11 15:36:55 +00:00
|
|
|
config: config,
|
2020-03-16 19:34:15 +00:00
|
|
|
minCoinPayment: minCoinPayment,
|
2019-03-02 15:22:20 +00:00
|
|
|
}, nil
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
func getRequestingIP(ctx context.Context) (source, forwardedFor string) {
|
|
|
|
if req := GetRequest(ctx); req != nil {
|
|
|
|
return req.RemoteAddr, req.Header.Get("X-Forwarded-For")
|
|
|
|
}
|
|
|
|
return "", ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) auditLog(ctx context.Context, operation string, userID *uuid.UUID, email string, extra ...zap.Field) {
|
|
|
|
sourceIP, forwardedForIP := getRequestingIP(ctx)
|
|
|
|
fields := append(
|
|
|
|
make([]zap.Field, 0, len(extra)+5),
|
|
|
|
zap.String("operation", operation),
|
|
|
|
zap.String("source-ip", sourceIP),
|
|
|
|
zap.String("forwarded-for-ip", forwardedForIP),
|
|
|
|
)
|
|
|
|
if userID != nil {
|
|
|
|
fields = append(fields, zap.String("userID", userID.String()))
|
|
|
|
}
|
|
|
|
if email != "" {
|
|
|
|
fields = append(fields, zap.String("email", email))
|
|
|
|
}
|
|
|
|
fields = append(fields, fields...)
|
|
|
|
s.auditLogger.Info("console activity", fields...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) getAuthAndAuditLog(ctx context.Context, operation string, extra ...zap.Field) (Authorization, error) {
|
|
|
|
auth, err := GetAuth(ctx)
|
|
|
|
if err != nil {
|
|
|
|
sourceIP, forwardedForIP := getRequestingIP(ctx)
|
|
|
|
s.auditLogger.Info("console activity unauthorized",
|
|
|
|
append(append(
|
|
|
|
make([]zap.Field, 0, len(extra)+4),
|
|
|
|
zap.String("operation", operation),
|
|
|
|
zap.Error(err),
|
|
|
|
zap.String("source-ip", sourceIP),
|
|
|
|
zap.String("forwarded-for-ip", forwardedForIP),
|
|
|
|
), extra...)...)
|
|
|
|
return Authorization{}, err
|
|
|
|
}
|
|
|
|
s.auditLog(ctx, operation, &auth.User.ID, auth.User.Email, extra...)
|
|
|
|
return auth, nil
|
|
|
|
}
|
|
|
|
|
2020-01-24 13:38:53 +00:00
|
|
|
// Payments separates all payment related functionality.
|
2019-10-17 15:42:18 +01:00
|
|
|
func (s *Service) Payments() PaymentsService {
|
|
|
|
return PaymentsService{service: s}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetupAccount creates payment account for authorized user.
|
2020-01-29 00:57:15 +00:00
|
|
|
func (paymentService PaymentsService) SetupAccount(ctx context.Context) (err error) {
|
2019-10-17 15:42:18 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "setup payment account")
|
2019-10-17 15:42:18 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
return paymentService.service.accounts.Setup(ctx, auth.User.ID, auth.User.Email)
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// AccountBalance return account balance.
|
2020-05-12 18:16:04 +01:00
|
|
|
func (paymentService PaymentsService) AccountBalance(ctx context.Context) (balance payments.Balance, err error) {
|
2019-10-17 15:42:18 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "get account balance")
|
2019-10-17 15:42:18 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return payments.Balance{}, Error.Wrap(err)
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
return paymentService.service.accounts.Balance(ctx, auth.User.ID)
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
2019-10-23 18:33:24 +01:00
|
|
|
// AddCreditCard is used to save new credit card and attach it to payment account.
|
2020-01-29 00:57:15 +00:00
|
|
|
func (paymentService PaymentsService) AddCreditCard(ctx context.Context, creditCardToken string) (err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx, creditCardToken)(&err)
|
2019-10-17 15:42:18 +01:00
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "add credit card")
|
2019-10-17 15:42:18 +01:00
|
|
|
if err != nil {
|
2020-01-07 10:41:19 +00:00
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-10-06 11:45:38 +01:00
|
|
|
err = paymentService.service.accounts.CreditCards().Add(ctx, auth.User.ID, creditCardToken)
|
2020-01-29 00:57:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2021-07-01 00:13:45 +01:00
|
|
|
if !auth.User.PaidTier {
|
|
|
|
// put this user into the paid tier and convert projects to upgraded limits.
|
|
|
|
err = paymentService.service.store.Users().UpdatePaidTier(ctx, auth.User.ID, true)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
projects, err := paymentService.service.store.Projects().GetOwn(ctx, auth.User.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
for _, project := range projects {
|
|
|
|
if project.StorageLimit == nil || *project.StorageLimit < paymentService.service.config.UsageLimits.Storage.Paid {
|
|
|
|
project.StorageLimit = new(memory.Size)
|
|
|
|
*project.StorageLimit = paymentService.service.config.UsageLimits.Storage.Paid
|
|
|
|
}
|
|
|
|
if project.BandwidthLimit == nil || *project.BandwidthLimit < paymentService.service.config.UsageLimits.Bandwidth.Paid {
|
|
|
|
project.BandwidthLimit = new(memory.Size)
|
|
|
|
*project.BandwidthLimit = paymentService.service.config.UsageLimits.Bandwidth.Paid
|
|
|
|
}
|
|
|
|
err = paymentService.service.store.Projects().Update(ctx, &project)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
return nil
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
2019-10-23 18:33:24 +01:00
|
|
|
// MakeCreditCardDefault makes a credit card default payment method.
|
2020-01-29 00:57:15 +00:00
|
|
|
func (paymentService PaymentsService) MakeCreditCardDefault(ctx context.Context, cardID string) (err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx, cardID)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "make credit card default")
|
2019-10-23 18:33:24 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-10-23 18:33:24 +01:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
return paymentService.service.accounts.CreditCards().MakeDefault(ctx, auth.User.ID, cardID)
|
2019-10-23 18:33:24 +01:00
|
|
|
}
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
// ProjectsCharges returns how much money current user will be charged for each project which he owns.
|
2020-03-04 13:23:10 +00:00
|
|
|
func (paymentService PaymentsService) ProjectsCharges(ctx context.Context, since, before time.Time) (_ []payments.ProjectCharge, err error) {
|
2019-11-15 14:27:44 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "project charges")
|
2019-11-15 14:27:44 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-11-15 14:27:44 +00:00
|
|
|
}
|
|
|
|
|
2020-03-04 13:23:10 +00:00
|
|
|
return paymentService.service.accounts.ProjectCharges(ctx, auth.User.ID, since, before)
|
2019-11-15 14:27:44 +00:00
|
|
|
}
|
|
|
|
|
2019-10-23 18:33:24 +01:00
|
|
|
// ListCreditCards returns a list of credit cards for a given payment account.
|
2020-01-29 00:57:15 +00:00
|
|
|
func (paymentService PaymentsService) ListCreditCards(ctx context.Context) (_ []payments.CreditCard, err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "list credit cards")
|
2019-10-23 18:33:24 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-10-23 18:33:24 +01:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
return paymentService.service.accounts.CreditCards().List(ctx, auth.User.ID)
|
2019-10-23 18:33:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveCreditCard is used to detach a credit card from payment account.
|
2020-01-29 00:57:15 +00:00
|
|
|
func (paymentService PaymentsService) RemoveCreditCard(ctx context.Context, cardID string) (err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx, cardID)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "remove credit card")
|
2019-10-23 18:33:24 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-10-23 18:33:24 +01:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
return paymentService.service.accounts.CreditCards().Remove(ctx, auth.User.ID, cardID)
|
2019-10-23 18:33:24 +01:00
|
|
|
}
|
|
|
|
|
2020-01-18 02:34:06 +00:00
|
|
|
// BillingHistory returns a list of billing history items for payment account.
|
2020-01-29 00:57:15 +00:00
|
|
|
func (paymentService PaymentsService) BillingHistory(ctx context.Context) (billingHistory []*BillingHistoryItem, err error) {
|
2019-10-31 16:56:54 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "get billing history")
|
2019-10-31 16:56:54 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-10-31 16:56:54 +00:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
invoices, err := paymentService.service.accounts.Invoices().List(ctx, auth.User.ID)
|
2019-10-31 16:56:54 +00:00
|
|
|
if err != nil {
|
2020-01-03 14:21:05 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-10-31 16:56:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, invoice := range invoices {
|
|
|
|
billingHistory = append(billingHistory, &BillingHistoryItem{
|
|
|
|
ID: invoice.ID,
|
|
|
|
Description: invoice.Description,
|
|
|
|
Amount: invoice.Amount,
|
|
|
|
Status: invoice.Status,
|
|
|
|
Link: invoice.Link,
|
|
|
|
End: invoice.End,
|
|
|
|
Start: invoice.Start,
|
|
|
|
Type: Invoice,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
txsInfos, err := paymentService.service.accounts.StorjTokens().ListTransactionInfos(ctx, auth.User.ID)
|
2019-11-12 11:14:34 +00:00
|
|
|
if err != nil {
|
2020-01-03 14:21:05 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-11-12 11:14:34 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 13:23:16 +00:00
|
|
|
for _, info := range txsInfos {
|
2020-01-03 14:21:05 +00:00
|
|
|
billingHistory = append(billingHistory, &BillingHistoryItem{
|
|
|
|
ID: info.ID.String(),
|
|
|
|
Description: "STORJ Token Deposit",
|
|
|
|
Amount: info.AmountCents,
|
|
|
|
Received: info.ReceivedCents,
|
|
|
|
Status: info.Status.String(),
|
|
|
|
Link: info.Link,
|
|
|
|
Start: info.CreatedAt,
|
|
|
|
End: info.ExpiresAt,
|
|
|
|
Type: Transaction,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
charges, err := paymentService.service.accounts.Charges(ctx, auth.User.ID)
|
2020-01-03 14:21:05 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, charge := range charges {
|
|
|
|
desc := fmt.Sprintf("Payment(%s %s)", charge.CardInfo.Brand, charge.CardInfo.LastFour)
|
|
|
|
|
|
|
|
billingHistory = append(billingHistory, &BillingHistoryItem{
|
|
|
|
ID: charge.ID,
|
|
|
|
Description: desc,
|
|
|
|
Amount: charge.Amount,
|
|
|
|
Start: charge.CreatedAt,
|
|
|
|
Type: Charge,
|
|
|
|
})
|
2019-11-12 11:14:34 +00:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
coupons, err := paymentService.service.accounts.Coupons().ListByUserID(ctx, auth.User.ID)
|
2020-01-07 10:41:19 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2020-01-07 10:41:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, coupon := range coupons {
|
2020-05-14 11:34:42 +01:00
|
|
|
alreadyUsed, err := paymentService.service.accounts.Coupons().TotalUsage(ctx, coupon.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
remaining := coupon.Amount - alreadyUsed
|
|
|
|
if coupon.Status == payments.CouponExpired {
|
|
|
|
remaining = 0
|
|
|
|
}
|
|
|
|
|
2020-06-10 12:42:44 +01:00
|
|
|
var couponStatus string
|
|
|
|
|
|
|
|
switch coupon.Status {
|
|
|
|
case 0:
|
|
|
|
couponStatus = "Active"
|
|
|
|
case 1:
|
|
|
|
couponStatus = "Used"
|
|
|
|
default:
|
|
|
|
couponStatus = "Expired"
|
|
|
|
}
|
|
|
|
|
2021-03-30 00:37:46 +01:00
|
|
|
billingHistoryItem := &BillingHistoryItem{
|
|
|
|
ID: coupon.ID.String(),
|
|
|
|
Description: coupon.Description,
|
|
|
|
Amount: coupon.Amount,
|
|
|
|
Remaining: remaining,
|
|
|
|
Status: couponStatus,
|
|
|
|
Link: "",
|
|
|
|
Start: coupon.Created,
|
|
|
|
Type: Coupon,
|
|
|
|
}
|
|
|
|
if coupon.ExpirationDate() != nil {
|
|
|
|
billingHistoryItem.End = *coupon.ExpirationDate()
|
|
|
|
}
|
|
|
|
billingHistory = append(billingHistory, billingHistoryItem)
|
2020-01-07 10:41:19 +00:00
|
|
|
}
|
|
|
|
|
2020-05-28 12:31:02 +01:00
|
|
|
bonuses, err := paymentService.service.accounts.StorjTokens().ListDepositBonuses(ctx, auth.User.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, bonus := range bonuses {
|
|
|
|
billingHistory = append(billingHistory,
|
|
|
|
&BillingHistoryItem{
|
2020-06-02 09:29:43 +01:00
|
|
|
Description: fmt.Sprintf("%d%% Bonus for STORJ Token Deposit", bonus.Percentage),
|
2020-05-28 12:31:02 +01:00
|
|
|
Amount: bonus.AmountCents,
|
|
|
|
Status: "Added to balance",
|
|
|
|
Start: bonus.CreatedAt,
|
|
|
|
Type: DepositBonus,
|
2020-01-24 13:38:53 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-11-12 11:14:34 +00:00
|
|
|
sort.SliceStable(billingHistory,
|
|
|
|
func(i, j int) bool {
|
|
|
|
return billingHistory[i].Start.After(billingHistory[j].Start)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
2019-10-31 16:56:54 +00:00
|
|
|
return billingHistory, nil
|
|
|
|
}
|
|
|
|
|
2019-11-12 11:14:34 +00:00
|
|
|
// TokenDeposit creates new deposit transaction for adding STORJ tokens to account balance.
|
2020-01-29 00:57:15 +00:00
|
|
|
func (paymentService PaymentsService) TokenDeposit(ctx context.Context, amount int64) (_ *payments.Transaction, err error) {
|
2019-11-12 11:14:34 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "token deposit")
|
2019-11-12 11:14:34 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-11-12 11:14:34 +00:00
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
tx, err := paymentService.service.accounts.StorjTokens().Deposit(ctx, auth.User.ID, amount)
|
2021-04-08 18:34:23 +01:00
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
return tx, Error.Wrap(err)
|
2019-11-12 11:14:34 +00:00
|
|
|
}
|
|
|
|
|
2020-10-09 14:40:12 +01:00
|
|
|
// checkOutstandingInvoice returns if the payment account has any unpaid/outstanding invoices or/and invoice items.
|
|
|
|
func (paymentService PaymentsService) checkOutstandingInvoice(ctx context.Context) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "get outstanding invoices")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
invoices, err := paymentService.service.accounts.Invoices().List(ctx, auth.User.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(invoices) > 0 {
|
|
|
|
for _, invoice := range invoices {
|
|
|
|
if invoice.Status != string(stripe.InvoiceStatusPaid) {
|
|
|
|
return ErrUsage.New("user has unpaid/pending invoices")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hasItems, err := paymentService.service.accounts.Invoices().CheckPendingItems(ctx, auth.User.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if hasItems {
|
|
|
|
return ErrUsage.New("user has pending invoice items")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// checkProjectInvoicingStatus returns if for the given project there are outstanding project records and/or usage
|
|
|
|
// which have not been applied/invoiced yet (meaning sent over to stripe).
|
|
|
|
func (paymentService PaymentsService) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
_, err = paymentService.service.getAuthAndAuditLog(ctx, "project charges")
|
|
|
|
if err != nil {
|
|
|
|
return false, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return paymentService.service.accounts.CheckProjectInvoicingStatus(ctx, projectID)
|
|
|
|
}
|
|
|
|
|
2021-06-22 01:09:56 +01:00
|
|
|
// ApplyCouponCode applies a coupon code to a Stripe customer.
|
|
|
|
func (paymentService PaymentsService) ApplyCouponCode(ctx context.Context, couponCode string) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "apply coupon code")
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
return paymentService.service.accounts.Coupons().ApplyCouponCode(ctx, auth.User.ID, couponCode)
|
|
|
|
}
|
|
|
|
|
2021-07-08 20:06:07 +01:00
|
|
|
// HasCouponApplied checks if a user as a coupon applied to their Stripe account.
|
|
|
|
func (paymentService PaymentsService) HasCouponApplied(ctx context.Context) (_ bool, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
auth, err := paymentService.service.getAuthAndAuditLog(ctx, "list coupon codes")
|
|
|
|
if err != nil {
|
|
|
|
return false, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return paymentService.service.accounts.Coupons().HasCouponApplied(ctx, auth.User.ID)
|
|
|
|
}
|
|
|
|
|
2020-01-29 00:57:15 +00:00
|
|
|
// AddPromotionalCoupon creates new coupon for specified user.
|
2020-05-12 11:05:52 +01:00
|
|
|
func (paymentService PaymentsService) AddPromotionalCoupon(ctx context.Context, userID uuid.UUID) (err error) {
|
2020-01-29 00:57:15 +00:00
|
|
|
defer mon.Task()(&ctx, userID)(&err)
|
|
|
|
|
2020-03-16 19:34:15 +00:00
|
|
|
return paymentService.service.accounts.Coupons().AddPromotionalCoupon(ctx, userID)
|
2020-01-18 02:34:06 +00:00
|
|
|
}
|
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
// checkRegistrationSecret returns a RegistrationToken if applicable (nil if not), and an error
|
|
|
|
// if and only if the registration shouldn't proceed.
|
|
|
|
func (s *Service) checkRegistrationSecret(ctx context.Context, tokenSecret RegistrationSecret) (*RegistrationToken, error) {
|
|
|
|
if s.config.OpenRegistrationEnabled && tokenSecret.IsZero() {
|
|
|
|
// in this case we're going to let the registration happen without a token
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// in all other cases, require a registration token
|
|
|
|
registrationToken, err := s.store.RegistrationTokens().GetBySecret(ctx, tokenSecret)
|
|
|
|
if err != nil {
|
|
|
|
return nil, ErrUnauthorized.Wrap(err)
|
|
|
|
}
|
|
|
|
// if a registration token is already associated with an user ID, that means the token is already used
|
|
|
|
// we should terminate the account creation process and return an error
|
|
|
|
if registrationToken.OwnerID != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, ErrValidation.New(usedRegTokenErrMsg)
|
2020-03-11 15:36:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return registrationToken, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// CreateUser gets password hash value and creates new inactive User.
|
2021-02-04 18:16:49 +00:00
|
|
|
func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret RegistrationSecret) (u *User, err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2021-06-25 12:17:55 +01:00
|
|
|
|
|
|
|
if s.config.Recaptcha.Enabled {
|
|
|
|
valid, err := s.recaptchaHandler.Verify(ctx, user.RecaptchaResponse, user.IP)
|
|
|
|
if err != nil {
|
|
|
|
s.log.Error("reCAPTCHA authorization failed", zap.Error(err))
|
2021-07-15 22:06:23 +01:00
|
|
|
return nil, ErrRecaptcha.Wrap(err)
|
2021-06-25 12:17:55 +01:00
|
|
|
}
|
|
|
|
if !valid {
|
2021-07-15 22:06:23 +01:00
|
|
|
return nil, ErrRecaptcha.New("reCAPTCHA validation unsuccessful")
|
2021-06-25 12:17:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-10 15:57:06 +00:00
|
|
|
if err := user.IsValid(); err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-29 16:23:44 +00:00
|
|
|
}
|
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
registrationToken, err := s.checkRegistrationSecret(ctx, tokenSecret)
|
2020-02-12 18:53:30 +00:00
|
|
|
if err != nil {
|
2021-07-15 22:06:23 +01:00
|
|
|
return nil, ErrRegToken.Wrap(err)
|
2019-08-14 16:27:22 +01:00
|
|
|
}
|
|
|
|
|
2019-09-10 15:00:33 +01:00
|
|
|
u, err = s.store.Users().GetByEmail(ctx, user.Email)
|
2019-08-14 16:27:22 +01:00
|
|
|
if err == nil {
|
2020-03-26 15:35:14 +00:00
|
|
|
return nil, ErrEmailUsed.New(emailUsedErrMsg)
|
2019-08-14 16:27:22 +01:00
|
|
|
}
|
2020-07-16 16:50:15 +01:00
|
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
2020-03-11 15:36:55 +00:00
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
2019-08-14 16:27:22 +01:00
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), s.config.PasswordCost)
|
2019-08-14 16:27:22 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-08-14 16:27:22 +01:00
|
|
|
}
|
|
|
|
|
2019-06-03 14:46:57 +01:00
|
|
|
// store data
|
2019-12-19 10:07:56 +00:00
|
|
|
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
2019-11-20 19:16:27 +00:00
|
|
|
userID, err := uuid.New()
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2019-07-17 21:53:14 +01:00
|
|
|
newUser := &User{
|
2021-04-27 19:40:03 +01:00
|
|
|
ID: userID,
|
|
|
|
Email: user.Email,
|
|
|
|
FullName: user.FullName,
|
|
|
|
ShortName: user.ShortName,
|
|
|
|
PasswordHash: hash,
|
|
|
|
Status: Inactive,
|
|
|
|
IsProfessional: user.IsProfessional,
|
|
|
|
Position: user.Position,
|
|
|
|
CompanyName: user.CompanyName,
|
|
|
|
EmployeeCount: user.EmployeeCount,
|
|
|
|
HaveSalesContact: user.HaveSalesContact,
|
2019-07-17 21:53:14 +01:00
|
|
|
}
|
2021-04-27 19:40:03 +01:00
|
|
|
|
2019-07-17 21:53:14 +01:00
|
|
|
if user.PartnerID != "" {
|
2020-04-02 13:30:43 +01:00
|
|
|
newUser.PartnerID, err = uuid.FromString(user.PartnerID)
|
2019-07-17 21:53:14 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-07-17 21:53:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-15 17:49:37 +01:00
|
|
|
if registrationToken != nil {
|
|
|
|
newUser.ProjectLimit = registrationToken.ProjectLimit
|
2021-03-22 20:26:59 +00:00
|
|
|
} else {
|
|
|
|
newUser.ProjectLimit = s.config.DefaultProjectLimit
|
2020-07-15 17:49:37 +01:00
|
|
|
}
|
|
|
|
|
2019-06-03 14:46:57 +01:00
|
|
|
u, err = tx.Users().Insert(ctx,
|
2019-07-17 21:53:14 +01:00
|
|
|
newUser,
|
2019-06-03 14:46:57 +01:00
|
|
|
)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-06-03 14:46:57 +01:00
|
|
|
}
|
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
if registrationToken != nil {
|
|
|
|
err = tx.RegistrationTokens().UpdateOwner(ctx, registrationToken.Secret, u.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
2019-06-03 14:46:57 +01:00
|
|
|
}
|
|
|
|
|
2019-09-27 10:46:37 +01:00
|
|
|
return nil
|
2019-06-03 14:46:57 +01:00
|
|
|
})
|
|
|
|
|
2019-03-19 17:55:43 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
s.auditLog(ctx, "create user", nil, user.Email)
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
return u, nil
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2021-06-25 12:17:55 +01:00
|
|
|
// TestSwapRecaptchaHandler replaces the existing handler for reCAPTCHAs with
|
|
|
|
// the one specified for use in testing.
|
|
|
|
func (s *Service) TestSwapRecaptchaHandler(h RecaptchaHandler) {
|
|
|
|
s.recaptchaHandler = h
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GenerateActivationToken - is a method for generating activation token.
|
2019-03-26 15:56:16 +00:00
|
|
|
func (s *Service) GenerateActivationToken(ctx context.Context, id uuid.UUID, email string) (token string, err error) {
|
2019-01-30 15:04:40 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-10-13 13:47:55 +01:00
|
|
|
// TODO: activation token should differ from auth token
|
2019-01-30 15:04:40 +00:00
|
|
|
claims := &consoleauth.Claims{
|
|
|
|
ID: id,
|
|
|
|
Email: email,
|
2019-03-02 15:22:20 +00:00
|
|
|
Expiration: time.Now().Add(time.Hour * 24),
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2019-06-04 12:55:38 +01:00
|
|
|
return s.createToken(ctx, claims)
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GeneratePasswordRecoveryToken - is a method for generating password recovery token.
|
2019-05-13 16:53:52 +01:00
|
|
|
func (s *Service) GeneratePasswordRecoveryToken(ctx context.Context, id uuid.UUID) (token string, err error) {
|
2019-04-10 20:16:10 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-05-13 16:53:52 +01:00
|
|
|
resetPasswordToken, err := s.store.ResetPasswordTokens().GetByOwnerID(ctx, id)
|
|
|
|
if err == nil {
|
|
|
|
err := s.store.ResetPasswordTokens().Delete(ctx, resetPasswordToken.Secret)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return "", Error.Wrap(err)
|
2019-05-13 16:53:52 +01:00
|
|
|
}
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2019-05-13 16:53:52 +01:00
|
|
|
resetPasswordToken, err = s.store.ResetPasswordTokens().Create(ctx, id)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return "", Error.Wrap(err)
|
2019-05-13 16:53:52 +01:00
|
|
|
}
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
s.auditLog(ctx, "generate password recovery token", &id, "")
|
|
|
|
|
2019-05-13 16:53:52 +01:00
|
|
|
return resetPasswordToken.Secret.String(), nil
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// ActivateAccount - is a method for activating user account after registration.
|
2019-03-08 14:01:11 +00:00
|
|
|
func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (err error) {
|
2019-01-30 15:04:40 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
token, err := consoleauth.FromBase64URLString(activationToken)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2019-06-04 12:55:38 +01:00
|
|
|
claims, err := s.authenticate(ctx, token)
|
2019-01-30 15:04:40 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return err
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2019-09-10 15:00:33 +01:00
|
|
|
_, err = s.store.Users().GetByEmail(ctx, claims.Email)
|
2019-04-05 16:08:14 +01:00
|
|
|
if err == nil {
|
2020-02-10 12:03:38 +00:00
|
|
|
return ErrEmailUsed.New(emailUsedErrMsg)
|
2019-04-05 16:08:14 +01:00
|
|
|
}
|
|
|
|
|
2019-01-30 15:04:40 +00:00
|
|
|
user, err := s.store.Users().Get(ctx, claims.ID)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
if now.After(user.CreatedAt.Add(tokenExpirationTime)) {
|
2019-11-12 13:14:31 +00:00
|
|
|
return ErrTokenExpiration.Wrap(err)
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2019-02-11 10:33:56 +00:00
|
|
|
user.Status = Active
|
2019-04-10 01:15:12 +01:00
|
|
|
err = s.store.Users().Update(ctx, user)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
2020-09-06 02:56:07 +01:00
|
|
|
s.auditLog(ctx, "activate account", &user.ID, user.Email)
|
2019-04-10 01:15:12 +01:00
|
|
|
|
2021-04-12 17:58:36 +01:00
|
|
|
s.analytics.TrackAccountVerified(user.ID, user.Email)
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
return nil
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
// ResetPassword - is a method for resetting user password.
|
2019-04-10 20:16:10 +01:00
|
|
|
func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, password string) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-05-13 16:53:52 +01:00
|
|
|
secret, err := ResetPasswordSecretFromBase64(resetPasswordToken)
|
2019-04-10 20:16:10 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
2019-05-13 16:53:52 +01:00
|
|
|
token, err := s.store.ResetPasswordTokens().GetBySecret(ctx, secret)
|
2019-04-10 20:16:10 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2019-05-13 16:53:52 +01:00
|
|
|
user, err := s.store.Users().Get(ctx, *token.OwnerID)
|
2019-04-10 20:16:10 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2019-11-25 21:36:36 +00:00
|
|
|
if err := ValidatePassword(password); err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2019-05-13 16:53:52 +01:00
|
|
|
if time.Since(token.CreatedAt) > tokenExpirationTime {
|
2020-10-06 15:25:53 +01:00
|
|
|
return ErrTokenExpiration.New(passwordRecoveryTokenIsExpiredErrMsg)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), s.config.PasswordCost)
|
2019-04-10 20:16:10 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
user.PasswordHash = hash
|
2019-05-13 16:53:52 +01:00
|
|
|
|
|
|
|
err = s.store.Users().Update(ctx, user)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-05-13 16:53:52 +01:00
|
|
|
}
|
2020-09-06 02:56:07 +01:00
|
|
|
s.auditLog(ctx, "password reset", &user.ID, user.Email)
|
2019-05-13 16:53:52 +01:00
|
|
|
|
2019-11-12 13:14:31 +00:00
|
|
|
if err = s.store.ResetPasswordTokens().Delete(ctx, token.Secret); err != nil {
|
2019-11-15 14:27:44 +00:00
|
|
|
return Error.Wrap(err)
|
2019-11-12 13:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2019-05-13 16:53:52 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// RevokeResetPasswordToken - is a method to revoke reset password token.
|
2019-05-13 16:53:52 +01:00
|
|
|
func (s *Service) RevokeResetPasswordToken(ctx context.Context, resetPasswordToken string) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
secret, err := ResetPasswordSecretFromBase64(resetPasswordToken)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-05-13 16:53:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return s.store.ResetPasswordTokens().Delete(ctx, secret)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// Token authenticates User by credentials and returns auth token.
|
2021-07-13 18:21:16 +01:00
|
|
|
func (s *Service) Token(ctx context.Context, request AuthUser) (token string, err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-01-08 13:54:12 +00:00
|
|
|
|
2021-07-13 18:21:16 +01:00
|
|
|
user, err := s.store.Users().GetByEmail(ctx, request.Email)
|
2018-11-14 10:50:15 +00:00
|
|
|
if err != nil {
|
2019-11-18 11:38:43 +00:00
|
|
|
return "", ErrUnauthorized.New(credentialsErrMsg)
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2021-07-13 18:21:16 +01:00
|
|
|
err = bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(request.Password))
|
2018-12-10 15:57:06 +00:00
|
|
|
if err != nil {
|
2020-04-08 20:40:49 +01:00
|
|
|
return "", ErrUnauthorized.New(credentialsErrMsg)
|
2018-12-10 13:47:48 +00:00
|
|
|
}
|
|
|
|
|
2021-07-13 18:21:16 +01:00
|
|
|
if user.MFAEnabled {
|
|
|
|
if request.MFARecoveryCode != "" {
|
|
|
|
found := false
|
|
|
|
codeIndex := -1
|
|
|
|
for i, code := range user.MFARecoveryCodes {
|
|
|
|
if code == request.MFARecoveryCode {
|
|
|
|
found = true
|
|
|
|
codeIndex = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
return "", ErrUnauthorized.New(mfaRecoveryInvalidErrMsg)
|
|
|
|
}
|
|
|
|
|
|
|
|
user.MFARecoveryCodes = append(user.MFARecoveryCodes[:codeIndex], user.MFARecoveryCodes[codeIndex+1:]...)
|
|
|
|
|
|
|
|
err = s.store.Users().Update(ctx, user)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
} else if request.MFAPasscode != "" {
|
|
|
|
valid, err := ValidateMFAPasscode(request.MFAPasscode, user.MFASecretKey, time.Now())
|
|
|
|
if err != nil {
|
|
|
|
return "", ErrUnauthorized.Wrap(err)
|
|
|
|
}
|
|
|
|
if !valid {
|
|
|
|
return "", ErrUnauthorized.New(mfaPasscodeInvalidErrMsg)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return "", ErrMFAPasscodeRequired.New(mfaPasscodeRequiredErrMsg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-15 13:03:24 +00:00
|
|
|
claims := consoleauth.Claims{
|
2018-11-14 10:50:15 +00:00
|
|
|
ID: user.ID,
|
2019-01-30 15:04:40 +00:00
|
|
|
Expiration: time.Now().Add(tokenExpirationTime),
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2019-06-04 12:55:38 +01:00
|
|
|
token, err = s.createToken(ctx, &claims)
|
2018-11-14 10:50:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
2020-09-06 02:56:07 +01:00
|
|
|
s.auditLog(ctx, "login", &user.ID, user.Email)
|
2018-11-14 10:50:15 +00:00
|
|
|
|
2021-04-08 18:34:23 +01:00
|
|
|
s.analytics.TrackSignedIn(user.ID, user.Email)
|
|
|
|
|
2018-11-14 10:50:15 +00:00
|
|
|
return token, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetUser returns User by id.
|
2018-12-20 20:10:27 +00:00
|
|
|
func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (u *User, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-11-14 10:50:15 +00:00
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
user, err := s.store.Users().Get(ctx, id)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return user, nil
|
2018-11-21 15:51:43 +00:00
|
|
|
}
|
2018-11-14 10:50:15 +00:00
|
|
|
|
2021-03-31 19:34:44 +01:00
|
|
|
// GetUserID returns the User ID from the session.
|
|
|
|
func (s *Service) GetUserID(ctx context.Context) (id uuid.UUID, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get user ID")
|
|
|
|
if err != nil {
|
|
|
|
return uuid.UUID{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
return auth.User.ID, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetUserByEmail returns User by email.
|
2019-04-10 20:16:10 +01:00
|
|
|
func (s *Service) GetUserByEmail(ctx context.Context, email string) (u *User, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2019-11-12 13:14:31 +00:00
|
|
|
result, err := s.store.Users().GetByEmail(ctx, email)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// UpdateAccount updates User.
|
2019-10-25 13:07:17 +01:00
|
|
|
func (s *Service) UpdateAccount(ctx context.Context, fullName string, shortName string) (err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "update account")
|
2018-11-28 10:31:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2018-11-28 10:31:15 +00:00
|
|
|
}
|
|
|
|
|
2019-10-25 13:07:17 +01:00
|
|
|
// validate fullName
|
2019-11-25 21:36:36 +00:00
|
|
|
err = ValidateFullName(fullName)
|
2019-11-12 13:14:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return ErrValidation.Wrap(err)
|
2018-11-29 16:23:44 +00:00
|
|
|
}
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
err = s.store.Users().Update(ctx, &User{
|
2018-12-24 12:52:52 +00:00
|
|
|
ID: auth.User.ID,
|
2019-10-25 13:07:17 +01:00
|
|
|
FullName: fullName,
|
|
|
|
ShortName: shortName,
|
2019-04-25 16:06:19 +01:00
|
|
|
Email: auth.User.Email,
|
2018-12-10 15:57:06 +00:00
|
|
|
PasswordHash: nil,
|
2019-04-23 15:46:54 +01:00
|
|
|
Status: auth.User.Status,
|
2018-11-28 10:31:15 +00:00
|
|
|
})
|
2019-04-10 01:15:12 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2018-11-28 10:31:15 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 16:16:55 +00:00
|
|
|
// ChangeEmail updates email for a given user.
|
|
|
|
func (s *Service) ChangeEmail(ctx context.Context, newEmail string) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "change email")
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := mail.ParseAddress(newEmail); err != nil {
|
|
|
|
return ErrValidation.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.store.Users().GetByEmail(ctx, newEmail)
|
|
|
|
if err == nil {
|
|
|
|
return ErrEmailUsed.New(emailUsedErrMsg)
|
|
|
|
}
|
|
|
|
|
|
|
|
auth.User.Email = newEmail
|
|
|
|
err = s.store.Users().Update(ctx, &auth.User)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// ChangePassword updates password for a given user.
|
2018-12-24 12:52:52 +00:00
|
|
|
func (s *Service) ChangePassword(ctx context.Context, pass, newPass string) (err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "change password")
|
2018-12-10 15:57:06 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2018-12-10 15:57:06 +00:00
|
|
|
}
|
|
|
|
|
2018-12-24 12:52:52 +00:00
|
|
|
err = bcrypt.CompareHashAndPassword(auth.User.PasswordHash, []byte(pass))
|
2018-12-10 15:57:06 +00:00
|
|
|
if err != nil {
|
2020-04-08 20:40:49 +01:00
|
|
|
return ErrUnauthorized.New(credentialsErrMsg)
|
2018-12-10 15:57:06 +00:00
|
|
|
}
|
|
|
|
|
2019-11-25 21:36:36 +00:00
|
|
|
if err := ValidatePassword(newPass); err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return ErrValidation.Wrap(err)
|
2018-12-10 15:57:06 +00:00
|
|
|
}
|
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(newPass), s.config.PasswordCost)
|
2018-12-10 15:57:06 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2018-12-10 15:57:06 +00:00
|
|
|
}
|
|
|
|
|
2018-12-24 12:52:52 +00:00
|
|
|
auth.User.PasswordHash = hash
|
2019-04-10 01:15:12 +01:00
|
|
|
err = s.store.Users().Update(ctx, &auth.User)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2018-12-10 15:57:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// DeleteAccount deletes User.
|
2018-12-24 12:52:52 +00:00
|
|
|
func (s *Service) DeleteAccount(ctx context.Context, password string) (err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "delete account")
|
2018-11-27 14:20:58 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2018-11-27 14:20:58 +00:00
|
|
|
}
|
|
|
|
|
2018-12-14 16:14:17 +00:00
|
|
|
err = bcrypt.CompareHashAndPassword(auth.User.PasswordHash, []byte(password))
|
|
|
|
if err != nil {
|
2020-04-08 20:40:49 +01:00
|
|
|
return ErrUnauthorized.New(credentialsErrMsg)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
2020-10-09 14:40:12 +01:00
|
|
|
err = s.Payments().checkOutstandingInvoice(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
err = s.store.Users().Delete(ctx, auth.User.ID)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2018-12-14 16:14:17 +00:00
|
|
|
}
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
return nil
|
2018-11-27 14:20:58 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetProject is a method for querying project by id.
|
2018-12-20 20:10:27 +00:00
|
|
|
func (s *Service) GetProject(ctx context.Context, projectID uuid.UUID) (p *Project, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-10-06 15:25:53 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get project", zap.String("projectID", projectID.String()))
|
2018-11-26 10:47:23 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err = s.isProjectMember(ctx, auth.User.ID, projectID); err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
p, err = s.store.Projects().Get(ctx, projectID)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetUsersProjects is a method for querying all projects.
|
2018-12-20 20:10:27 +00:00
|
|
|
func (s *Service) GetUsersProjects(ctx context.Context) (ps []Project, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get users projects")
|
2018-11-26 10:47:23 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
ps, err = s.store.Projects().GetByUserID(ctx, auth.User.ID)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2021-01-21 18:19:37 +00:00
|
|
|
// GetUsersOwnedProjectsPage is a method for querying paged projects.
|
|
|
|
func (s *Service) GetUsersOwnedProjectsPage(ctx context.Context, cursor ProjectsCursor) (_ ProjectsPage, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get user's owned projects page")
|
|
|
|
if err != nil {
|
|
|
|
return ProjectsPage{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
projects, err := s.store.Projects().ListByOwnerID(ctx, auth.User.ID, cursor)
|
|
|
|
if err != nil {
|
|
|
|
return ProjectsPage{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return projects, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// CreateProject is a method for creating new project.
|
2018-12-20 20:10:27 +00:00
|
|
|
func (s *Service) CreateProject(ctx context.Context, projectInfo ProjectInfo) (p *Project, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "create project")
|
2018-11-26 10:47:23 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2021-04-08 18:34:23 +01:00
|
|
|
currentProjectCount, err := s.checkProjectLimit(ctx, auth.User.ID)
|
2019-03-19 17:55:43 +00:00
|
|
|
if err != nil {
|
2019-12-09 13:20:44 +00:00
|
|
|
return nil, ErrProjLimit.Wrap(err)
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
|
|
|
|
2021-04-08 18:34:23 +01:00
|
|
|
var projectID uuid.UUID
|
2019-12-19 10:07:56 +00:00
|
|
|
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
2021-07-01 00:13:45 +01:00
|
|
|
storageLimit := s.config.UsageLimits.Storage.Free
|
|
|
|
bandwidthLimit := s.config.UsageLimits.Bandwidth.Free
|
|
|
|
if auth.User.PaidTier {
|
|
|
|
storageLimit = s.config.UsageLimits.Storage.Paid
|
|
|
|
bandwidthLimit = s.config.UsageLimits.Bandwidth.Paid
|
|
|
|
}
|
2019-06-03 14:46:57 +01:00
|
|
|
p, err = tx.Projects().Insert(ctx,
|
|
|
|
&Project{
|
2021-03-22 20:26:59 +00:00
|
|
|
Description: projectInfo.Description,
|
|
|
|
Name: projectInfo.Name,
|
|
|
|
OwnerID: auth.User.ID,
|
|
|
|
PartnerID: auth.User.PartnerID,
|
2021-07-01 00:13:45 +01:00
|
|
|
StorageLimit: &storageLimit,
|
|
|
|
BandwidthLimit: &bandwidthLimit,
|
2019-06-03 14:46:57 +01:00
|
|
|
},
|
|
|
|
)
|
2018-12-26 14:00:53 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2018-12-26 14:00:53 +00:00
|
|
|
}
|
|
|
|
|
2019-06-03 14:46:57 +01:00
|
|
|
_, err = tx.ProjectMembers().Insert(ctx, auth.User.ID, p.ID)
|
2019-04-10 01:15:12 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
2018-12-26 14:00:53 +00:00
|
|
|
|
2021-04-08 18:34:23 +01:00
|
|
|
projectID = p.ID
|
|
|
|
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil
|
2019-06-03 14:46:57 +01:00
|
|
|
})
|
2021-04-08 18:34:23 +01:00
|
|
|
|
2018-12-10 12:29:01 +00:00
|
|
|
if err != nil {
|
2020-03-16 19:34:15 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-10 12:29:01 +00:00
|
|
|
}
|
2018-12-06 15:19:47 +00:00
|
|
|
|
2021-04-08 18:34:23 +01:00
|
|
|
s.analytics.TrackProjectCreated(auth.User.ID, projectID, currentProjectCount+1)
|
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
// ToDo: check if this is actually the right place.
|
2020-03-16 19:34:15 +00:00
|
|
|
err = s.accounts.Coupons().AddPromotionalCoupon(ctx, auth.User.ID)
|
2020-01-29 00:57:15 +00:00
|
|
|
if err != nil {
|
|
|
|
s.log.Debug(fmt.Sprintf("could not add promotional coupon for user %s", auth.User.ID.String()), zap.Error(Error.Wrap(err)))
|
|
|
|
}
|
|
|
|
|
2019-06-03 14:46:57 +01:00
|
|
|
return p, nil
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// DeleteProject is a method for deleting project by id.
|
2018-12-20 20:10:27 +00:00
|
|
|
func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "delete project", zap.String("projectID", projectID.String()))
|
2018-12-18 17:43:02 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2018-12-18 17:43:02 +00:00
|
|
|
}
|
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
_, err = s.isProjectOwner(ctx, auth.User.ID, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
2019-03-29 12:13:37 +00:00
|
|
|
}
|
|
|
|
|
2020-10-09 14:40:12 +01:00
|
|
|
err = s.checkProjectCanBeDeleted(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
err = s.store.Projects().Delete(ctx, projectID)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2020-09-10 10:32:35 +01:00
|
|
|
// UpdateProject is a method for updating project name and description by id.
|
|
|
|
func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, name string, description string) (p *Project, err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-10 10:32:35 +01:00
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "update project name and description", zap.String("projectID", projectID.String()))
|
2020-09-10 10:32:35 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
err = ValidateNameAndDescription(name, description)
|
2018-11-26 10:47:23 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2019-03-29 12:13:37 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, auth.User.ID, projectID)
|
2018-11-26 10:47:23 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
2019-03-29 12:13:37 +00:00
|
|
|
project := isMember.project
|
2020-09-10 10:32:35 +01:00
|
|
|
project.Name = name
|
2018-11-28 16:20:23 +00:00
|
|
|
project.Description = description
|
2018-11-26 10:47:23 +00:00
|
|
|
|
|
|
|
err = s.store.Projects().Update(ctx, project)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return project, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// AddProjectMembers adds users by email to given project.
|
2019-03-06 15:42:19 +00:00
|
|
|
func (s *Service) AddProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (users []*User, err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "add project members", zap.String("projectID", projectID.String()), zap.Strings("emails", emails))
|
2018-12-06 14:40:32 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-06 14:40:32 +00:00
|
|
|
}
|
|
|
|
|
2018-12-27 15:30:15 +00:00
|
|
|
if _, err = s.isProjectMember(ctx, auth.User.ID, projectID); err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2018-12-21 15:41:53 +00:00
|
|
|
var userErr errs.Group
|
|
|
|
|
|
|
|
// collect user querying errors
|
|
|
|
for _, email := range emails {
|
|
|
|
user, err := s.store.Users().GetByEmail(ctx, email)
|
|
|
|
if err != nil {
|
|
|
|
userErr.Add(err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-03-06 15:42:19 +00:00
|
|
|
users = append(users, user)
|
2018-12-21 15:41:53 +00:00
|
|
|
}
|
|
|
|
|
2018-12-27 15:30:15 +00:00
|
|
|
if err = userErr.Err(); err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, ErrValidation.New(teamMemberDoesNotExistErrMsg)
|
2018-12-21 15:41:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// add project members in transaction scope
|
2019-12-19 10:07:56 +00:00
|
|
|
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
|
|
|
for _, user := range users {
|
|
|
|
if _, err := tx.ProjectMembers().Insert(ctx, user.ID, projectID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
2018-12-21 15:41:53 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-21 15:41:53 +00:00
|
|
|
}
|
|
|
|
|
2019-03-06 15:42:19 +00:00
|
|
|
return users, nil
|
2018-12-06 14:40:32 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// DeleteProjectMembers removes users by email from given project.
|
2018-12-21 15:41:53 +00:00
|
|
|
func (s *Service) DeleteProjectMembers(ctx context.Context, projectID uuid.UUID, emails []string) (err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-10-06 11:40:31 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "delete project members", zap.String("projectID", projectID.String()), zap.Strings("emails", emails))
|
2018-12-06 14:40:32 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2018-12-06 14:40:32 +00:00
|
|
|
}
|
|
|
|
|
2020-10-06 11:40:31 +01:00
|
|
|
if _, err = s.isProjectMember(ctx, auth.User.ID, projectID); err != nil {
|
2020-10-06 11:32:34 +01:00
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2018-12-21 15:41:53 +00:00
|
|
|
var userIDs []uuid.UUID
|
|
|
|
var userErr errs.Group
|
|
|
|
|
|
|
|
// collect user querying errors
|
|
|
|
for _, email := range emails {
|
|
|
|
user, err := s.store.Users().GetByEmail(ctx, email)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
userErr.Add(err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
isOwner, err := s.isProjectOwner(ctx, user.ID, projectID)
|
|
|
|
if isOwner {
|
|
|
|
return ErrValidation.New(projectOwnerDeletionForbiddenErrMsg, user.Email)
|
2019-09-04 16:02:39 +01:00
|
|
|
}
|
2020-10-06 15:25:53 +01:00
|
|
|
if err != nil && !ErrUnauthorized.Has(err) {
|
|
|
|
return Error.Wrap(err)
|
2019-09-04 16:02:39 +01:00
|
|
|
}
|
|
|
|
|
2018-12-21 15:41:53 +00:00
|
|
|
userIDs = append(userIDs, user.ID)
|
|
|
|
}
|
|
|
|
|
2018-12-27 15:30:15 +00:00
|
|
|
if err = userErr.Err(); err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return ErrValidation.New(teamMemberDoesNotExistErrMsg)
|
2018-12-21 15:41:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// delete project members in transaction scope
|
2019-12-19 10:07:56 +00:00
|
|
|
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
|
|
|
for _, uID := range userIDs {
|
|
|
|
err = tx.ProjectMembers().Delete(ctx, uID, projectID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return err
|
2019-12-19 10:07:56 +00:00
|
|
|
}
|
2018-12-21 15:41:53 +00:00
|
|
|
}
|
2019-12-19 10:07:56 +00:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return Error.Wrap(err)
|
2018-12-06 14:40:32 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetProjectMembers returns ProjectMembers for given Project.
|
2019-08-12 11:22:32 +01:00
|
|
|
func (s *Service) GetProjectMembers(ctx context.Context, projectID uuid.UUID, cursor ProjectMembersCursor) (pmp *ProjectMembersPage, err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
|
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get project members", zap.String("projectID", projectID.String()))
|
2018-12-10 11:38:42 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-10 11:38:42 +00:00
|
|
|
}
|
|
|
|
|
2019-04-05 16:08:14 +01:00
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-05 16:08:14 +01:00
|
|
|
}
|
|
|
|
|
2019-08-12 11:22:32 +01:00
|
|
|
if cursor.Limit > maxLimit {
|
|
|
|
cursor.Limit = maxLimit
|
2018-12-19 13:03:12 +00:00
|
|
|
}
|
|
|
|
|
2019-08-12 11:22:32 +01:00
|
|
|
pmp, err = s.store.ProjectMembers().GetPagedByProjectID(ctx, projectID, cursor)
|
2019-04-10 01:15:12 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return
|
2018-12-10 11:38:42 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// CreateAPIKey creates new api key.
|
2019-06-04 12:55:38 +01:00
|
|
|
func (s *Service) CreateAPIKey(ctx context.Context, projectID uuid.UUID, name string) (_ *APIKeyInfo, _ *macaroon.APIKey, err error) {
|
2018-12-27 15:30:15 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-12-26 14:00:53 +00:00
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "create api key", zap.String("projectID", projectID.String()))
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, nil, Error.Wrap(err)
|
2018-12-26 14:00:53 +00:00
|
|
|
}
|
|
|
|
|
2018-12-27 15:30:15 +00:00
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
2018-12-26 14:00:53 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, nil, Error.Wrap(err)
|
2018-12-26 14:00:53 +00:00
|
|
|
}
|
|
|
|
|
2019-10-10 14:28:35 +01:00
|
|
|
_, err = s.store.APIKeys().GetByNameAndProjectID(ctx, name, projectID)
|
|
|
|
if err == nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, nil, ErrValidation.New(apiKeyWithNameExistsErrMsg)
|
2019-10-10 14:28:35 +01:00
|
|
|
}
|
|
|
|
|
2019-05-24 17:51:27 +01:00
|
|
|
secret, err := macaroon.NewSecret()
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, nil, Error.Wrap(err)
|
2019-05-24 17:51:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
key, err := macaroon.NewAPIKey(secret)
|
2018-12-26 14:00:53 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, nil, Error.Wrap(err)
|
2018-12-26 14:00:53 +00:00
|
|
|
}
|
|
|
|
|
2019-07-17 21:53:14 +01:00
|
|
|
apikey := APIKeyInfo{
|
2018-12-26 14:00:53 +00:00
|
|
|
Name: name,
|
|
|
|
ProjectID: projectID,
|
2019-05-24 17:51:27 +01:00
|
|
|
Secret: secret,
|
2019-08-12 22:29:40 +01:00
|
|
|
PartnerID: auth.User.PartnerID,
|
2019-07-17 21:53:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
info, err := s.store.APIKeys().Create(ctx, key.Head(), apikey)
|
2019-04-10 01:15:12 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, nil, Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
2021-04-08 18:34:23 +01:00
|
|
|
s.analytics.TrackAccessGrantCreated(auth.User.ID)
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
return info, key, nil
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetAPIKeyInfo retrieves api key by id.
|
2019-06-04 12:55:38 +01:00
|
|
|
func (s *Service) GetAPIKeyInfo(ctx context.Context, id uuid.UUID) (_ *APIKeyInfo, err error) {
|
2018-12-27 15:30:15 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get api key info", zap.String("apiKeyID", id.String()))
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := s.store.APIKeys().Get(ctx, id)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, key.ProjectID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return key, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// DeleteAPIKeys deletes api key by id.
|
2019-02-13 11:34:40 +00:00
|
|
|
func (s *Service) DeleteAPIKeys(ctx context.Context, ids []uuid.UUID) (err error) {
|
2018-12-27 15:30:15 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-09-06 02:56:07 +01:00
|
|
|
|
|
|
|
idStrings := make([]string, 0, len(ids))
|
|
|
|
for _, id := range ids {
|
|
|
|
idStrings = append(idStrings, id.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "delete api keys", zap.Strings("apiKeyIDs", idStrings))
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2019-02-13 11:34:40 +00:00
|
|
|
var keysErr errs.Group
|
|
|
|
|
|
|
|
for _, keyID := range ids {
|
|
|
|
key, err := s.store.APIKeys().Get(ctx, keyID)
|
|
|
|
if err != nil {
|
|
|
|
keysErr.Add(err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, key.ProjectID)
|
|
|
|
if err != nil {
|
|
|
|
keysErr.Add(ErrUnauthorized.Wrap(err))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = keysErr.Err(); err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2019-12-19 10:07:56 +00:00
|
|
|
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
|
|
|
for _, keyToDeleteID := range ids {
|
|
|
|
err = tx.APIKeys().Delete(ctx, keyToDeleteID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return err
|
2019-12-19 10:07:56 +00:00
|
|
|
}
|
2019-02-13 11:34:40 +00:00
|
|
|
}
|
|
|
|
|
2019-12-19 10:07:56 +00:00
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 19:43:02 +00:00
|
|
|
// DeleteAPIKeyByNameAndProjectID deletes api key by name and project ID.
|
|
|
|
func (s *Service) DeleteAPIKeyByNameAndProjectID(ctx context.Context, name string, projectID uuid.UUID) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "delete api key by name and project ID", zap.String("apiKeyName", name), zap.String("projectID", projectID.String()))
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := s.store.APIKeys().GetByNameAndProjectID(ctx, name, projectID)
|
|
|
|
if err != nil {
|
2021-03-23 20:23:27 +00:00
|
|
|
return ErrNoAPIKey.New(apiKeyWithNameDoesntExistErrMsg)
|
2021-03-16 19:43:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = s.store.APIKeys().Delete(ctx, key.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetAPIKeys returns paged api key list for given Project.
|
2019-09-12 15:19:30 +01:00
|
|
|
func (s *Service) GetAPIKeys(ctx context.Context, projectID uuid.UUID, cursor APIKeyCursor) (page *APIKeyPage, err error) {
|
2018-12-27 15:30:15 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-09-12 15:19:30 +01:00
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get api keys", zap.String("projectID", projectID.String()))
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2019-09-12 15:19:30 +01:00
|
|
|
if cursor.Limit > maxLimit {
|
|
|
|
cursor.Limit = maxLimit
|
|
|
|
}
|
|
|
|
|
|
|
|
page, err = s.store.APIKeys().GetPagedByProjectID(ctx, projectID, cursor)
|
2019-04-10 01:15:12 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
2019-09-12 15:19:30 +01:00
|
|
|
return
|
2018-12-26 14:00:53 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetProjectUsage retrieves project usage for a given period.
|
2019-11-15 14:27:44 +00:00
|
|
|
func (s *Service) GetProjectUsage(ctx context.Context, projectID uuid.UUID, since, before time.Time) (_ *accounting.ProjectUsage, err error) {
|
2019-04-04 15:56:20 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get project usage", zap.String("projectID", projectID.String()))
|
2019-04-04 15:56:20 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-04 15:56:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-04 15:56:20 +01:00
|
|
|
}
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
projectUsage, err := s.projectAccounting.GetProjectTotal(ctx, projectID, since, before)
|
2019-04-10 01:15:12 +01:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 01:15:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return projectUsage, nil
|
2019-04-04 15:56:20 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetBucketTotals retrieves paged bucket total usages since project creation.
|
2019-11-15 14:27:44 +00:00
|
|
|
func (s *Service) GetBucketTotals(ctx context.Context, projectID uuid.UUID, cursor accounting.BucketUsageCursor, before time.Time) (_ *accounting.BucketUsagePage, err error) {
|
2019-05-16 11:43:46 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get bucket totals", zap.String("projectID", projectID.String()))
|
2019-05-16 11:43:46 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-05-16 11:43:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
isMember, err := s.isProjectMember(ctx, auth.User.ID, projectID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-05-16 11:43:46 +01:00
|
|
|
}
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
usage, err := s.projectAccounting.GetBucketTotals(ctx, projectID, cursor, isMember.project.CreatedAt, before)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return usage, nil
|
2019-05-16 11:43:46 +01:00
|
|
|
}
|
|
|
|
|
2020-11-13 11:41:35 +00:00
|
|
|
// GetAllBucketNames retrieves all bucket names of a specific project.
|
|
|
|
func (s *Service) GetAllBucketNames(ctx context.Context, projectID uuid.UUID) (_ []string, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-11-30 16:51:47 +00:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get all bucket names", zap.String("projectID", projectID.String()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
2020-11-13 11:41:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
listOptions := storj.BucketListOptions{
|
|
|
|
Direction: storj.Forward,
|
|
|
|
}
|
|
|
|
|
|
|
|
allowedBuckets := macaroon.AllowedBuckets{
|
|
|
|
All: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
bucketsList, err := s.buckets.ListBuckets(ctx, projectID, listOptions, allowedBuckets)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var list []string
|
|
|
|
for _, bucket := range bucketsList.Items {
|
|
|
|
list = append(list, bucket.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
return list, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// GetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period.
|
2019-11-15 14:27:44 +00:00
|
|
|
func (s *Service) GetBucketUsageRollups(ctx context.Context, projectID uuid.UUID, since, before time.Time) (_ []accounting.BucketUsageRollup, err error) {
|
2019-04-10 00:14:19 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2020-09-06 02:56:07 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get bucket usage rollups", zap.String("projectID", projectID.String()))
|
2019-04-10 00:14:19 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 00:14:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
_, err = s.isProjectMember(ctx, auth.User.ID, projectID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-04-10 00:14:19 +01:00
|
|
|
}
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
result, err := s.projectAccounting.GetBucketUsageRollups(ctx, projectID, since, before)
|
2019-11-12 13:14:31 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-11-12 13:14:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
2019-04-10 00:14:19 +01:00
|
|
|
}
|
|
|
|
|
2019-12-12 12:58:15 +00:00
|
|
|
// GetProjectUsageLimits returns project limits and current usage.
|
2020-12-22 12:05:22 +00:00
|
|
|
//
|
|
|
|
// Among others,it can return one of the following errors returned by
|
|
|
|
// storj.io/storj/satellite/accounting.Service, wrapped Error.
|
2019-12-12 12:58:15 +00:00
|
|
|
func (s *Service) GetProjectUsageLimits(ctx context.Context, projectID uuid.UUID) (_ *ProjectUsageLimits, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2021-06-24 16:49:15 +01:00
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get project usage limits", zap.String("projectID", projectID.String()))
|
2019-12-12 12:58:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
|
|
|
|
2021-06-24 16:49:15 +01:00
|
|
|
if _, err = s.isProjectMember(ctx, auth.User.ID, projectID); err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
prUsageLimits, err := s.getProjectUsageLimits(ctx, projectID)
|
2019-12-12 12:58:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
2021-06-24 16:49:15 +01:00
|
|
|
|
|
|
|
return &ProjectUsageLimits{
|
|
|
|
StorageLimit: prUsageLimits.StorageLimit,
|
|
|
|
BandwidthLimit: prUsageLimits.BandwidthLimit,
|
|
|
|
StorageUsed: prUsageLimits.StorageUsed,
|
|
|
|
BandwidthUsed: prUsageLimits.BandwidthUsed,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTotalUsageLimits returns total limits and current usage for all the projects.
|
|
|
|
func (s *Service) GetTotalUsageLimits(ctx context.Context) (_ *ProjectUsageLimits, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
auth, err := s.getAuthAndAuditLog(ctx, "get total usage and limits for all the projects")
|
2019-12-12 12:58:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
|
|
|
|
2021-06-24 16:49:15 +01:00
|
|
|
projects, err := s.store.Projects().GetOwn(ctx, auth.User.ID)
|
2019-12-12 12:58:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
2021-06-24 16:49:15 +01:00
|
|
|
|
|
|
|
var totalStorageLimit int64
|
|
|
|
var totalBandwidthLimit int64
|
|
|
|
var totalStorageUsed int64
|
|
|
|
var totalBandwidthUsed int64
|
|
|
|
|
|
|
|
for _, pr := range projects {
|
|
|
|
prUsageLimits, err := s.getProjectUsageLimits(ctx, pr.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
totalStorageLimit += prUsageLimits.StorageLimit
|
|
|
|
totalBandwidthLimit += prUsageLimits.BandwidthLimit
|
|
|
|
totalStorageUsed += prUsageLimits.StorageUsed
|
|
|
|
totalBandwidthUsed += prUsageLimits.BandwidthUsed
|
|
|
|
}
|
|
|
|
|
|
|
|
return &ProjectUsageLimits{
|
|
|
|
StorageLimit: totalStorageLimit,
|
|
|
|
BandwidthLimit: totalBandwidthLimit,
|
|
|
|
StorageUsed: totalStorageUsed,
|
|
|
|
BandwidthUsed: totalBandwidthUsed,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) getProjectUsageLimits(ctx context.Context, projectID uuid.UUID) (_ *ProjectUsageLimits, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
storageLimit, err := s.projectUsage.GetProjectStorageLimit(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
bandwidthLimit, err := s.projectUsage.GetProjectBandwidthLimit(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-12-12 12:58:15 +00:00
|
|
|
bandwidthUsed, err := s.projectUsage.GetProjectBandwidthTotals(ctx, projectID)
|
|
|
|
if err != nil {
|
2021-06-24 16:49:15 +01:00
|
|
|
return nil, err
|
2019-12-12 12:58:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &ProjectUsageLimits{
|
|
|
|
StorageLimit: storageLimit.Int64(),
|
|
|
|
BandwidthLimit: bandwidthLimit.Int64(),
|
|
|
|
StorageUsed: storageUsed,
|
|
|
|
BandwidthUsed: bandwidthUsed,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// Authorize validates token from context and returns authorized Authorization.
|
2018-12-20 20:10:27 +00:00
|
|
|
func (s *Service) Authorize(ctx context.Context) (a Authorization, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-10-06 11:40:31 +01:00
|
|
|
tokenS, ok := consoleauth.GetAPIKey(ctx)
|
2018-11-21 15:51:43 +00:00
|
|
|
if !ok {
|
2018-12-18 17:43:02 +00:00
|
|
|
return Authorization{}, ErrUnauthorized.New("no api key was provided")
|
2018-11-21 15:51:43 +00:00
|
|
|
}
|
|
|
|
|
2019-01-15 13:03:24 +00:00
|
|
|
token, err := consoleauth.FromBase64URLString(string(tokenS))
|
2018-11-21 15:51:43 +00:00
|
|
|
if err != nil {
|
2018-12-18 17:43:02 +00:00
|
|
|
return Authorization{}, ErrUnauthorized.Wrap(err)
|
2018-11-21 15:51:43 +00:00
|
|
|
}
|
|
|
|
|
2019-06-04 12:55:38 +01:00
|
|
|
claims, err := s.authenticate(ctx, token)
|
2018-11-27 14:20:58 +00:00
|
|
|
if err != nil {
|
2018-12-18 17:43:02 +00:00
|
|
|
return Authorization{}, ErrUnauthorized.Wrap(err)
|
2018-11-27 14:20:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
user, err := s.authorize(ctx, claims)
|
|
|
|
if err != nil {
|
2020-01-20 13:02:44 +00:00
|
|
|
return Authorization{}, ErrUnauthorized.Wrap(err)
|
2018-11-27 14:20:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return Authorization{
|
|
|
|
User: *user,
|
|
|
|
Claims: *claims,
|
|
|
|
}, nil
|
2018-11-21 15:51:43 +00:00
|
|
|
}
|
|
|
|
|
2020-10-09 14:40:12 +01:00
|
|
|
// checkProjectCanBeDeleted ensures that all data, api-keys and buckets are deleted and usage has been accounted.
|
|
|
|
// no error means the project status is clean.
|
|
|
|
func (s *Service) checkProjectCanBeDeleted(ctx context.Context, project uuid.UUID) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
buckets, err := s.buckets.CountBuckets(ctx, project)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if buckets > 0 {
|
|
|
|
return ErrUsage.New("some buckets still exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
keys, err := s.store.APIKeys().GetPagedByProjectID(ctx, project, APIKeyCursor{Limit: 1, Page: 1})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if keys.TotalCount > 0 {
|
|
|
|
return ErrUsage.New("some api-keys still exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
outstanding, err := s.Payments().checkProjectInvoicingStatus(ctx, project)
|
|
|
|
if outstanding {
|
|
|
|
return ErrUsage.New("there is outstanding usage that is not charged yet")
|
|
|
|
}
|
|
|
|
return ErrUsage.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// checkProjectLimit is used to check if user is able to create a new project.
|
2021-04-08 18:34:23 +01:00
|
|
|
func (s *Service) checkProjectLimit(ctx context.Context, userID uuid.UUID) (currentProjects int, err error) {
|
2019-06-04 12:55:38 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2020-03-10 23:01:19 +00:00
|
|
|
|
2020-07-15 17:49:37 +01:00
|
|
|
limit, err := s.store.Users().GetProjectLimit(ctx, userID)
|
2019-03-19 17:55:43 +00:00
|
|
|
if err != nil {
|
2021-04-08 18:34:23 +01:00
|
|
|
return 0, Error.Wrap(err)
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
2020-07-15 17:49:37 +01:00
|
|
|
if limit == 0 {
|
|
|
|
limit = s.config.DefaultProjectLimit
|
|
|
|
}
|
2019-03-19 17:55:43 +00:00
|
|
|
|
|
|
|
projects, err := s.GetUsersProjects(ctx)
|
|
|
|
if err != nil {
|
2021-04-08 18:34:23 +01:00
|
|
|
return 0, Error.Wrap(err)
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
2020-03-10 23:01:19 +00:00
|
|
|
|
2020-07-15 17:49:37 +01:00
|
|
|
if len(projects) >= limit {
|
2021-04-08 18:34:23 +01:00
|
|
|
return 0, ErrProjLimit.New(projLimitErrMsg)
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
|
|
|
|
2021-04-08 18:34:23 +01:00
|
|
|
return len(projects), nil
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// CreateRegToken creates new registration token. Needed for testing.
|
2019-06-04 12:55:38 +01:00
|
|
|
func (s *Service) CreateRegToken(ctx context.Context, projLimit int) (_ *RegistrationToken, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-11-12 13:14:31 +00:00
|
|
|
result, err := s.store.RegistrationTokens().Create(ctx, projLimit)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result, nil
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// createToken creates string representation.
|
2019-06-04 12:55:38 +01:00
|
|
|
func (s *Service) createToken(ctx context.Context, claims *consoleauth.Claims) (_ string, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2018-11-22 10:38:58 +00:00
|
|
|
json, err := claims.JSON()
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return "", Error.Wrap(err)
|
2018-11-22 10:38:58 +00:00
|
|
|
}
|
|
|
|
|
2019-01-15 13:03:24 +00:00
|
|
|
token := consoleauth.Token{Payload: json}
|
2018-11-22 10:38:58 +00:00
|
|
|
err = signToken(&token, s.Signer)
|
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return "", Error.Wrap(err)
|
2018-11-22 10:38:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return token.String(), nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// authenticate validates token signature and returns authenticated *satelliteauth.Authorization.
|
2019-06-04 12:55:38 +01:00
|
|
|
func (s *Service) authenticate(ctx context.Context, token consoleauth.Token) (_ *consoleauth.Claims, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-11-14 10:50:15 +00:00
|
|
|
signature := token.Signature
|
|
|
|
|
2019-06-04 12:55:38 +01:00
|
|
|
err = signToken(&token, s.Signer)
|
2018-11-14 10:50:15 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if subtle.ConstantTimeCompare(signature, token.Signature) != 1 {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, Error.New("incorrect signature")
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2019-01-15 13:03:24 +00:00
|
|
|
claims, err := consoleauth.FromJSON(token.Payload)
|
2018-11-14 10:50:15 +00:00
|
|
|
if err != nil {
|
2019-11-12 13:14:31 +00:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return claims, nil
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// authorize checks claims and returns authorized User.
|
2019-06-04 12:55:38 +01:00
|
|
|
func (s *Service) authorize(ctx context.Context, claims *consoleauth.Claims) (_ *User, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-11-14 10:50:15 +00:00
|
|
|
if !claims.Expiration.IsZero() && claims.Expiration.Before(time.Now()) {
|
2020-02-21 11:47:53 +00:00
|
|
|
return nil, ErrTokenExpiration.New("")
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 15:51:43 +00:00
|
|
|
user, err := s.store.Users().Get(ctx, claims.ID)
|
2018-11-14 10:50:15 +00:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return nil, ErrValidation.New("authorization failed. no user with id: %s", claims.ID.String())
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2018-11-21 15:51:43 +00:00
|
|
|
return user, nil
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
2018-12-27 15:30:15 +00:00
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// isProjectMember is return type of isProjectMember service method.
|
2018-12-27 15:30:15 +00:00
|
|
|
type isProjectMember struct {
|
|
|
|
project *Project
|
|
|
|
membership *ProjectMember
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// isProjectOwner checks if the user is an owner of a project.
|
2020-10-06 15:25:53 +01:00
|
|
|
func (s *Service) isProjectOwner(ctx context.Context, userID uuid.UUID, projectID uuid.UUID) (isOwner bool, err error) {
|
2019-08-07 13:28:13 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
project, err := s.store.Projects().Get(ctx, projectID)
|
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return false, err
|
2019-08-07 13:28:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if project.OwnerID != userID {
|
2020-10-06 15:25:53 +01:00
|
|
|
return false, ErrUnauthorized.New(unauthorizedErrMsg)
|
2019-08-07 13:28:13 +01:00
|
|
|
}
|
|
|
|
|
2020-10-06 15:25:53 +01:00
|
|
|
return true, nil
|
2019-08-07 13:28:13 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// isProjectMember checks if the user is a member of given project.
|
2020-07-20 16:22:36 +01:00
|
|
|
func (s *Service) isProjectMember(ctx context.Context, userID uuid.UUID, projectID uuid.UUID) (_ isProjectMember, err error) {
|
2019-06-04 12:55:38 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-12-27 15:30:15 +00:00
|
|
|
project, err := s.store.Projects().Get(ctx, projectID)
|
|
|
|
if err != nil {
|
2020-07-20 16:22:36 +01:00
|
|
|
return isProjectMember{}, Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
memberships, err := s.store.ProjectMembers().GetByMemberID(ctx, userID)
|
|
|
|
if err != nil {
|
2020-07-20 16:22:36 +01:00
|
|
|
return isProjectMember{}, Error.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2020-07-20 16:22:36 +01:00
|
|
|
membership, ok := findMembershipByProjectID(memberships, projectID)
|
|
|
|
if ok {
|
|
|
|
return isProjectMember{
|
|
|
|
project: project,
|
|
|
|
membership: &membership,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return isProjectMember{}, ErrNoMembership.New(unauthorizedErrMsg)
|
|
|
|
}
|
|
|
|
|
|
|
|
func findMembershipByProjectID(memberships []ProjectMember, projectID uuid.UUID) (ProjectMember, bool) {
|
2018-12-27 15:30:15 +00:00
|
|
|
for _, membership := range memberships {
|
|
|
|
if membership.ProjectID == projectID {
|
2020-07-20 16:22:36 +01:00
|
|
|
return membership, true
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
2020-07-20 16:22:36 +01:00
|
|
|
return ProjectMember{}, false
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|