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"
|
2022-08-31 14:55:28 +01:00
|
|
|
"database/sql"
|
2023-01-06 21:40:03 +00:00
|
|
|
"errors"
|
2020-01-03 14:21:05 +00:00
|
|
|
"fmt"
|
2022-03-31 12:51:07 +01:00
|
|
|
"math"
|
2022-02-17 13:48:39 +00:00
|
|
|
"net/http"
|
2020-11-05 16:16:55 +00:00
|
|
|
"net/mail"
|
2019-11-12 11:14:34 +00:00
|
|
|
"sort"
|
2022-07-14 14:44:06 +01:00
|
|
|
"strings"
|
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
|
|
|
|
2022-09-06 13:43:09 +01:00
|
|
|
"storj.io/common/currency"
|
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"
|
2022-02-17 13:48:39 +00:00
|
|
|
"storj.io/storj/private/api"
|
2022-05-11 09:02:58 +01:00
|
|
|
"storj.io/storj/private/blockchain"
|
2022-07-14 14:44:06 +01:00
|
|
|
"storj.io/storj/private/post"
|
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"
|
2022-11-09 08:50:54 +00:00
|
|
|
"storj.io/storj/satellite/buckets"
|
2019-01-15 13:03:24 +00:00
|
|
|
"storj.io/storj/satellite/console/consoleauth"
|
2022-07-14 14:44:06 +01:00
|
|
|
"storj.io/storj/satellite/mailservice"
|
2019-10-17 15:42:18 +01:00
|
|
|
"storj.io/storj/satellite/payments"
|
2022-08-15 15:41:19 +01:00
|
|
|
"storj.io/storj/satellite/payments/billing"
|
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.
|
2021-07-23 21:53:19 +01:00
|
|
|
maxLimit = 50
|
|
|
|
|
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"
|
2021-11-18 18:55:37 +00:00
|
|
|
emailNotFoundErrMsg = "There are no users with the specified email"
|
2019-04-10 20:16:10 +01:00
|
|
|
passwordRecoveryTokenIsExpiredErrMsg = "Your password recovery link has expired, please request another one"
|
2021-11-18 18:55:37 +00:00
|
|
|
credentialsErrMsg = "Your login credentials are incorrect, please try again"
|
2022-12-15 12:52:28 +00:00
|
|
|
changePasswordErrMsg = "Your old password is incorrect, please try again"
|
2022-09-06 21:55:15 +01:00
|
|
|
passwordTooShortErrMsg = "Your password needs to be at least %d characters long"
|
|
|
|
passwordTooLongErrMsg = "Your password must be no longer than %d characters"
|
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`
|
2021-11-18 18:55:37 +00:00
|
|
|
activationTokenExpiredErrMsg = "This activation token has expired, please request another one"
|
2022-02-11 22:48:35 +00:00
|
|
|
usedRegTokenErrMsg = "This registration token has already been used"
|
|
|
|
projLimitErrMsg = "Sorry, project creation is limited for your account. Please contact support!"
|
2023-01-05 14:51:45 +00:00
|
|
|
projNameErrMsg = "The new project must have a name you haven't used before!"
|
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
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
// ErrUnauthorized is error class for authorization related errors.
|
|
|
|
ErrUnauthorized = errs.Class("unauthorized")
|
|
|
|
|
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
|
|
|
|
2022-06-21 12:39:44 +01:00
|
|
|
// ErrTokenInvalid is error type of tokens which are invalid.
|
|
|
|
ErrTokenInvalid = errs.Class("invalid token")
|
|
|
|
|
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
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
// ErrLoginCredentials occurs when provided invalid login credentials.
|
|
|
|
ErrLoginCredentials = errs.Class("login credentials")
|
|
|
|
|
2022-12-15 12:52:28 +00:00
|
|
|
// ErrChangePassword occurs when provided old password is incorrect.
|
|
|
|
ErrChangePassword = errs.Class("change password")
|
|
|
|
|
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
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
// ErrEmailNotFound occurs when no users have the specified email.
|
|
|
|
ErrEmailNotFound = errs.Class("email not found")
|
|
|
|
|
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
|
|
|
|
2022-08-31 14:55:28 +01:00
|
|
|
// ErrAPIKeyRequest is returned when there is an error parsing a request for api keys.
|
|
|
|
ErrAPIKeyRequest = errs.Class("api key request")
|
|
|
|
|
2021-07-15 22:06:23 +01:00
|
|
|
// ErrRegToken describes registration token errors.
|
|
|
|
ErrRegToken = errs.Class("registration token")
|
|
|
|
|
2022-05-06 21:56:18 +01:00
|
|
|
// ErrCaptcha describes captcha validation errors.
|
|
|
|
ErrCaptcha = errs.Class("captcha validation")
|
2021-07-23 21:53:19 +01:00
|
|
|
|
|
|
|
// ErrRecoveryToken describes account recovery token errors.
|
|
|
|
ErrRecoveryToken = errs.Class("recovery token")
|
2023-01-05 14:51:45 +00:00
|
|
|
|
|
|
|
// ErrProjName is error that occurs with reused project names.
|
|
|
|
ErrProjName = errs.Class("project name")
|
2023-02-02 22:11:09 +00:00
|
|
|
|
|
|
|
// ErrPurchaseDesc is error that occurs when something is wrong with Purchase description.
|
|
|
|
ErrPurchaseDesc = errs.Class("purchase description")
|
2023-03-22 20:23:44 +00:00
|
|
|
|
|
|
|
// ErrAlreadyHasPackage is error that occurs when a user tries to update package, but already has one.
|
|
|
|
ErrAlreadyHasPackage = errs.Class("user already has package")
|
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 {
|
2022-07-21 14:31:38 +01:00
|
|
|
log, auditLogger *zap.Logger
|
|
|
|
store DB
|
|
|
|
restKeys RESTKeys
|
|
|
|
projectAccounting accounting.ProjectAccounting
|
|
|
|
projectUsage *accounting.Service
|
2022-11-09 08:50:54 +00:00
|
|
|
buckets buckets.DB
|
2022-07-21 14:31:38 +01:00
|
|
|
accounts payments.Accounts
|
|
|
|
depositWallets payments.DepositWallets
|
2022-08-15 15:41:19 +01:00
|
|
|
billing billing.TransactionsDB
|
2022-07-21 14:31:38 +01:00
|
|
|
registrationCaptchaHandler CaptchaHandler
|
|
|
|
loginCaptchaHandler CaptchaHandler
|
|
|
|
analytics *analytics.Service
|
|
|
|
tokens *consoleauth.Service
|
2022-07-14 14:44:06 +01:00
|
|
|
mailService *mailservice.Service
|
|
|
|
|
|
|
|
satelliteAddress string
|
2019-02-05 17:31:53 +00:00
|
|
|
|
2020-03-11 15:36:55 +00:00
|
|
|
config Config
|
|
|
|
}
|
|
|
|
|
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 {
|
2022-03-31 12:51:07 +01:00
|
|
|
PasswordCost int `help:"password hashing cost (0=automatic)" testDefault:"4" default:"0"`
|
|
|
|
OpenRegistrationEnabled bool `help:"enable open registration" default:"false" testDefault:"true"`
|
|
|
|
DefaultProjectLimit int `help:"default project limits for users" default:"1" testDefault:"5"`
|
|
|
|
AsOfSystemTimeDuration time.Duration `help:"default duration for AS OF SYSTEM TIME" devDefault:"-5m" releaseDefault:"-5m" testDefault:"0"`
|
|
|
|
LoginAttemptsWithoutPenalty int `help:"number of times user can try to login without penalty" default:"3"`
|
|
|
|
FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"`
|
|
|
|
UsageLimits UsageLimitsConfig
|
2022-07-21 14:31:38 +01:00
|
|
|
Captcha CaptchaConfig
|
2022-07-19 10:26:18 +01:00
|
|
|
Session SessionConfig
|
2021-06-25 12:17:55 +01:00
|
|
|
}
|
|
|
|
|
2022-07-21 14:31:38 +01:00
|
|
|
// CaptchaConfig contains configurations for login/registration captcha system.
|
|
|
|
type CaptchaConfig struct {
|
2023-02-22 10:32:32 +00:00
|
|
|
Login MultiCaptchaConfig `json:"login"`
|
|
|
|
Registration MultiCaptchaConfig `json:"registration"`
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2022-07-21 14:31:38 +01:00
|
|
|
// MultiCaptchaConfig contains configurations for Recaptcha and Hcaptcha systems.
|
|
|
|
type MultiCaptchaConfig struct {
|
2023-02-22 10:32:32 +00:00
|
|
|
Recaptcha SingleCaptchaConfig `json:"recaptcha"`
|
|
|
|
Hcaptcha SingleCaptchaConfig `json:"hcaptcha"`
|
2022-07-21 14:31:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// SingleCaptchaConfig contains configurations abstract captcha system.
|
|
|
|
type SingleCaptchaConfig struct {
|
2023-02-22 10:32:32 +00:00
|
|
|
Enabled bool `help:"whether or not captcha is enabled" default:"false" json:"enabled"`
|
|
|
|
SiteKey string `help:"captcha site key" json:"siteKey"`
|
|
|
|
SecretKey string `help:"captcha secret key" json:"-"`
|
2022-05-06 21:56:18 +01:00
|
|
|
}
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
// SessionConfig contains configurations for session management.
|
|
|
|
type SessionConfig struct {
|
2022-11-16 07:22:03 +00:00
|
|
|
InactivityTimerEnabled bool `help:"indicates if session can be timed out due inactivity" default:"true"`
|
2022-07-19 10:26:18 +01:00
|
|
|
InactivityTimerDuration int `help:"inactivity timer delay in seconds" default:"600"`
|
|
|
|
InactivityTimerViewerEnabled bool `help:"indicates whether remaining session time is shown for debugging" default:"false"`
|
|
|
|
Duration time.Duration `help:"duration a session is valid for (superseded by inactivity timer delay if inactivity timer is enabled)" default:"168h"`
|
|
|
|
}
|
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
// Payments separates all payment related functionality.
|
|
|
|
type Payments struct {
|
2019-10-17 15:42:18 +01:00
|
|
|
service *Service
|
|
|
|
}
|
|
|
|
|
2019-11-15 14:27:44 +00:00
|
|
|
// NewService returns new instance of Service.
|
2023-01-27 21:07:32 +00:00
|
|
|
func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets buckets.DB, accounts payments.Accounts, depositWallets payments.DepositWallets, billing billing.TransactionsDB, analytics *analytics.Service, tokens *consoleauth.Service, mailService *mailservice.Service, satelliteAddress string, config Config) (*Service, error) {
|
2018-11-14 10:50:15 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-07-21 14:31:38 +01:00
|
|
|
// We have two separate captcha handlers for login and registration.
|
|
|
|
// We want to easily swap between captchas independently.
|
|
|
|
// For example, google recaptcha for login screen and hcaptcha for registration screen.
|
|
|
|
var registrationCaptchaHandler CaptchaHandler
|
|
|
|
if config.Captcha.Registration.Recaptcha.Enabled {
|
|
|
|
registrationCaptchaHandler = NewDefaultCaptcha(Recaptcha, config.Captcha.Registration.Recaptcha.SecretKey)
|
|
|
|
} else if config.Captcha.Registration.Hcaptcha.Enabled {
|
|
|
|
registrationCaptchaHandler = NewDefaultCaptcha(Hcaptcha, config.Captcha.Registration.Hcaptcha.SecretKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
var loginCaptchaHandler CaptchaHandler
|
|
|
|
if config.Captcha.Login.Recaptcha.Enabled {
|
|
|
|
loginCaptchaHandler = NewDefaultCaptcha(Recaptcha, config.Captcha.Login.Recaptcha.SecretKey)
|
|
|
|
} else if config.Captcha.Login.Hcaptcha.Enabled {
|
|
|
|
loginCaptchaHandler = NewDefaultCaptcha(Hcaptcha, config.Captcha.Login.Hcaptcha.SecretKey)
|
2022-05-06 21:56:18 +01:00
|
|
|
}
|
|
|
|
|
2019-03-02 15:22:20 +00:00
|
|
|
return &Service{
|
2022-07-21 14:31:38 +01:00
|
|
|
log: log,
|
|
|
|
auditLogger: log.Named("auditlog"),
|
|
|
|
store: store,
|
|
|
|
restKeys: restKeys,
|
|
|
|
projectAccounting: projectAccounting,
|
|
|
|
projectUsage: projectUsage,
|
|
|
|
buckets: buckets,
|
|
|
|
accounts: accounts,
|
|
|
|
depositWallets: depositWallets,
|
2022-08-15 15:41:19 +01:00
|
|
|
billing: billing,
|
2022-07-21 14:31:38 +01:00
|
|
|
registrationCaptchaHandler: registrationCaptchaHandler,
|
|
|
|
loginCaptchaHandler: loginCaptchaHandler,
|
|
|
|
analytics: analytics,
|
|
|
|
tokens: tokens,
|
2022-07-14 14:44:06 +01:00
|
|
|
mailService: mailService,
|
|
|
|
satelliteAddress: satelliteAddress,
|
2022-07-21 14:31:38 +01:00
|
|
|
config: config,
|
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...)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
func (s *Service) getUserAndAuditLog(ctx context.Context, operation string, extra ...zap.Field) (*User, error) {
|
|
|
|
user, err := GetUser(ctx)
|
2020-09-06 02:56:07 +01:00
|
|
|
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...)...)
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, err
|
2020-09-06 02:56:07 +01:00
|
|
|
}
|
2022-06-05 23:41:38 +01:00
|
|
|
s.auditLog(ctx, operation, &user.ID, user.Email, extra...)
|
|
|
|
return user, nil
|
2020-09-06 02:56:07 +01:00
|
|
|
}
|
|
|
|
|
2020-01-24 13:38:53 +00:00
|
|
|
// Payments separates all payment related functionality.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (s *Service) Payments() Payments {
|
|
|
|
return Payments{service: s}
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetupAccount creates payment account for authorized user.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) SetupAccount(ctx context.Context) (_ payments.CouponType, err error) {
|
2019-10-17 15:42:18 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "setup payment account")
|
2019-10-17 15:42:18 +01:00
|
|
|
if err != nil {
|
2021-10-26 14:30:19 +01:00
|
|
|
return payments.NoCoupon, Error.Wrap(err)
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return payment.service.accounts.Setup(ctx, user.ID, user.Email, user.SignupPromoCode)
|
2019-10-17 15:42:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// AccountBalance return account balance.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) AccountBalance(ctx context.Context) (balance payments.Balance, err error) {
|
2019-10-17 15:42:18 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2023-03-24 22:25:36 +00:00
|
|
|
return payment.service.accounts.Balances().Get(ctx, 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.
|
2023-02-06 20:21:33 +00:00
|
|
|
func (payment Payments) AddCreditCard(ctx context.Context, creditCardToken string) (card payments.CreditCard, err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx, creditCardToken)(&err)
|
2019-10-17 15:42:18 +01:00
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "add credit card")
|
2019-10-17 15:42:18 +01:00
|
|
|
if err != nil {
|
2023-02-06 20:21:33 +00:00
|
|
|
return payments.CreditCard{}, Error.Wrap(err)
|
2020-01-07 10:41:19 +00:00
|
|
|
}
|
|
|
|
|
2023-02-06 20:21:33 +00:00
|
|
|
card, err = payment.service.accounts.CreditCards().Add(ctx, user.ID, creditCardToken)
|
2020-01-29 00:57:15 +00:00
|
|
|
if err != nil {
|
2023-02-06 20:21:33 +00:00
|
|
|
return payments.CreditCard{}, Error.Wrap(err)
|
2020-01-29 00:57:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
payment.service.analytics.TrackCreditCardAdded(user.ID, user.Email)
|
2022-04-28 19:12:42 +01:00
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
if !user.PaidTier {
|
2021-07-01 00:13:45 +01:00
|
|
|
// put this user into the paid tier and convert projects to upgraded limits.
|
2022-06-05 23:41:38 +01:00
|
|
|
err = payment.service.store.Users().UpdatePaidTier(ctx, user.ID, true,
|
2022-04-28 03:54:56 +01:00
|
|
|
payment.service.config.UsageLimits.Bandwidth.Paid,
|
|
|
|
payment.service.config.UsageLimits.Storage.Paid,
|
|
|
|
payment.service.config.UsageLimits.Segment.Paid,
|
|
|
|
payment.service.config.UsageLimits.Project.Paid,
|
2021-12-06 19:06:50 +00:00
|
|
|
)
|
2021-07-01 00:13:45 +01:00
|
|
|
if err != nil {
|
2023-02-06 20:21:33 +00:00
|
|
|
return payments.CreditCard{}, Error.Wrap(err)
|
2021-07-01 00:13:45 +01:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
projects, err := payment.service.store.Projects().GetOwn(ctx, user.ID)
|
2021-07-01 00:13:45 +01:00
|
|
|
if err != nil {
|
2023-02-06 20:21:33 +00:00
|
|
|
return payments.CreditCard{}, Error.Wrap(err)
|
2021-07-01 00:13:45 +01:00
|
|
|
}
|
|
|
|
for _, project := range projects {
|
2022-04-28 03:54:56 +01:00
|
|
|
if project.StorageLimit == nil || *project.StorageLimit < payment.service.config.UsageLimits.Storage.Paid {
|
2021-07-01 00:13:45 +01:00
|
|
|
project.StorageLimit = new(memory.Size)
|
2022-04-28 03:54:56 +01:00
|
|
|
*project.StorageLimit = payment.service.config.UsageLimits.Storage.Paid
|
2021-07-01 00:13:45 +01:00
|
|
|
}
|
2022-04-28 03:54:56 +01:00
|
|
|
if project.BandwidthLimit == nil || *project.BandwidthLimit < payment.service.config.UsageLimits.Bandwidth.Paid {
|
2021-07-01 00:13:45 +01:00
|
|
|
project.BandwidthLimit = new(memory.Size)
|
2022-04-28 03:54:56 +01:00
|
|
|
*project.BandwidthLimit = payment.service.config.UsageLimits.Bandwidth.Paid
|
2021-07-01 00:13:45 +01:00
|
|
|
}
|
2022-04-28 03:54:56 +01:00
|
|
|
if project.SegmentLimit == nil || *project.SegmentLimit < payment.service.config.UsageLimits.Segment.Paid {
|
|
|
|
*project.SegmentLimit = payment.service.config.UsageLimits.Segment.Paid
|
2021-12-06 19:06:50 +00:00
|
|
|
}
|
2022-04-28 03:54:56 +01:00
|
|
|
err = payment.service.store.Projects().Update(ctx, &project)
|
2021-07-01 00:13:45 +01:00
|
|
|
if err != nil {
|
2023-02-06 20:21:33 +00:00
|
|
|
return payments.CreditCard{}, Error.Wrap(err)
|
2021-07-01 00:13:45 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-06 20:21:33 +00:00
|
|
|
return card, 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.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) MakeCreditCardDefault(ctx context.Context, cardID string) (err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx, cardID)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return payment.service.accounts.CreditCards().MakeDefault(ctx, 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.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) 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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return payment.service.accounts.ProjectCharges(ctx, 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.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) ListCreditCards(ctx context.Context) (_ []payments.CreditCard, err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return payment.service.accounts.CreditCards().List(ctx, user.ID)
|
2019-10-23 18:33:24 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveCreditCard is used to detach a credit card from payment account.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) RemoveCreditCard(ctx context.Context, cardID string) (err error) {
|
2019-10-23 18:33:24 +01:00
|
|
|
defer mon.Task()(&ctx, cardID)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return payment.service.accounts.CreditCards().Remove(ctx, 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.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) BillingHistory(ctx context.Context) (billingHistory []*BillingHistoryItem, err error) {
|
2019-10-31 16:56:54 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
invoices, couponUsages, err := payment.service.accounts.Invoices().ListWithDiscounts(ctx, 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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
txsInfos, err := payment.service.accounts.StorjTokens().ListTransactionInfos(ctx, 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,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
charges, err := payment.service.accounts.Charges(ctx, 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
|
|
|
}
|
|
|
|
|
2021-08-27 01:51:26 +01:00
|
|
|
for _, usage := range couponUsages {
|
|
|
|
desc := "Coupon"
|
|
|
|
if usage.Coupon.Name != "" {
|
|
|
|
desc = usage.Coupon.Name
|
2020-05-14 11:34:42 +01:00
|
|
|
}
|
2021-08-27 01:51:26 +01:00
|
|
|
if usage.Coupon.PromoCode != "" {
|
|
|
|
desc += " (" + usage.Coupon.PromoCode + ")"
|
2020-06-10 12:42:44 +01:00
|
|
|
}
|
|
|
|
|
2021-08-27 01:51:26 +01:00
|
|
|
billingHistory = append(billingHistory, &BillingHistoryItem{
|
|
|
|
Description: desc,
|
|
|
|
Amount: usage.Amount,
|
|
|
|
Start: usage.PeriodStart,
|
|
|
|
End: usage.PeriodEnd,
|
2021-03-30 00:37:46 +01:00
|
|
|
Type: Coupon,
|
2021-08-27 01:51:26 +01:00
|
|
|
})
|
2020-01-07 10:41:19 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
bonuses, err := payment.service.accounts.StorjTokens().ListDepositBonuses(ctx, user.ID)
|
2020-05-28 12:31:02 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-10-09 14:40:12 +01:00
|
|
|
// checkOutstandingInvoice returns if the payment account has any unpaid/outstanding invoices or/and invoice items.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) checkOutstandingInvoice(ctx context.Context) (err error) {
|
2020-10-09 14:40:12 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "get outstanding invoices")
|
2020-10-09 14:40:12 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
invoices, err := payment.service.accounts.Invoices().List(ctx, user.ID)
|
2020-10-09 14:40:12 +01:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
hasItems, err := payment.service.accounts.Invoices().CheckPendingItems(ctx, user.ID)
|
2020-10-09 14:40:12 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if hasItems {
|
|
|
|
return ErrUsage.New("user has pending invoice items")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-04-28 16:59:55 +01:00
|
|
|
// checkProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
|
2020-10-09 14:40:12 +01:00
|
|
|
// which have not been applied/invoiced yet (meaning sent over to stripe).
|
2022-04-28 16:59:55 +01:00
|
|
|
func (payment Payments) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (err error) {
|
2020-10-09 14:40:12 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = payment.service.getUserAndAuditLog(ctx, "project invoicing status")
|
2020-10-09 14:40:12 +01:00
|
|
|
if err != nil {
|
2022-04-28 16:59:55 +01:00
|
|
|
return Error.Wrap(err)
|
2020-10-09 14:40:12 +01:00
|
|
|
}
|
|
|
|
|
2022-04-28 03:54:56 +01:00
|
|
|
return payment.service.accounts.CheckProjectInvoicingStatus(ctx, projectID)
|
2020-10-09 14:40:12 +01:00
|
|
|
}
|
|
|
|
|
2022-04-28 16:59:55 +01:00
|
|
|
// checkProjectUsageStatus returns error if for the given project there is some usage for current or previous month.
|
|
|
|
func (payment Payments) checkProjectUsageStatus(ctx context.Context, projectID uuid.UUID) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = payment.service.getUserAndAuditLog(ctx, "project usage status")
|
2022-04-28 16:59:55 +01:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return payment.service.accounts.CheckProjectUsageStatus(ctx, projectID)
|
|
|
|
}
|
|
|
|
|
2023-01-25 21:38:29 +00:00
|
|
|
// ApplyCoupon applies a coupon to an account based on couponID.
|
|
|
|
func (payment Payments) ApplyCoupon(ctx context.Context, couponID string) (coupon *payments.Coupon, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "apply coupon")
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
coupon, err = payment.service.accounts.Coupons().ApplyCoupon(ctx, user.ID, couponID)
|
|
|
|
if err != nil {
|
|
|
|
return coupon, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
return coupon, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ApplyFreeTierCoupon applies the default free tier coupon to an account.
|
|
|
|
func (payment Payments) ApplyFreeTierCoupon(ctx context.Context) (coupon *payments.Coupon, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
user, err := GetUser(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
coupon, err = payment.service.accounts.Coupons().ApplyFreeTierCoupon(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return coupon, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return coupon, nil
|
|
|
|
}
|
|
|
|
|
2021-08-06 21:14:33 +01:00
|
|
|
// ApplyCouponCode applies a coupon code to a Stripe customer
|
|
|
|
// and returns the coupon corresponding to the code.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) ApplyCouponCode(ctx context.Context, couponCode string) (coupon *payments.Coupon, err error) {
|
2021-06-22 01:09:56 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "apply coupon code")
|
2021-06-22 01:09:56 +01:00
|
|
|
if err != nil {
|
2021-08-06 21:14:33 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2021-06-22 01:09:56 +01:00
|
|
|
}
|
2021-08-06 21:14:33 +01:00
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
coupon, err = payment.service.accounts.Coupons().ApplyCouponCode(ctx, user.ID, couponCode)
|
2021-08-06 21:14:33 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return coupon, nil
|
2021-06-22 01:09:56 +01:00
|
|
|
}
|
|
|
|
|
2021-08-06 21:14:33 +01:00
|
|
|
// GetCoupon returns the coupon applied to the user's account.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) GetCoupon(ctx context.Context) (coupon *payments.Coupon, err error) {
|
2021-07-08 20:06:07 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "get coupon")
|
2021-07-08 20:06:07 +01:00
|
|
|
if err != nil {
|
2021-08-06 21:14:33 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2021-07-08 20:06:07 +01:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
coupon, err = payment.service.accounts.Coupons().GetByUserID(ctx, user.ID)
|
2021-08-06 21:14:33 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
2020-01-29 00:57:15 +00:00
|
|
|
|
2021-08-06 21:14:33 +01:00
|
|
|
return coupon, nil
|
2020-01-18 02:34:06 +00:00
|
|
|
}
|
|
|
|
|
2022-12-21 20:27:29 +00:00
|
|
|
// AttemptPayOverdueInvoices attempts to pay a user's open, overdue invoices.
|
|
|
|
func (payment Payments) AttemptPayOverdueInvoices(ctx context.Context) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "attempt to pay overdue invoices")
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = payment.service.accounts.Invoices().AttemptPayOverdueInvoices(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2022-08-17 11:30:07 +01:00
|
|
|
var captchaScore *float64
|
|
|
|
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("create_user_attempt").Inc(1) //mon:locked
|
|
|
|
|
2022-07-21 14:31:38 +01:00
|
|
|
if s.config.Captcha.Registration.Recaptcha.Enabled || s.config.Captcha.Registration.Hcaptcha.Enabled {
|
2022-08-17 11:30:07 +01:00
|
|
|
valid, score, err := s.registrationCaptchaHandler.Verify(ctx, user.CaptchaResponse, user.IP)
|
2021-06-25 12:17:55 +01:00
|
|
|
if err != nil {
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("create_user_captcha_error").Inc(1) //mon:locked
|
2022-05-06 21:56:18 +01:00
|
|
|
s.log.Error("captcha authorization failed", zap.Error(err))
|
|
|
|
return nil, ErrCaptcha.Wrap(err)
|
2021-06-25 12:17:55 +01:00
|
|
|
}
|
|
|
|
if !valid {
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("create_user_captcha_unsuccessful").Inc(1) //mon:locked
|
2022-05-06 21:56:18 +01:00
|
|
|
return nil, ErrCaptcha.New("captcha validation unsuccessful")
|
2021-06-25 12:17:55 +01:00
|
|
|
}
|
2022-08-17 11:30:07 +01:00
|
|
|
captchaScore = score
|
2021-06-25 12:17:55 +01:00
|
|
|
}
|
|
|
|
|
2018-12-10 15:57:06 +00:00
|
|
|
if err := user.IsValid(); err != nil {
|
2022-02-23 16:02:45 +00:00
|
|
|
// NOTE: error is already wrapped with an appropriated class.
|
|
|
|
return nil, 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
|
|
|
}
|
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
verified, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, user.Email)
|
|
|
|
if err != nil {
|
2020-03-11 15:36:55 +00:00
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
2022-05-24 19:18:52 +01:00
|
|
|
if verified != nil {
|
|
|
|
mon.Counter("create_user_duplicate_verified").Inc(1) //mon:locked
|
|
|
|
return nil, ErrEmailUsed.New(emailUsedErrMsg)
|
|
|
|
} else if len(unverified) != 0 {
|
|
|
|
mon.Counter("create_user_duplicate_unverified").Inc(1) //mon:locked
|
2021-11-18 18:55:37 +00:00
|
|
|
return nil, ErrEmailUsed.New(emailUsedErrMsg)
|
|
|
|
}
|
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,
|
2021-10-26 14:30:19 +01:00
|
|
|
SignupPromoCode: user.SignupPromoCode,
|
2022-08-17 11:30:07 +01:00
|
|
|
SignupCaptcha: captchaScore,
|
2019-07-17 21:53:14 +01:00
|
|
|
}
|
2021-04-27 19:40:03 +01:00
|
|
|
|
2021-09-23 00:38:18 +01:00
|
|
|
if user.UserAgent != nil {
|
|
|
|
newUser.UserAgent = user.UserAgent
|
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 {
|
2022-02-28 21:37:46 +00:00
|
|
|
newUser.ProjectLimit = s.config.UsageLimits.Project.Free
|
2020-07-15 17:49:37 +01:00
|
|
|
}
|
|
|
|
|
2021-11-01 15:27:32 +00:00
|
|
|
// TODO: move the project limits into the registration token.
|
|
|
|
newUser.ProjectStorageLimit = s.config.UsageLimits.Storage.Free.Int64()
|
|
|
|
newUser.ProjectBandwidthLimit = s.config.UsageLimits.Bandwidth.Free.Int64()
|
2021-12-06 19:06:50 +00:00
|
|
|
newUser.ProjectSegmentLimit = s.config.UsageLimits.Segment.Free
|
2021-11-01 15:27:32 +00: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)
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("create_user_success").Inc(1) //mon:locked
|
2020-09-06 02:56:07 +01:00
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
return u, nil
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2022-05-06 21:56:18 +01:00
|
|
|
// TestSwapCaptchaHandler replaces the existing handler for captchas with
|
2021-06-25 12:17:55 +01:00
|
|
|
// the one specified for use in testing.
|
2022-05-06 21:56:18 +01:00
|
|
|
func (s *Service) TestSwapCaptchaHandler(h CaptchaHandler) {
|
2022-07-21 14:31:38 +01:00
|
|
|
s.registrationCaptchaHandler = h
|
|
|
|
s.loginCaptchaHandler = h
|
2021-06-25 12:17:55 +01:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
2022-04-19 21:50:15 +01:00
|
|
|
return s.tokens.CreateToken(ctx, id, email)
|
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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
// GenerateSessionToken creates a new session and returns the string representation of its token.
|
2022-07-19 10:26:18 +01:00
|
|
|
func (s *Service) GenerateSessionToken(ctx context.Context, userID uuid.UUID, email, ip, userAgent string) (_ *TokenInfo, err error) {
|
2022-08-16 12:20:18 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
sessionID, err := uuid.New()
|
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
duration := s.config.Session.Duration
|
|
|
|
if s.config.Session.InactivityTimerEnabled {
|
2023-03-07 16:40:49 +00:00
|
|
|
settings, err := s.store.Users().GetSettings(ctx, userID)
|
|
|
|
if err != nil && !errs.Is(err, sql.ErrNoRows) {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
if settings != nil && settings.SessionDuration != nil {
|
|
|
|
duration = *settings.SessionDuration
|
|
|
|
} else {
|
|
|
|
duration = time.Duration(s.config.Session.InactivityTimerDuration) * time.Second
|
|
|
|
}
|
2022-07-19 10:26:18 +01:00
|
|
|
}
|
|
|
|
expiresAt := time.Now().Add(duration)
|
|
|
|
|
|
|
|
_, err = s.store.WebappSessions().Create(ctx, sessionID, userID, ip, userAgent, expiresAt)
|
2022-06-05 23:41:38 +01:00
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
token := consoleauth.Token{Payload: sessionID.Bytes()}
|
|
|
|
|
|
|
|
signature, err := s.tokens.SignToken(token)
|
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
|
|
|
token.Signature = signature
|
|
|
|
|
|
|
|
s.auditLog(ctx, "login", &userID, email)
|
|
|
|
|
|
|
|
s.analytics.TrackSignedIn(userID, email)
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
return &TokenInfo{
|
|
|
|
Token: token,
|
|
|
|
ExpiresAt: expiresAt,
|
|
|
|
}, nil
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// ActivateAccount - is a method for activating user account after registration.
|
2022-06-05 23:41:38 +01:00
|
|
|
func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (user *User, err error) {
|
2019-01-30 15:04:40 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2021-10-06 14:33:54 +01:00
|
|
|
parsedActivationToken, err := consoleauth.FromBase64URLString(activationToken)
|
2019-01-30 15:04:40 +00:00
|
|
|
if err != nil {
|
2022-06-21 12:39:44 +01:00
|
|
|
return nil, ErrTokenInvalid.Wrap(err)
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
valid, err := s.tokens.ValidateToken(parsedActivationToken)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
if !valid {
|
2022-06-21 12:39:44 +01:00
|
|
|
return nil, ErrTokenInvalid.New("incorrect signature")
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
claims, err := consoleauth.FromJSON(parsedActivationToken.Payload)
|
2019-01-30 15:04:40 +00:00
|
|
|
if err != nil {
|
2022-06-21 12:39:44 +01:00
|
|
|
return nil, ErrTokenInvalid.New("JSON decoder: %w", err)
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
if time.Now().After(claims.Expiration) {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, ErrTokenExpiration.New(activationTokenExpiredErrMsg)
|
2021-11-18 18:55:37 +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 {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, ErrEmailUsed.New(emailUsedErrMsg)
|
2019-04-05 16:08:14 +01:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err = s.store.Users().Get(ctx, claims.ID)
|
2019-01-30 15:04:40 +00:00
|
|
|
if err != nil {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2019-01-30 15:04:40 +00:00
|
|
|
}
|
|
|
|
|
2022-06-01 22:15:37 +01:00
|
|
|
status := Active
|
|
|
|
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
|
|
|
Status: &status,
|
|
|
|
})
|
2019-04-10 01:15:12 +01:00
|
|
|
if err != nil {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, 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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return user, 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.
|
2022-01-12 01:42:38 +00:00
|
|
|
func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, password string, passcode string, recoveryCode string, t time.Time) (err error) {
|
2019-04-10 20:16:10 +01:00
|
|
|
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 {
|
2021-07-23 21:53:19 +01:00
|
|
|
return ErrRecoveryToken.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 {
|
2021-07-23 21:53:19 +01:00
|
|
|
return ErrRecoveryToken.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
|
|
|
}
|
|
|
|
|
2022-01-12 01:42:38 +00:00
|
|
|
if user.MFAEnabled {
|
|
|
|
if recoveryCode != "" {
|
|
|
|
found := false
|
|
|
|
for _, code := range user.MFARecoveryCodes {
|
|
|
|
if code == recoveryCode {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
return ErrUnauthorized.Wrap(ErrMFARecoveryCode.New(mfaRecoveryInvalidErrMsg))
|
|
|
|
}
|
|
|
|
} else if passcode != "" {
|
|
|
|
valid, err := ValidateMFAPasscode(passcode, user.MFASecretKey, t)
|
|
|
|
if err != nil {
|
|
|
|
return ErrValidation.Wrap(ErrMFAPasscode.Wrap(err))
|
|
|
|
}
|
|
|
|
if !valid {
|
|
|
|
return ErrValidation.Wrap(ErrMFAPasscode.New(mfaPasscodeInvalidErrMsg))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return ErrMFAMissing.New(mfaRequiredErrMsg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 13:48:11 +00:00
|
|
|
if err := ValidateNewPassword(password); err != nil {
|
2022-02-23 16:02:45 +00:00
|
|
|
return ErrValidation.Wrap(err)
|
2019-04-10 20:16:10 +01:00
|
|
|
}
|
|
|
|
|
2022-04-19 21:50:15 +01:00
|
|
|
if s.tokens.IsExpired(t, token.CreatedAt) {
|
2021-07-23 21:53:19 +01:00
|
|
|
return ErrRecoveryToken.Wrap(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
|
|
|
}
|
|
|
|
|
2022-06-01 22:15:37 +01:00
|
|
|
updateRequest := UpdateUserRequest{
|
|
|
|
PasswordHash: hash,
|
|
|
|
}
|
|
|
|
|
2022-03-31 12:51:07 +01:00
|
|
|
if user.FailedLoginCount != 0 {
|
2022-06-01 22:15:37 +01:00
|
|
|
resetFailedLoginCount := 0
|
|
|
|
resetLoginLockoutExpirationPtr := &time.Time{}
|
|
|
|
updateRequest.FailedLoginCount = &resetFailedLoginCount
|
|
|
|
updateRequest.LoginLockoutExpiration = &resetLoginLockoutExpirationPtr
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
2019-05-13 16:53:52 +01:00
|
|
|
|
2022-06-01 22:15:37 +01:00
|
|
|
err = s.store.Users().Update(ctx, user.ID, updateRequest)
|
2019-05-13 16:53:52 +01:00
|
|
|
if err != nil {
|
2020-10-06 15:25:53 +01:00
|
|
|
return Error.Wrap(err)
|
2019-05-13 16:53:52 +01:00
|
|
|
}
|
2022-06-01 22:15:37 +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
|
|
|
}
|
|
|
|
|
2022-07-22 18:25:54 +01:00
|
|
|
_, err = s.store.WebappSessions().DeleteAllByUserID(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
// Token authenticates User by credentials and returns session token.
|
2022-07-19 10:26:18 +01:00
|
|
|
func (s *Service) Token(ctx context.Context, request AuthUser) (response *TokenInfo, err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2019-01-08 13:54:12 +00:00
|
|
|
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_attempt").Inc(1) //mon:locked
|
|
|
|
|
2022-07-21 14:31:38 +01:00
|
|
|
if s.config.Captcha.Login.Recaptcha.Enabled || s.config.Captcha.Login.Hcaptcha.Enabled {
|
2022-08-17 11:30:07 +01:00
|
|
|
valid, _, err := s.loginCaptchaHandler.Verify(ctx, request.CaptchaResponse, request.IP)
|
2022-07-21 14:31:38 +01:00
|
|
|
if err != nil {
|
|
|
|
mon.Counter("login_user_captcha_error").Inc(1) //mon:locked
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrCaptcha.Wrap(err)
|
2022-07-21 14:31:38 +01:00
|
|
|
}
|
|
|
|
if !valid {
|
|
|
|
mon.Counter("login_user_captcha_unsuccessful").Inc(1) //mon:locked
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrCaptcha.New("captcha validation unsuccessful")
|
2022-07-21 14:31:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:18:52 +01:00
|
|
|
user, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, request.Email)
|
2021-11-18 18:55:37 +00:00
|
|
|
if user == nil {
|
2022-05-24 19:18:52 +01:00
|
|
|
if len(unverified) > 0 {
|
|
|
|
mon.Counter("login_email_unverified").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed email unverified", nil, request.Email)
|
2022-05-24 19:18:52 +01:00
|
|
|
} else {
|
|
|
|
mon.Counter("login_email_invalid").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed invalid email", nil, request.Email)
|
2022-05-24 19:18:52 +01:00
|
|
|
}
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrLoginCredentials.New(credentialsErrMsg)
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 12:51:07 +01:00
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
if user.LoginLockoutExpiration.After(now) {
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_locked_out").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed account locked out", &user.ID, request.Email)
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrLoginCredentials.New(credentialsErrMsg)
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
handleLockAccount := func() error {
|
2022-07-14 14:44:06 +01:00
|
|
|
err = s.UpdateUsersFailedLoginState(ctx, user)
|
2022-03-31 12:51:07 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_failed").Inc(1) //mon:locked
|
|
|
|
mon.IntVal("login_user_failed_count").Observe(int64(user.FailedLoginCount)) //mon:locked
|
|
|
|
|
2022-03-31 12:51:07 +01:00
|
|
|
if user.FailedLoginCount == s.config.LoginAttemptsWithoutPenalty {
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_lockout_initiated").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed login count reached maximum attempts", &user.ID, request.Email)
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if user.FailedLoginCount > s.config.LoginAttemptsWithoutPenalty {
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_lockout_reinitiated").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed locked account", &user.ID, request.Email)
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
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 {
|
2022-03-31 12:51:07 +01:00
|
|
|
err = handleLockAccount()
|
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_invalid_password").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed password invalid", &user.ID, user.Email)
|
2022-11-30 04:54:07 +00:00
|
|
|
return nil, ErrLoginCredentials.New(credentialsErrMsg)
|
2018-12-10 13:47:48 +00:00
|
|
|
}
|
|
|
|
|
2021-07-13 18:21:16 +01:00
|
|
|
if user.MFAEnabled {
|
2021-08-16 22:23:06 +01:00
|
|
|
if request.MFARecoveryCode != "" && request.MFAPasscode != "" {
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_mfa_conflict").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed mfa conflict", &user.ID, user.Email)
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrMFAConflict.New(mfaConflictErrMsg)
|
2021-08-16 22:23:06 +01:00
|
|
|
}
|
|
|
|
|
2021-07-13 18:21:16 +01:00
|
|
|
if request.MFARecoveryCode != "" {
|
|
|
|
found := false
|
|
|
|
codeIndex := -1
|
|
|
|
for i, code := range user.MFARecoveryCodes {
|
|
|
|
if code == request.MFARecoveryCode {
|
|
|
|
found = true
|
|
|
|
codeIndex = i
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
2022-03-31 12:51:07 +01:00
|
|
|
err = handleLockAccount()
|
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_mfa_recovery_failure").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed mfa recovery", &user.ID, user.Email)
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrMFARecoveryCode.New(mfaRecoveryInvalidErrMsg)
|
2021-07-13 18:21:16 +01:00
|
|
|
}
|
|
|
|
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_mfa_recovery_success").Inc(1) //mon:locked
|
|
|
|
|
2021-07-13 18:21:16 +01:00
|
|
|
user.MFARecoveryCodes = append(user.MFARecoveryCodes[:codeIndex], user.MFARecoveryCodes[codeIndex+1:]...)
|
|
|
|
|
2022-06-01 22:15:37 +01:00
|
|
|
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
|
|
|
MFARecoveryCodes: &user.MFARecoveryCodes,
|
|
|
|
})
|
2021-07-13 18:21:16 +01:00
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2021-07-13 18:21:16 +01:00
|
|
|
}
|
|
|
|
} else if request.MFAPasscode != "" {
|
2022-07-14 14:44:06 +01:00
|
|
|
valid, err := ValidateMFAPasscode(request.MFAPasscode, user.MFASecretKey, now)
|
2021-07-13 18:21:16 +01:00
|
|
|
if err != nil {
|
2022-03-31 12:51:07 +01:00
|
|
|
err = handleLockAccount()
|
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrMFAPasscode.Wrap(err)
|
2021-07-13 18:21:16 +01:00
|
|
|
}
|
|
|
|
if !valid {
|
2022-03-31 12:51:07 +01:00
|
|
|
err = handleLockAccount()
|
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_mfa_passcode_failure").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed mfa passcode invalid", &user.ID, user.Email)
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrMFAPasscode.New(mfaPasscodeInvalidErrMsg)
|
2021-07-13 18:21:16 +01:00
|
|
|
}
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_mfa_passcode_success").Inc(1) //mon:locked
|
2021-07-13 18:21:16 +01:00
|
|
|
} else {
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_mfa_missing").Inc(1) //mon:locked
|
2022-07-27 14:05:10 +01:00
|
|
|
s.auditLog(ctx, "login: failed mfa missing", &user.ID, user.Email)
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, ErrMFAMissing.New(mfaRequiredErrMsg)
|
2021-07-13 18:21:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-31 12:51:07 +01:00
|
|
|
if user.FailedLoginCount != 0 {
|
|
|
|
user.FailedLoginCount = 0
|
2022-06-01 22:15:37 +01:00
|
|
|
loginLockoutExpirationPtr := &time.Time{}
|
|
|
|
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
|
|
|
FailedLoginCount: &user.FailedLoginCount,
|
|
|
|
LoginLockoutExpiration: &loginLockoutExpirationPtr,
|
|
|
|
})
|
2022-03-31 12:51:07 +01:00
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
response, err = s.GenerateSessionToken(ctx, user.ID, user.Email, request.IP, request.UserAgent)
|
2018-11-14 10:50:15 +00:00
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return nil, err
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
2021-04-08 18:34:23 +01:00
|
|
|
|
2022-05-24 19:18:52 +01:00
|
|
|
mon.Counter("login_success").Inc(1) //mon:locked
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
return response, nil
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 12:51:07 +01:00
|
|
|
// UpdateUsersFailedLoginState updates User's failed login state.
|
2022-08-16 12:20:18 +01:00
|
|
|
func (s *Service) UpdateUsersFailedLoginState(ctx context.Context, user *User) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-09-23 23:23:32 +01:00
|
|
|
var failedLoginPenalty *float64
|
2022-03-31 12:51:07 +01:00
|
|
|
if user.FailedLoginCount >= s.config.LoginAttemptsWithoutPenalty-1 {
|
2022-07-14 14:44:06 +01:00
|
|
|
lockoutDuration := time.Duration(math.Pow(s.config.FailedLoginPenalty, float64(user.FailedLoginCount-1))) * time.Minute
|
2022-09-23 23:23:32 +01:00
|
|
|
failedLoginPenalty = &s.config.FailedLoginPenalty
|
2022-07-14 14:44:06 +01:00
|
|
|
|
|
|
|
address := s.satelliteAddress
|
|
|
|
if !strings.HasSuffix(address, "/") {
|
|
|
|
address += "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
s.mailService.SendRenderedAsync(
|
|
|
|
ctx,
|
|
|
|
[]post.Address{{Address: user.Email, Name: user.FullName}},
|
|
|
|
&LockAccountEmail{
|
|
|
|
Name: user.FullName,
|
|
|
|
LockoutDuration: lockoutDuration,
|
|
|
|
ResetPasswordLink: address + "forgot-password",
|
|
|
|
},
|
|
|
|
)
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
|
|
|
|
2022-09-23 23:23:32 +01:00
|
|
|
return s.store.Users().UpdateFailedLoginCountAndExpiration(ctx, failedLoginPenalty, user.ID)
|
2022-03-31 12:51:07 +01:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2022-05-27 15:31:28 +01:00
|
|
|
// GenGetUser returns ResponseUser by request context for generated api.
|
|
|
|
func (s *Service) GenGetUser(ctx context.Context) (*ResponseUser, api.HTTPError) {
|
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get user")
|
2022-05-27 15:31:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
respUser := &ResponseUser{
|
|
|
|
ID: user.ID,
|
|
|
|
FullName: user.FullName,
|
|
|
|
ShortName: user.ShortName,
|
|
|
|
Email: user.Email,
|
|
|
|
UserAgent: user.UserAgent,
|
|
|
|
ProjectLimit: user.ProjectLimit,
|
|
|
|
IsProfessional: user.IsProfessional,
|
|
|
|
Position: user.Position,
|
|
|
|
CompanyName: user.CompanyName,
|
|
|
|
EmployeeCount: user.EmployeeCount,
|
|
|
|
HaveSalesContact: user.HaveSalesContact,
|
|
|
|
PaidTier: user.PaidTier,
|
|
|
|
MFAEnabled: user.MFAEnabled,
|
|
|
|
MFARecoveryCodeCount: len(user.MFARecoveryCodes),
|
2022-05-27 15:31:28 +01:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return respUser, api.HTTPError{}
|
2022-05-27 15:31:28 +01: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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get user ID")
|
2021-03-31 19:34:44 +01:00
|
|
|
if err != nil {
|
|
|
|
return uuid.UUID{}, Error.Wrap(err)
|
|
|
|
}
|
2022-06-05 23:41:38 +01:00
|
|
|
return user.ID, nil
|
2021-03-31 19:34:44 +01:00
|
|
|
}
|
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
// GetUserByEmailWithUnverified returns Users by email.
|
|
|
|
func (s *Service) GetUserByEmailWithUnverified(ctx context.Context, email string) (verified *User, unverified []User, err error) {
|
2019-04-10 20:16:10 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
verified, unverified, err = s.store.Users().GetByEmailWithUnverified(ctx, email)
|
2019-11-12 13:14:31 +00:00
|
|
|
if err != nil {
|
2021-11-18 18:55:37 +00:00
|
|
|
return verified, unverified, err
|
2019-11-12 13:14:31 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
if verified == nil && len(unverified) == 0 {
|
|
|
|
err = ErrEmailNotFound.New(emailNotFoundErrMsg)
|
|
|
|
}
|
|
|
|
|
|
|
|
return verified, unverified, err
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user.FullName = fullName
|
|
|
|
user.ShortName = shortName
|
2022-06-01 22:15:37 +01:00
|
|
|
shortNamePtr := &user.ShortName
|
|
|
|
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
|
|
|
FullName: &user.FullName,
|
|
|
|
ShortName: &shortNamePtr,
|
|
|
|
})
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "change email")
|
2020-11-05 16:16:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := mail.ParseAddress(newEmail); err != nil {
|
|
|
|
return ErrValidation.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2021-11-18 18:55:37 +00:00
|
|
|
verified, unverified, err := s.store.Users().GetByEmailWithUnverified(ctx, newEmail)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
if verified != nil || len(unverified) != 0 {
|
2020-11-05 16:16:55 +00:00
|
|
|
return ErrEmailUsed.New(emailUsedErrMsg)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user.Email = newEmail
|
2022-06-01 22:15:37 +01:00
|
|
|
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
|
|
|
Email: &user.Email,
|
|
|
|
})
|
2020-11-05 16:16:55 +00:00
|
|
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
err = bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(pass))
|
2018-12-10 15:57:06 +00:00
|
|
|
if err != nil {
|
2022-12-15 12:52:28 +00:00
|
|
|
return ErrChangePassword.New(changePasswordErrMsg)
|
2018-12-10 15:57:06 +00:00
|
|
|
}
|
|
|
|
|
2023-02-03 13:48:11 +00:00
|
|
|
if err := ValidateNewPassword(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user.PasswordHash = hash
|
2022-06-01 22:15:37 +01:00
|
|
|
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
|
|
|
PasswordHash: hash,
|
|
|
|
})
|
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
|
|
|
}
|
|
|
|
|
2023-03-01 19:52:52 +00:00
|
|
|
resetPasswordToken, err := s.store.ResetPasswordTokens().GetByOwnerID(ctx, user.ID)
|
|
|
|
if err == nil {
|
|
|
|
err := s.store.ResetPasswordTokens().Delete(ctx, resetPasswordToken.Secret)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-22 18:25:54 +01:00
|
|
|
_, err = s.store.WebappSessions().DeleteAllByUserID(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
err = bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password))
|
2018-12-14 16:14:17 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
err = s.store.Users().Delete(ctx, user.ID)
|
2019-04-10 01:15:12 +01:00
|
|
|
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.
|
2023-01-19 14:54:17 +00:00
|
|
|
// projectID here may be project.PublicID or project.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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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)
|
|
|
|
}
|
|
|
|
|
2023-01-19 14:54:17 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
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
|
|
|
}
|
|
|
|
|
2023-01-19 14:54:17 +00:00
|
|
|
p = isMember.project
|
|
|
|
|
2019-04-10 01:15:12 +01:00
|
|
|
return
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2022-09-13 13:59:15 +01:00
|
|
|
// GetSalt is a method for querying project salt by id.
|
2023-01-05 11:14:55 +00:00
|
|
|
// id may be project.ID or project.PublicID.
|
2022-09-13 13:59:15 +01:00
|
|
|
func (s *Service) GetSalt(ctx context.Context, projectID uuid.UUID) (salt []byte, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get project salt", zap.String("projectID", projectID.String()))
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-05 11:14:55 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
|
|
|
if err != nil {
|
2022-09-13 13:59:15 +01:00
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-05 11:14:55 +00:00
|
|
|
return s.store.Projects().GetSalt(ctx, isMember.project.ID)
|
2022-09-13 13:59:15 +01: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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
ps, err = s.store.Projects().GetByUserID(ctx, user.ID)
|
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-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2022-02-17 13:48:39 +00:00
|
|
|
// GenGetUsersProjects is a method for querying all projects for generated api.
|
|
|
|
func (s *Service) GenGetUsersProjects(ctx context.Context) (ps []Project, httpErr api.HTTPError) {
|
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get users projects")
|
2022-02-17 13:48:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
ps, err = s.store.Projects().GetByUserID(ctx, user.ID)
|
2022-02-17 13:48:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get user's owned projects page")
|
2021-01-21 18:19:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return ProjectsPage{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
projects, err := s.store.Projects().ListByOwnerID(ctx, user.ID, cursor)
|
2021-01-21 18:19:37 +00:00
|
|
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
currentProjectCount, err := s.checkProjectLimit(ctx, user.ID)
|
2019-03-19 17:55:43 +00:00
|
|
|
if err != nil {
|
2022-06-17 20:57:10 +01:00
|
|
|
s.analytics.TrackProjectLimitError(user.ID, user.Email)
|
2019-12-09 13:20:44 +00:00
|
|
|
return nil, ErrProjLimit.Wrap(err)
|
2019-03-19 17:55:43 +00:00
|
|
|
}
|
|
|
|
|
2023-01-05 14:51:45 +00:00
|
|
|
passesNameCheck, err := s.checkProjectName(ctx, projectInfo, user.ID)
|
|
|
|
if err != nil || !passesNameCheck {
|
|
|
|
return nil, ErrProjName.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
newProjectLimits, err := s.getUserProjectLimits(ctx, user.ID)
|
2021-11-01 15:27:32 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, ErrProjLimit.Wrap(err)
|
|
|
|
}
|
|
|
|
|
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 {
|
2022-12-15 03:56:11 +00:00
|
|
|
storageLimit := memory.Size(newProjectLimits.Storage)
|
|
|
|
bandwidthLimit := memory.Size(newProjectLimits.Bandwidth)
|
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,
|
2022-06-05 23:41:38 +01:00
|
|
|
OwnerID: user.ID,
|
|
|
|
UserAgent: user.UserAgent,
|
2022-12-15 03:56:11 +00:00
|
|
|
StorageLimit: &storageLimit,
|
|
|
|
BandwidthLimit: &bandwidthLimit,
|
|
|
|
SegmentLimit: &newProjectLimits.Segment,
|
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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = tx.ProjectMembers().Insert(ctx, 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
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
s.analytics.TrackProjectCreated(user.ID, user.Email, projectID, currentProjectCount+1)
|
2021-04-08 18:34:23 +01:00
|
|
|
|
2019-06-03 14:46:57 +01:00
|
|
|
return p, nil
|
2018-11-26 10:47:23 +00:00
|
|
|
}
|
|
|
|
|
2022-03-21 12:15:33 +00:00
|
|
|
// GenCreateProject is a method for creating new project for generated api.
|
|
|
|
func (s *Service) GenCreateProject(ctx context.Context, projectInfo ProjectInfo) (p *Project, httpError api.HTTPError) {
|
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "create project")
|
2022-03-21 12:15:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
currentProjectCount, err := s.checkProjectLimit(ctx, user.ID)
|
2022-03-21 12:15:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: ErrProjLimit.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
newProjectLimits, err := s.getUserProjectLimits(ctx, user.ID)
|
2022-03-21 12:15:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: ErrProjLimit.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var projectID uuid.UUID
|
|
|
|
err = s.store.WithTx(ctx, func(ctx context.Context, tx DBTx) error {
|
2022-12-15 03:56:11 +00:00
|
|
|
storageLimit := memory.Size(newProjectLimits.Storage)
|
|
|
|
bandwidthLimit := memory.Size(newProjectLimits.Bandwidth)
|
2022-03-21 12:15:33 +00:00
|
|
|
p, err = tx.Projects().Insert(ctx,
|
|
|
|
&Project{
|
|
|
|
Description: projectInfo.Description,
|
|
|
|
Name: projectInfo.Name,
|
2022-06-05 23:41:38 +01:00
|
|
|
OwnerID: user.ID,
|
|
|
|
UserAgent: user.UserAgent,
|
2022-12-15 03:56:11 +00:00
|
|
|
StorageLimit: &storageLimit,
|
|
|
|
BandwidthLimit: &bandwidthLimit,
|
|
|
|
SegmentLimit: &newProjectLimits.Segment,
|
2022-03-21 12:15:33 +00:00
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = tx.ProjectMembers().Insert(ctx, user.ID, p.ID)
|
2022-03-21 12:15:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
projectID = p.ID
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
s.analytics.TrackProjectCreated(user.ID, user.Email, projectID, currentProjectCount+1)
|
2022-03-21 12:15:33 +00:00
|
|
|
|
|
|
|
return p, httpError
|
|
|
|
}
|
|
|
|
|
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)
|
2022-04-28 16:59:55 +01:00
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
_, _, err = s.isProjectOwner(ctx, user.ID, projectID)
|
2020-10-06 15:25:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
2019-03-29 12:13:37 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
err = s.checkProjectCanBeDeleted(ctx, user, projectID)
|
2020-10-09 14:40:12 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-04-28 16:59:55 +01:00
|
|
|
// GenDeleteProject is a method for deleting project by id for generated API.
|
|
|
|
func (s *Service) GenDeleteProject(ctx context.Context, projectID uuid.UUID) (httpError api.HTTPError) {
|
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "delete project", zap.String("projectID", projectID.String()))
|
2022-04-28 16:59:55 +01:00
|
|
|
if err != nil {
|
|
|
|
return api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
_, p, err := s.isProjectOwner(ctx, user.ID, projectID)
|
2022-04-28 16:59:55 +01:00
|
|
|
if err != nil {
|
2023-01-06 21:40:03 +00:00
|
|
|
status := http.StatusInternalServerError
|
|
|
|
if ErrUnauthorized.Has(err) {
|
|
|
|
status = http.StatusUnauthorized
|
|
|
|
}
|
2022-04-28 16:59:55 +01:00
|
|
|
return api.HTTPError{
|
2023-01-06 21:40:03 +00:00
|
|
|
Status: status,
|
2022-04-28 16:59:55 +01:00
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
projectID = p.ID
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
err = s.checkProjectCanBeDeleted(ctx, user, projectID)
|
2022-04-28 16:59:55 +01:00
|
|
|
if err != nil {
|
|
|
|
return api.HTTPError{
|
|
|
|
Status: http.StatusConflict,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
err = s.store.Projects().Delete(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return httpError
|
|
|
|
}
|
|
|
|
|
2020-09-10 10:32:35 +01:00
|
|
|
// UpdateProject is a method for updating project name and description by id.
|
2023-01-19 14:54:17 +00:00
|
|
|
// projectID here may be project.PublicID or project.ID.
|
2022-07-08 03:01:08 +01:00
|
|
|
func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, updatedProject ProjectInfo) (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
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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)
|
|
|
|
}
|
|
|
|
|
2022-07-08 03:01:08 +01:00
|
|
|
err = ValidateNameAndDescription(updatedProject.Name, updatedProject.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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
isMember, err := s.isProjectMember(ctx, 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
|
|
|
}
|
2023-01-05 14:51:45 +00:00
|
|
|
|
2019-03-29 12:13:37 +00:00
|
|
|
project := isMember.project
|
2023-01-05 14:51:45 +00:00
|
|
|
if updatedProject.Name != project.Name {
|
|
|
|
passesNameCheck, err := s.checkProjectName(ctx, updatedProject, user.ID)
|
|
|
|
if err != nil || !passesNameCheck {
|
|
|
|
return nil, ErrProjName.Wrap(err)
|
|
|
|
}
|
|
|
|
}
|
2022-07-08 03:01:08 +01:00
|
|
|
project.Name = updatedProject.Name
|
|
|
|
project.Description = updatedProject.Description
|
2021-08-02 23:06:15 +01:00
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
if user.PaidTier {
|
2021-08-24 22:12:07 +01:00
|
|
|
if project.BandwidthLimit != nil && *project.BandwidthLimit == 0 {
|
|
|
|
return nil, Error.New("current bandwidth limit for project is set to 0 (updating disabled)")
|
|
|
|
}
|
|
|
|
if project.StorageLimit != nil && *project.StorageLimit == 0 {
|
|
|
|
return nil, Error.New("current storage limit for project is set to 0 (updating disabled)")
|
|
|
|
}
|
2022-07-08 03:01:08 +01:00
|
|
|
if updatedProject.StorageLimit <= 0 || updatedProject.BandwidthLimit <= 0 {
|
2021-08-24 22:12:07 +01:00
|
|
|
return nil, Error.New("project limits must be greater than 0")
|
2021-08-02 23:06:15 +01:00
|
|
|
}
|
|
|
|
|
2022-07-08 03:01:08 +01:00
|
|
|
if updatedProject.StorageLimit > s.config.UsageLimits.Storage.Paid && updatedProject.StorageLimit > *project.StorageLimit {
|
2021-08-24 22:12:07 +01:00
|
|
|
return nil, Error.New("specified storage limit exceeds allowed maximum for current tier")
|
2021-08-02 23:06:15 +01:00
|
|
|
}
|
|
|
|
|
2022-07-08 03:01:08 +01:00
|
|
|
if updatedProject.BandwidthLimit > s.config.UsageLimits.Bandwidth.Paid && updatedProject.BandwidthLimit > *project.BandwidthLimit {
|
2021-08-24 22:12:07 +01:00
|
|
|
return nil, Error.New("specified bandwidth limit exceeds allowed maximum for current tier")
|
2021-08-02 23:06:15 +01:00
|
|
|
}
|
|
|
|
|
2023-01-19 14:54:17 +00:00
|
|
|
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, project.ID)
|
2021-08-02 23:06:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
2022-07-08 03:01:08 +01:00
|
|
|
if updatedProject.StorageLimit.Int64() < storageUsed {
|
2021-08-24 22:12:07 +01:00
|
|
|
return nil, Error.New("cannot set storage limit below current usage")
|
2021-08-02 23:06:15 +01:00
|
|
|
}
|
|
|
|
|
2023-01-19 14:54:17 +00:00
|
|
|
bandwidthUsed, err := s.projectUsage.GetProjectBandwidthTotals(ctx, project.ID)
|
2021-08-02 23:06:15 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
2022-07-08 03:01:08 +01:00
|
|
|
if updatedProject.BandwidthLimit.Int64() < bandwidthUsed {
|
2021-08-24 22:12:07 +01:00
|
|
|
return nil, Error.New("cannot set bandwidth limit below current usage")
|
2021-08-02 23:06:15 +01:00
|
|
|
}
|
2022-11-06 00:43:11 +00:00
|
|
|
/*
|
|
|
|
The purpose of userSpecifiedBandwidthLimit and userSpecifiedStorageLimit is to know if a user has set a bandwidth
|
|
|
|
or storage limit in the UI (to ensure their limits are not unintentionally modified by the satellite admin),
|
|
|
|
the BandwidthLimit and StorageLimit is still used for verifying limits during uploads and downloads.
|
|
|
|
*/
|
|
|
|
if project.StorageLimit != nil && updatedProject.StorageLimit != *project.StorageLimit {
|
|
|
|
project.UserSpecifiedStorageLimit = new(memory.Size)
|
|
|
|
*project.UserSpecifiedStorageLimit = updatedProject.StorageLimit
|
|
|
|
}
|
|
|
|
if project.BandwidthLimit != nil && updatedProject.BandwidthLimit != *project.BandwidthLimit {
|
|
|
|
project.UserSpecifiedBandwidthLimit = new(memory.Size)
|
|
|
|
*project.UserSpecifiedBandwidthLimit = updatedProject.BandwidthLimit
|
|
|
|
}
|
2021-08-02 23:06:15 +01:00
|
|
|
|
|
|
|
project.StorageLimit = new(memory.Size)
|
2022-07-08 03:01:08 +01:00
|
|
|
*project.StorageLimit = updatedProject.StorageLimit
|
2021-08-02 23:06:15 +01:00
|
|
|
project.BandwidthLimit = new(memory.Size)
|
2022-07-08 03:01:08 +01:00
|
|
|
*project.BandwidthLimit = updatedProject.BandwidthLimit
|
2021-08-02 23:06:15 +01:00
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
2022-04-07 09:05:28 +01:00
|
|
|
// GenUpdateProject is a method for updating project name and description by id for generated api.
|
|
|
|
func (s *Service) GenUpdateProject(ctx context.Context, projectID uuid.UUID, projectInfo ProjectInfo) (p *Project, httpError api.HTTPError) {
|
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "update project name and description", zap.String("projectID", projectID.String()))
|
2022-04-07 09:05:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
err = ValidateNameAndDescription(projectInfo.Name, projectInfo.Description)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
2022-04-07 09:05:28 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
project := isMember.project
|
|
|
|
project.Name = projectInfo.Name
|
|
|
|
project.Description = projectInfo.Description
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
if user.PaidTier {
|
2022-04-07 09:05:28 +01:00
|
|
|
if project.BandwidthLimit != nil && *project.BandwidthLimit == 0 {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.New("current bandwidth limit for project is set to 0 (updating disabled)"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if project.StorageLimit != nil && *project.StorageLimit == 0 {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.New("current storage limit for project is set to 0 (updating disabled)"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if projectInfo.StorageLimit <= 0 || projectInfo.BandwidthLimit <= 0 {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Err: Error.New("project limits must be greater than 0"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-08 03:01:08 +01:00
|
|
|
if projectInfo.StorageLimit > s.config.UsageLimits.Storage.Paid && projectInfo.StorageLimit > *project.StorageLimit {
|
2022-04-07 09:05:28 +01:00
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Err: Error.New("specified storage limit exceeds allowed maximum for current tier"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-08 03:01:08 +01:00
|
|
|
if projectInfo.BandwidthLimit > s.config.UsageLimits.Bandwidth.Paid && projectInfo.BandwidthLimit > *project.BandwidthLimit {
|
2022-04-07 09:05:28 +01:00
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Err: Error.New("specified bandwidth limit exceeds allowed maximum for current tier"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
storageUsed, err := s.projectUsage.GetProjectStorageTotals(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if projectInfo.StorageLimit.Int64() < storageUsed {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Err: Error.New("cannot set storage limit below current usage"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bandwidthUsed, err := s.projectUsage.GetProjectBandwidthTotals(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if projectInfo.BandwidthLimit.Int64() < bandwidthUsed {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Err: Error.New("cannot set bandwidth limit below current usage"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
project.StorageLimit = new(memory.Size)
|
|
|
|
*project.StorageLimit = projectInfo.StorageLimit
|
|
|
|
project.BandwidthLimit = new(memory.Size)
|
|
|
|
*project.BandwidthLimit = projectInfo.BandwidthLimit
|
|
|
|
}
|
|
|
|
|
|
|
|
err = s.store.Projects().Update(ctx, project)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return project, httpError
|
|
|
|
}
|
|
|
|
|
2020-07-16 15:18:02 +01:00
|
|
|
// AddProjectMembers adds users by email to given project.
|
2022-11-09 03:16:02 +00:00
|
|
|
// Email addresses not belonging to a user are ignored.
|
2023-01-19 14:54:17 +00:00
|
|
|
// projectID here may be project.PublicID or project.ID.
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2023-01-19 14:54:17 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
|
|
|
if 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
|
|
|
// collect user querying errors
|
|
|
|
for _, email := range emails {
|
|
|
|
user, err := s.store.Users().GetByEmail(ctx, email)
|
2022-11-09 03:16:02 +00:00
|
|
|
if err == nil {
|
|
|
|
users = append(users, user)
|
|
|
|
} else if !errs.Is(err, sql.ErrNoRows) {
|
|
|
|
return nil, Error.Wrap(err)
|
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 {
|
2023-01-19 14:54:17 +00:00
|
|
|
if _, err := tx.ProjectMembers().Insert(ctx, user.ID, isMember.project.ID); err != nil {
|
2019-12-19 10:07:56 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-11-29 22:56:03 +00:00
|
|
|
s.analytics.TrackProjectMemberAddition(user.ID, user.Email)
|
|
|
|
|
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.
|
2023-01-19 14:54:17 +00:00
|
|
|
// projectID here may be project.PublicID or project.ID.
|
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)
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
var isMember isProjectMember
|
|
|
|
if isMember, err = s.isProjectMember(ctx, user.ID, projectID); err != nil {
|
2020-10-06 11:32:34 +01:00
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
projectID = isMember.project.ID
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
isOwner, _, err := s.isProjectOwner(ctx, user.ID, projectID)
|
2020-10-06 15:25:53 +01:00
|
|
|
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
|
|
|
|
})
|
2022-11-29 22:56:03 +00:00
|
|
|
|
|
|
|
s.analytics.TrackProjectMemberDeletion(user.ID, user.Email)
|
|
|
|
|
2019-12-19 10:07:56 +00:00
|
|
|
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
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = s.isProjectMember(ctx, user.ID, projectID)
|
2019-04-05 16:08:14 +01:00
|
|
|
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.
|
2023-01-19 14:54:17 +00:00
|
|
|
// projectID here may be project.PublicID or project.ID.
|
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
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2023-01-19 14:54:17 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, 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
|
|
|
}
|
|
|
|
|
2023-01-19 14:54:17 +00:00
|
|
|
_, err = s.store.APIKeys().GetByNameAndProjectID(ctx, name, isMember.project.ID)
|
2019-10-10 14:28:35 +01:00
|
|
|
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,
|
2023-01-19 14:54:17 +00:00
|
|
|
ProjectID: isMember.project.ID,
|
2019-05-24 17:51:27 +01:00
|
|
|
Secret: secret,
|
2022-06-05 23:41:38 +01:00
|
|
|
UserAgent: user.UserAgent,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
return info, key, nil
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2022-05-06 12:29:59 +01:00
|
|
|
// GenCreateAPIKey creates new api key for generated api.
|
|
|
|
func (s *Service) GenCreateAPIKey(ctx context.Context, requestInfo CreateAPIKeyRequest) (*CreateAPIKeyResponse, api.HTTPError) {
|
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "create api key", zap.String("projectID", requestInfo.ProjectID))
|
2022-05-06 12:29:59 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
reqProjectID, err := uuid.FromString(requestInfo.ProjectID)
|
2022-05-06 12:29:59 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID)
|
2022-05-06 12:29:59 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
projectID := isMember.project.ID
|
|
|
|
|
2022-05-06 12:29:59 +01:00
|
|
|
_, err = s.store.APIKeys().GetByNameAndProjectID(ctx, requestInfo.Name, projectID)
|
|
|
|
if err == nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusConflict,
|
|
|
|
Err: ErrValidation.New(apiKeyWithNameExistsErrMsg),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
secret, err := macaroon.NewSecret()
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := macaroon.NewAPIKey(secret)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
apikey := APIKeyInfo{
|
|
|
|
Name: requestInfo.Name,
|
|
|
|
ProjectID: projectID,
|
|
|
|
Secret: secret,
|
2022-06-05 23:41:38 +01:00
|
|
|
UserAgent: user.UserAgent,
|
2022-05-06 12:29:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
info, err := s.store.APIKeys().Create(ctx, key.Head(), apikey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
// in case the project ID from the request is the public ID, replace projectID with reqProjectID
|
|
|
|
info.ProjectID = reqProjectID
|
|
|
|
|
2022-05-06 12:29:59 +01:00
|
|
|
return &CreateAPIKeyResponse{
|
|
|
|
Key: key.Serialize(),
|
|
|
|
KeyInfo: info,
|
|
|
|
}, api.HTTPError{}
|
|
|
|
}
|
|
|
|
|
2022-08-31 14:55:28 +01:00
|
|
|
// GenDeleteAPIKey deletes api key for generated api.
|
|
|
|
func (s *Service) GenDeleteAPIKey(ctx context.Context, keyID uuid.UUID) (httpError api.HTTPError) {
|
|
|
|
err := s.DeleteAPIKeys(ctx, []uuid.UUID{keyID})
|
|
|
|
if err != nil {
|
|
|
|
if errs.Is(err, sql.ErrNoRows) {
|
|
|
|
return httpError
|
|
|
|
}
|
|
|
|
|
|
|
|
status := http.StatusInternalServerError
|
|
|
|
if ErrUnauthorized.Has(err) {
|
|
|
|
status = http.StatusUnauthorized
|
|
|
|
} else if ErrAPIKeyRequest.Has(err) {
|
|
|
|
status = http.StatusBadRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
return api.HTTPError{
|
|
|
|
Status: status,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return httpError
|
|
|
|
}
|
|
|
|
|
|
|
|
// GenGetAPIKeys returns api keys belonging to a project for generated api.
|
|
|
|
func (s *Service) GenGetAPIKeys(ctx context.Context, projectID uuid.UUID, search string, limit, page uint, order APIKeyOrder, orderDirection OrderDirection) (*APIKeyPage, api.HTTPError) {
|
|
|
|
akp, err := s.GetAPIKeys(ctx, projectID, APIKeyCursor{
|
|
|
|
Search: search,
|
|
|
|
Limit: limit,
|
|
|
|
Page: page,
|
|
|
|
Order: order,
|
|
|
|
OrderDirection: orderDirection,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
status := http.StatusInternalServerError
|
|
|
|
if ErrUnauthorized.Has(err) {
|
|
|
|
status = http.StatusUnauthorized
|
|
|
|
} else if ErrAPIKeyRequest.Has(err) {
|
|
|
|
status = http.StatusBadRequest
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: status,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return akp, api.HTTPError{}
|
|
|
|
}
|
|
|
|
|
2022-02-14 18:20:12 +00:00
|
|
|
// GetAPIKeyInfoByName retrieves an api key by its name and project id.
|
|
|
|
func (s *Service) GetAPIKeyInfoByName(ctx context.Context, projectID uuid.UUID, name string) (_ *APIKeyInfo, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get api key info",
|
2022-02-14 18:20:12 +00:00
|
|
|
zap.String("projectID", projectID.String()),
|
|
|
|
zap.String("name", name))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
key, err := s.store.APIKeys().GetByNameAndProjectID(ctx, name, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = s.isProjectMember(ctx, user.ID, key.ProjectID)
|
2022-02-14 18:20:12 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return key, nil
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = s.isProjectMember(ctx, user.ID, key.ProjectID)
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = s.isProjectMember(ctx, user.ID, key.ProjectID)
|
2019-02-13 11:34:40 +00:00
|
|
|
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.
|
2023-01-05 09:17:16 +00:00
|
|
|
// ID here may be project.publicID or project.ID.
|
2021-03-16 19:43:02 +00:00
|
|
|
func (s *Service) DeleteAPIKeyByNameAndProjectID(ctx context.Context, name string, projectID uuid.UUID) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "delete api key by name and project ID", zap.String("apiKeyName", name), zap.String("projectID", projectID.String()))
|
2021-03-16 19:43:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-05 09:17:16 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
2021-03-16 19:43:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-05 09:17:16 +00:00
|
|
|
key, err := s.store.APIKeys().GetByNameAndProjectID(ctx, name, isMember.project.ID)
|
2021-03-16 19:43:02 +00:00
|
|
|
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.
|
2023-01-06 21:40:03 +00:00
|
|
|
func (s *Service) GetAPIKeys(ctx context.Context, reqProjectID 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
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get api keys", zap.String("projectID", reqProjectID.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
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID)
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
2022-08-31 14:55:28 +01:00
|
|
|
return nil, ErrUnauthorized.Wrap(err)
|
2018-12-27 15:30:15 +00:00
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
projectID := isMember.project.ID
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
// if project ID from request is public ID, replace api key's project IDs with public ID
|
|
|
|
if projectID != reqProjectID {
|
|
|
|
for i := range page.APIKeys {
|
|
|
|
page.APIKeys[i].ProjectID = reqProjectID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return page, err
|
2018-12-26 14:00:53 +00:00
|
|
|
}
|
|
|
|
|
2022-04-12 17:59:07 +01:00
|
|
|
// CreateRESTKey creates a satellite rest key.
|
|
|
|
func (s *Service) CreateRESTKey(ctx context.Context, expiration time.Duration) (apiKey string, expiresAt time.Time, err error) {
|
2022-02-11 22:48:35 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "create rest key")
|
2022-02-11 22:48:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", time.Time{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
apiKey, expiresAt, err = s.restKeys.Create(ctx, user.ID, expiration)
|
2022-02-11 22:48:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", time.Time{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
return apiKey, expiresAt, nil
|
|
|
|
}
|
|
|
|
|
2022-04-12 17:59:07 +01:00
|
|
|
// RevokeRESTKey revokes a satellite REST key.
|
|
|
|
func (s *Service) RevokeRESTKey(ctx context.Context, apiKey string) (err error) {
|
2022-02-11 22:48:35 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = s.getUserAndAuditLog(ctx, "revoke rest key")
|
2022-02-11 22:48:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-04-12 17:59:07 +01:00
|
|
|
err = s.restKeys.Revoke(ctx, apiKey)
|
2022-02-11 22:48:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = s.isProjectMember(ctx, user.ID, projectID)
|
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
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
_, err = s.isProjectMember(ctx, user.ID, projectID)
|
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
|
|
|
}
|
|
|
|
|
2022-05-04 13:33:47 +01:00
|
|
|
usage, err := s.projectAccounting.GetBucketTotals(ctx, projectID, cursor, before)
|
2019-11-15 14:27:44 +00:00
|
|
|
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.
|
2023-02-13 18:19:20 +00:00
|
|
|
// projectID here may be Project.ID or Project.PublicID.
|
2020-11-13 11:41:35 +00:00
|
|
|
func (s *Service) GetAllBucketNames(ctx context.Context, projectID uuid.UUID) (_ []string, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get all bucket names", zap.String("projectID", projectID.String()))
|
2020-11-30 16:51:47 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-02-13 18:19:20 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, 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,
|
|
|
|
}
|
|
|
|
|
2023-02-13 18:19:20 +00:00
|
|
|
bucketsList, err := s.buckets.ListBuckets(ctx, isMember.project.ID, listOptions, allowedBuckets)
|
2020-11-13 11:41:35 +00:00
|
|
|
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.
|
2023-02-13 18:19:20 +00:00
|
|
|
// projectID here may be Project.ID or Project.PublicID.
|
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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2023-02-13 18:19:20 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
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
|
|
|
}
|
|
|
|
|
2023-02-13 18:19:20 +00:00
|
|
|
result, err := s.projectAccounting.GetBucketUsageRollups(ctx, isMember.project.ID, 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
|
|
|
}
|
|
|
|
|
apigen: endpoint to get all buckets usage by project ID
Added new endpoint to get all bucket rollups by bucket ID.
Example of response:
vitalii:~/Documents$ ./testapi.sh
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 11:18:55 GMT
Content-Length: 671
[{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"demo-bucket","totalStoredData":0.0026272243089674662,"totalSegments":0.05000107166666666,"objectCount":0.03333373083333333,"metadataSize":1.6750359008333334e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"},{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"qwe","totalStoredData":0.000018436725422435552,"totalSegments":0.016667081388888887,"objectCount":0.016667081388888887,"metadataSize":1.933381441111111e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"}]
Change-Id: I8b04b24dbc67b78be5c309ce542bf03d6f67e65d
2022-03-07 11:20:28 +00:00
|
|
|
// GenGetBucketUsageRollups retrieves summed usage rollups for every bucket of particular project for a given period for generated api.
|
2023-01-06 21:40:03 +00:00
|
|
|
func (s *Service) GenGetBucketUsageRollups(ctx context.Context, reqProjectID uuid.UUID, since, before time.Time) (rollups []accounting.BucketUsageRollup, httpError api.HTTPError) {
|
apigen: endpoint to get all buckets usage by project ID
Added new endpoint to get all bucket rollups by bucket ID.
Example of response:
vitalii:~/Documents$ ./testapi.sh
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 11:18:55 GMT
Content-Length: 671
[{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"demo-bucket","totalStoredData":0.0026272243089674662,"totalSegments":0.05000107166666666,"objectCount":0.03333373083333333,"metadataSize":1.6750359008333334e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"},{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"qwe","totalStoredData":0.000018436725422435552,"totalSegments":0.016667081388888887,"objectCount":0.016667081388888887,"metadataSize":1.933381441111111e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"}]
Change-Id: I8b04b24dbc67b78be5c309ce542bf03d6f67e65d
2022-03-07 11:20:28 +00:00
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get bucket usage rollups", zap.String("projectID", reqProjectID.String()))
|
apigen: endpoint to get all buckets usage by project ID
Added new endpoint to get all bucket rollups by bucket ID.
Example of response:
vitalii:~/Documents$ ./testapi.sh
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 11:18:55 GMT
Content-Length: 671
[{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"demo-bucket","totalStoredData":0.0026272243089674662,"totalSegments":0.05000107166666666,"objectCount":0.03333373083333333,"metadataSize":1.6750359008333334e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"},{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"qwe","totalStoredData":0.000018436725422435552,"totalSegments":0.016667081388888887,"objectCount":0.016667081388888887,"metadataSize":1.933381441111111e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"}]
Change-Id: I8b04b24dbc67b78be5c309ce542bf03d6f67e65d
2022-03-07 11:20:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID)
|
apigen: endpoint to get all buckets usage by project ID
Added new endpoint to get all bucket rollups by bucket ID.
Example of response:
vitalii:~/Documents$ ./testapi.sh
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 11:18:55 GMT
Content-Length: 671
[{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"demo-bucket","totalStoredData":0.0026272243089674662,"totalSegments":0.05000107166666666,"objectCount":0.03333373083333333,"metadataSize":1.6750359008333334e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"},{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"qwe","totalStoredData":0.000018436725422435552,"totalSegments":0.016667081388888887,"objectCount":0.016667081388888887,"metadataSize":1.933381441111111e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"}]
Change-Id: I8b04b24dbc67b78be5c309ce542bf03d6f67e65d
2022-03-07 11:20:28 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
projectID := isMember.project.ID
|
|
|
|
|
apigen: endpoint to get all buckets usage by project ID
Added new endpoint to get all bucket rollups by bucket ID.
Example of response:
vitalii:~/Documents$ ./testapi.sh
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 11:18:55 GMT
Content-Length: 671
[{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"demo-bucket","totalStoredData":0.0026272243089674662,"totalSegments":0.05000107166666666,"objectCount":0.03333373083333333,"metadataSize":1.6750359008333334e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"},{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"qwe","totalStoredData":0.000018436725422435552,"totalSegments":0.016667081388888887,"objectCount":0.016667081388888887,"metadataSize":1.933381441111111e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"}]
Change-Id: I8b04b24dbc67b78be5c309ce542bf03d6f67e65d
2022-03-07 11:20:28 +00:00
|
|
|
rollups, err = s.projectAccounting.GetBucketUsageRollups(ctx, projectID, since, before)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
// if project ID from request is public ID, replace rollup's project ID with public ID
|
|
|
|
if reqProjectID != projectID {
|
|
|
|
for i := range rollups {
|
|
|
|
rollups[i].ProjectID = reqProjectID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return rollups, httpError
|
apigen: endpoint to get all buckets usage by project ID
Added new endpoint to get all bucket rollups by bucket ID.
Example of response:
vitalii:~/Documents$ ./testapi.sh
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 07 Mar 2022 11:18:55 GMT
Content-Length: 671
[{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"demo-bucket","totalStoredData":0.0026272243089674662,"totalSegments":0.05000107166666666,"objectCount":0.03333373083333333,"metadataSize":1.6750359008333334e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"},{"projectID":"a9b2b1b6-714a-4c49-99f1-6a53d0852525","bucketName":"qwe","totalStoredData":0.000018436725422435552,"totalSegments":0.016667081388888887,"objectCount":0.016667081388888887,"metadataSize":1.933381441111111e-9,"repairEgress":0,"getEgress":0,"auditEgress":0,"since":"2022-03-01T11:00:00Z","before":"2022-03-07T11:17:07Z"}]
Change-Id: I8b04b24dbc67b78be5c309ce542bf03d6f67e65d
2022-03-07 11:20:28 +00:00
|
|
|
}
|
|
|
|
|
2022-02-17 13:48:39 +00:00
|
|
|
// GenGetSingleBucketUsageRollup retrieves usage rollup for single bucket of particular project for a given period for generated api.
|
2023-01-06 21:40:03 +00:00
|
|
|
func (s *Service) GenGetSingleBucketUsageRollup(ctx context.Context, reqProjectID uuid.UUID, bucket string, since, before time.Time) (rollup *accounting.BucketUsageRollup, httpError api.HTTPError) {
|
2022-02-17 13:48:39 +00:00
|
|
|
var err error
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get single bucket usage rollup", zap.String("projectID", reqProjectID.String()))
|
2022-02-17 13:48:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, reqProjectID)
|
2022-02-17 13:48:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
projectID := isMember.project.ID
|
|
|
|
|
2022-02-17 13:48:39 +00:00
|
|
|
rollup, err = s.projectAccounting.GetSingleBucketUsageRollup(ctx, projectID, bucket, since, before)
|
|
|
|
if err != nil {
|
|
|
|
return nil, api.HTTPError{
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Err: Error.Wrap(err),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
// make sure to replace rollup project ID with reqProjectID in case it is the public ID
|
|
|
|
rollup.ProjectID = reqProjectID
|
|
|
|
|
|
|
|
return rollup, httpError
|
2022-02-17 13:48:39 +00:00
|
|
|
}
|
|
|
|
|
2021-12-07 14:41:39 +00:00
|
|
|
// GetDailyProjectUsage returns daily usage by project ID.
|
2023-01-05 11:14:55 +00:00
|
|
|
// ID here may be project.ID or project.PublicID.
|
2021-12-07 14:41:39 +00:00
|
|
|
func (s *Service) GetDailyProjectUsage(ctx context.Context, projectID uuid.UUID, from, to time.Time) (_ *accounting.ProjectDailyUsage, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get daily usage by project ID")
|
2021-12-07 14:41:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-05 11:14:55 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
2021-12-07 14:41:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-05 11:14:55 +00:00
|
|
|
usage, err := s.projectAccounting.GetProjectDailyUsageByDateRange(ctx, isMember.project.ID, from, to, s.config.AsOfSystemTimeDuration)
|
2021-12-07 14:41:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2021-12-09 15:05:21 +00:00
|
|
|
return usage, nil
|
2021-12-07 14:41:39 +00: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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2023-01-05 11:14:55 +00:00
|
|
|
isMember, err := s.isProjectMember(ctx, user.ID, projectID)
|
2021-11-17 15:32:34 +00:00
|
|
|
if err != nil {
|
2021-06-24 16:49:15 +01:00
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2023-01-05 11:14:55 +00:00
|
|
|
prUsageLimits, err := s.getProjectUsageLimits(ctx, isMember.project.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
|
|
|
|
2023-01-05 11:14:55 +00:00
|
|
|
prObjectsSegments, err := s.projectAccounting.GetProjectObjectsSegments(ctx, isMember.project.ID)
|
2021-11-17 15:32:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2021-06-24 16:49:15 +01:00
|
|
|
return &ProjectUsageLimits{
|
|
|
|
StorageLimit: prUsageLimits.StorageLimit,
|
|
|
|
BandwidthLimit: prUsageLimits.BandwidthLimit,
|
|
|
|
StorageUsed: prUsageLimits.StorageUsed,
|
|
|
|
BandwidthUsed: prUsageLimits.BandwidthUsed,
|
2021-11-17 15:32:34 +00:00
|
|
|
ObjectCount: prObjectsSegments.ObjectCount,
|
|
|
|
SegmentCount: prObjectsSegments.SegmentCount,
|
2023-02-15 17:54:22 +00:00
|
|
|
SegmentLimit: prUsageLimits.SegmentLimit,
|
|
|
|
SegmentUsed: prUsageLimits.SegmentUsed,
|
2021-06-24 16:49:15 +01:00
|
|
|
}, 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)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.getUserAndAuditLog(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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
projects, err := s.store.Projects().GetOwn(ctx, 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
|
|
|
|
}
|
2023-02-15 17:54:22 +00:00
|
|
|
segmentLimit, err := s.projectUsage.GetProjectSegmentLimit(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-06-24 16:49:15 +01:00
|
|
|
|
|
|
|
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
|
|
|
}
|
2023-02-15 17:54:22 +00:00
|
|
|
segmentUsed, err := s.projectUsage.GetProjectSegmentTotals(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-12-12 12:58:15 +00:00
|
|
|
|
|
|
|
return &ProjectUsageLimits{
|
|
|
|
StorageLimit: storageLimit.Int64(),
|
|
|
|
BandwidthLimit: bandwidthLimit.Int64(),
|
|
|
|
StorageUsed: storageUsed,
|
|
|
|
BandwidthUsed: bandwidthUsed,
|
2023-02-15 17:54:22 +00:00
|
|
|
SegmentLimit: segmentLimit.Int64(),
|
|
|
|
SegmentUsed: segmentUsed,
|
2019-12-12 12:58:15 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
// TokenAuth returns an authenticated context by session token.
|
|
|
|
func (s *Service) TokenAuth(ctx context.Context, token consoleauth.Token, authTime time.Time) (_ context.Context, err error) {
|
2018-12-20 20:10:27 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2018-11-21 15:51:43 +00:00
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
valid, err := s.tokens.ValidateToken(token)
|
2018-11-21 15:51:43 +00:00
|
|
|
if err != nil {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
if !valid {
|
|
|
|
return nil, Error.New("incorrect signature")
|
2018-11-21 15:51:43 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
sessionID, err := uuid.FromBytes(token.Payload)
|
2018-11-27 14:20:58 +00:00
|
|
|
if err != nil {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-27 14:20:58 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
session, err := s.store.WebappSessions().GetBySessionID(ctx, sessionID)
|
2018-11-27 14:20:58 +00:00
|
|
|
if err != nil {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2018-11-27 14:20:58 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
ctx, err = s.authorize(ctx, session.UserID, session.ExpiresAt, authTime)
|
|
|
|
if err != nil {
|
|
|
|
err := errs.Combine(err, s.store.WebappSessions().DeleteBySessionID(ctx, sessionID))
|
2022-03-27 11:16:46 +01:00
|
|
|
if err != nil {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, Error.Wrap(err)
|
2022-03-27 11:16:46 +01:00
|
|
|
}
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, err
|
2022-03-27 11:16:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return ctx, nil
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
// KeyAuth returns an authenticated context by api key.
|
|
|
|
func (s *Service) KeyAuth(ctx context.Context, apikey string, authTime time.Time) (_ context.Context, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
2022-04-27 12:52:02 +01:00
|
|
|
|
2022-03-27 11:16:46 +01:00
|
|
|
ctx = consoleauth.WithAPIKey(ctx, []byte(apikey))
|
|
|
|
|
2022-04-12 17:59:07 +01:00
|
|
|
userID, exp, err := s.restKeys.GetUserAndExpirationFromKey(ctx, apikey)
|
2022-03-27 11:16:46 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
ctx, err = s.authorize(ctx, userID, exp, authTime)
|
2022-02-17 13:48:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
return ctx, nil
|
2022-02-17 13:48:39 +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.
|
2022-06-05 23:41:38 +01:00
|
|
|
func (s *Service) checkProjectCanBeDeleted(ctx context.Context, user *User, projectID uuid.UUID) (err error) {
|
2020-10-09 14:40:12 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-04-28 16:59:55 +01:00
|
|
|
buckets, err := s.buckets.CountBuckets(ctx, projectID)
|
2020-10-09 14:40:12 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if buckets > 0 {
|
|
|
|
return ErrUsage.New("some buckets still exist")
|
|
|
|
}
|
|
|
|
|
2022-04-28 16:59:55 +01:00
|
|
|
keys, err := s.store.APIKeys().GetPagedByProjectID(ctx, projectID, APIKeyCursor{Limit: 1, Page: 1})
|
2020-10-09 14:40:12 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if keys.TotalCount > 0 {
|
2022-04-28 16:59:55 +01:00
|
|
|
return ErrUsage.New("some api keys still exist")
|
|
|
|
}
|
|
|
|
|
|
|
|
if user.PaidTier {
|
|
|
|
err = s.Payments().checkProjectUsageStatus(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return ErrUsage.Wrap(err)
|
|
|
|
}
|
2020-10-09 14:40:12 +01:00
|
|
|
}
|
|
|
|
|
2022-04-28 16:59:55 +01:00
|
|
|
err = s.Payments().checkProjectInvoicingStatus(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return ErrUsage.Wrap(err)
|
2020-10-09 14:40:12 +01:00
|
|
|
}
|
2022-04-28 16:59:55 +01:00
|
|
|
|
|
|
|
return nil
|
2020-10-09 14:40:12 +01:00
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2023-01-05 14:51:45 +00:00
|
|
|
// checkProjectName is used to check if user has used project name before.
|
|
|
|
func (s *Service) checkProjectName(ctx context.Context, projectInfo ProjectInfo, userID uuid.UUID) (passesNameCheck bool, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
passesCheck := true
|
|
|
|
|
|
|
|
projects, err := s.store.Projects().GetOwn(ctx, userID)
|
|
|
|
if err != nil {
|
|
|
|
return false, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, project := range projects {
|
|
|
|
if project.Name == projectInfo.Name {
|
|
|
|
return false, ErrProjName.New(projNameErrMsg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return passesCheck, nil
|
|
|
|
}
|
|
|
|
|
2021-11-01 15:27:32 +00:00
|
|
|
// getUserProjectLimits is a method to get the users storage and bandwidth limits for new projects.
|
2022-12-15 03:56:11 +00:00
|
|
|
func (s *Service) getUserProjectLimits(ctx context.Context, userID uuid.UUID) (_ *UsageLimits, err error) {
|
2021-11-01 15:27:32 +00:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
result, err := s.store.Users().GetUserProjectLimits(ctx, userID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
2022-12-15 03:56:11 +00:00
|
|
|
return &UsageLimits{
|
|
|
|
Storage: result.ProjectStorageLimit.Int64(),
|
|
|
|
Bandwidth: result.ProjectBandwidthLimit.Int64(),
|
|
|
|
Segment: result.ProjectSegmentLimit,
|
2021-11-01 15:27:32 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
// authorize returns an authorized context by user ID.
|
|
|
|
func (s *Service) authorize(ctx context.Context, userID uuid.UUID, expiration time.Time, authTime time.Time) (_ context.Context, err error) {
|
2019-06-04 12:55:38 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2022-06-05 23:41:38 +01:00
|
|
|
if !expiration.IsZero() && expiration.Before(authTime) {
|
|
|
|
return nil, ErrTokenExpiration.New("authorization failed. expiration reached.")
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := s.store.Users().Get(ctx, userID)
|
2018-11-14 10:50:15 +00:00
|
|
|
if err != nil {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, Error.New("authorization failed. no user with id: %s", userID.String())
|
2018-11-14 10:50:15 +00:00
|
|
|
}
|
|
|
|
|
2021-09-09 14:24:41 +01:00
|
|
|
if user.Status != Active {
|
2022-06-05 23:41:38 +01:00
|
|
|
return nil, Error.New("authorization failed. no active user with id: %s", userID.String())
|
2021-09-09 14:24:41 +01:00
|
|
|
}
|
2022-06-05 23:41:38 +01:00
|
|
|
return WithUser(ctx, 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.
|
2023-01-06 21:40:03 +00:00
|
|
|
func (s *Service) isProjectOwner(ctx context.Context, userID uuid.UUID, projectID uuid.UUID) (isOwner bool, project *Project, err error) {
|
2019-08-07 13:28:13 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
2023-01-06 21:40:03 +00:00
|
|
|
|
|
|
|
project, err = s.store.Projects().GetByPublicID(ctx, projectID)
|
2019-08-07 13:28:13 +01:00
|
|
|
if err != nil {
|
2023-01-06 21:40:03 +00:00
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
|
|
project, err = s.store.Projects().Get(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return false, nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return false, nil, Error.Wrap(err)
|
|
|
|
}
|
2019-08-07 13:28:13 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if project.OwnerID != userID {
|
2023-01-06 21:40:03 +00:00
|
|
|
return false, nil, ErrUnauthorized.New(unauthorizedErrMsg)
|
2019-08-07 13:28:13 +01:00
|
|
|
}
|
|
|
|
|
2023-01-06 21:40:03 +00:00
|
|
|
return true, project, 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.
|
2023-01-05 09:17:16 +00:00
|
|
|
// projectID can be either private ID or public ID (project.ID/project.PublicID).
|
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)
|
2023-01-05 09:17:16 +00:00
|
|
|
var project *Project
|
|
|
|
project, err = s.store.Projects().GetByPublicID(ctx, projectID)
|
2018-12-27 15:30:15 +00:00
|
|
|
if err != nil {
|
2023-01-05 09:17:16 +00:00
|
|
|
tempError := err
|
|
|
|
project, err = s.store.Projects().Get(ctx, projectID)
|
|
|
|
if err != nil {
|
|
|
|
return isProjectMember{}, Error.Wrap(errs.Combine(tempError, 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
|
|
|
}
|
|
|
|
|
2023-01-05 09:17:16 +00:00
|
|
|
membership, ok := findMembershipByProjectID(memberships, project.ID)
|
2020-07-20 16:22:36 +01:00
|
|
|
if ok {
|
|
|
|
return isProjectMember{
|
|
|
|
project: project,
|
|
|
|
membership: &membership,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return isProjectMember{}, ErrNoMembership.New(unauthorizedErrMsg)
|
|
|
|
}
|
|
|
|
|
2022-05-11 09:02:58 +01:00
|
|
|
// WalletInfo contains all the information about a destination wallet assigned to a user.
|
|
|
|
type WalletInfo struct {
|
|
|
|
Address blockchain.Address `json:"address"`
|
2022-09-21 16:01:36 +01:00
|
|
|
Balance currency.Amount `json:"balance"`
|
2022-05-11 09:02:58 +01:00
|
|
|
}
|
|
|
|
|
2022-06-16 14:26:27 +01:00
|
|
|
// PaymentInfo includes token payment information required by GUI.
|
|
|
|
type PaymentInfo struct {
|
|
|
|
ID string
|
|
|
|
Type string
|
|
|
|
Wallet string
|
2022-09-06 13:43:09 +01:00
|
|
|
Amount currency.Amount
|
|
|
|
Received currency.Amount
|
2022-06-16 14:26:27 +01:00
|
|
|
Status string
|
|
|
|
Link string
|
|
|
|
Timestamp time.Time
|
2022-05-11 09:02:58 +01:00
|
|
|
}
|
|
|
|
|
2022-06-16 14:26:27 +01:00
|
|
|
// WalletPayments represents the list of ERC-20 token payments.
|
|
|
|
type WalletPayments struct {
|
|
|
|
Payments []PaymentInfo `json:"payments"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// EtherscanURL creates etherscan transaction URI.
|
|
|
|
func EtherscanURL(tx string) string {
|
|
|
|
return "https://etherscan.io/tx/" + tx
|
2022-05-11 09:02:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// ErrWalletNotClaimed shows that no address is claimed by the user.
|
|
|
|
var ErrWalletNotClaimed = errs.Class("wallet is not claimed")
|
|
|
|
|
|
|
|
// ClaimWallet requests a new wallet for the users to be used for payments. If wallet is already claimed,
|
|
|
|
// it will return with the info without error.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) ClaimWallet(ctx context.Context) (_ WalletInfo, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := payment.service.getUserAndAuditLog(ctx, "claim wallet")
|
2022-04-28 03:54:56 +01:00
|
|
|
if err != nil {
|
|
|
|
return WalletInfo{}, Error.Wrap(err)
|
|
|
|
}
|
2022-06-05 23:41:38 +01:00
|
|
|
address, err := payment.service.depositWallets.Claim(ctx, user.ID)
|
2022-04-28 03:54:56 +01:00
|
|
|
if err != nil {
|
|
|
|
return WalletInfo{}, Error.Wrap(err)
|
|
|
|
}
|
2022-08-15 15:41:19 +01:00
|
|
|
balance, err := payment.service.billing.GetBalance(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return WalletInfo{}, Error.Wrap(err)
|
|
|
|
}
|
2022-04-28 03:54:56 +01:00
|
|
|
return WalletInfo{
|
|
|
|
Address: address,
|
2022-09-21 16:01:36 +01:00
|
|
|
Balance: balance,
|
2022-04-28 03:54:56 +01:00
|
|
|
}, nil
|
2022-05-11 09:02:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetWallet returns with the assigned wallet, or with ErrWalletNotClaimed if not yet claimed.
|
2022-04-28 03:54:56 +01:00
|
|
|
func (payment Payments) GetWallet(ctx context.Context) (_ WalletInfo, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-06-05 23:41:38 +01:00
|
|
|
user, err := GetUser(ctx)
|
2022-04-28 03:54:56 +01:00
|
|
|
if err != nil {
|
|
|
|
return WalletInfo{}, Error.Wrap(err)
|
|
|
|
}
|
2022-06-05 23:41:38 +01:00
|
|
|
address, err := payment.service.depositWallets.Get(ctx, user.ID)
|
2022-04-28 03:54:56 +01:00
|
|
|
if err != nil {
|
|
|
|
return WalletInfo{}, Error.Wrap(err)
|
|
|
|
}
|
2022-08-15 15:41:19 +01:00
|
|
|
balance, err := payment.service.billing.GetBalance(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return WalletInfo{}, Error.Wrap(err)
|
|
|
|
}
|
2022-04-28 03:54:56 +01:00
|
|
|
return WalletInfo{
|
|
|
|
Address: address,
|
2022-09-21 16:01:36 +01:00
|
|
|
Balance: balance,
|
2022-04-28 03:54:56 +01:00
|
|
|
}, nil
|
2022-05-11 09:02:58 +01:00
|
|
|
}
|
|
|
|
|
2022-06-16 14:26:27 +01:00
|
|
|
// WalletPayments returns with all the native blockchain payments for a user's wallet.
|
|
|
|
func (payment Payments) WalletPayments(ctx context.Context) (_ WalletPayments, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
user, err := GetUser(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return WalletPayments{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
address, err := payment.service.depositWallets.Get(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return WalletPayments{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
walletPayments, err := payment.service.depositWallets.Payments(ctx, address, 3000, 0)
|
|
|
|
if err != nil {
|
|
|
|
return WalletPayments{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
txInfos, err := payment.service.accounts.StorjTokens().ListTransactionInfos(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return WalletPayments{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var paymentInfos []PaymentInfo
|
|
|
|
for _, walletPayment := range walletPayments {
|
|
|
|
paymentInfos = append(paymentInfos, PaymentInfo{
|
|
|
|
ID: fmt.Sprintf("%s#%d", walletPayment.Transaction.Hex(), walletPayment.LogIndex),
|
|
|
|
Type: "storjscan",
|
|
|
|
Wallet: walletPayment.To.Hex(),
|
|
|
|
Amount: walletPayment.USDValue,
|
|
|
|
Status: string(walletPayment.Status),
|
|
|
|
Link: EtherscanURL(walletPayment.Transaction.Hex()),
|
|
|
|
Timestamp: walletPayment.Timestamp,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
for _, txInfo := range txInfos {
|
|
|
|
paymentInfos = append(paymentInfos, PaymentInfo{
|
|
|
|
ID: txInfo.ID.String(),
|
|
|
|
Type: "coinpayments",
|
|
|
|
Wallet: txInfo.Address,
|
2022-09-06 13:43:09 +01:00
|
|
|
Amount: currency.AmountFromBaseUnits(txInfo.AmountCents, currency.USDollars),
|
|
|
|
Received: currency.AmountFromBaseUnits(txInfo.ReceivedCents, currency.USDollars),
|
2022-06-16 14:26:27 +01:00
|
|
|
Status: txInfo.Status.String(),
|
|
|
|
Link: txInfo.Link,
|
|
|
|
Timestamp: txInfo.CreatedAt.UTC(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return WalletPayments{
|
|
|
|
Payments: paymentInfos,
|
|
|
|
}, nil
|
2022-05-11 09:02:58 +01:00
|
|
|
}
|
|
|
|
|
2023-01-26 18:31:13 +00:00
|
|
|
// Purchase makes a purchase of `price` amount with description of `desc` and payment method with id of `paymentMethodID`.
|
2023-03-22 15:28:52 +00:00
|
|
|
// If a paid invoice with the same description exists, then we assume this is a retried request and don't create and pay
|
|
|
|
// another invoice.
|
2023-02-02 22:11:09 +00:00
|
|
|
func (payment Payments) Purchase(ctx context.Context, price int64, desc string, paymentMethodID string) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
if desc == "" {
|
|
|
|
return ErrPurchaseDesc.New("description cannot be empty")
|
|
|
|
}
|
|
|
|
user, err := GetUser(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
invoices, err := payment.service.accounts.Invoices().List(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check for any previously created unpaid invoice with the same description.
|
|
|
|
// If draft, delete it and create new and pay. If open, pay it and don't create new.
|
2023-03-22 15:28:52 +00:00
|
|
|
// If paid, skip.
|
2023-02-02 22:11:09 +00:00
|
|
|
for _, inv := range invoices {
|
|
|
|
if inv.Description == desc {
|
2023-03-22 15:28:52 +00:00
|
|
|
if inv.Status == payments.InvoiceStatusPaid {
|
|
|
|
return nil
|
|
|
|
}
|
2023-02-02 22:11:09 +00:00
|
|
|
if inv.Status == payments.InvoiceStatusDraft {
|
|
|
|
_, err := payment.service.accounts.Invoices().Delete(ctx, inv.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
} else if inv.Status == payments.InvoiceStatusOpen {
|
|
|
|
_, err = payment.service.accounts.Invoices().Pay(ctx, inv.ID, paymentMethodID)
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inv, err := payment.service.accounts.Invoices().Create(ctx, user.ID, price, desc)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = payment.service.accounts.Invoices().Pay(ctx, inv.ID, paymentMethodID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-03-22 20:23:44 +00:00
|
|
|
// UpdatePackage updates a user's package information unless they already have a package.
|
|
|
|
func (payment Payments) UpdatePackage(ctx context.Context, packagePlan string, purchaseTime time.Time) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
user, err := GetUser(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
dbPackagePlan, dbPurchaseTime, err := payment.service.accounts.GetPackageInfo(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
if dbPackagePlan != nil || dbPurchaseTime != nil {
|
|
|
|
return ErrAlreadyHasPackage.New("user already has package")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = payment.service.accounts.UpdatePackage(ctx, user.ID, &packagePlan, &purchaseTime)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-03-24 22:25:36 +00:00
|
|
|
// ApplyCredit applies a credit of `amount` with description of `desc` to the user's balance. `amount` is in cents USD.
|
|
|
|
// If a credit with `desc` already exists, another one will not be created.
|
|
|
|
func (payment Payments) ApplyCredit(ctx context.Context, amount int64, desc string) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
if desc == "" {
|
|
|
|
return ErrPurchaseDesc.New("description cannot be empty")
|
|
|
|
}
|
|
|
|
user, err := GetUser(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
btxs, err := payment.service.accounts.Balances().ListTransactions(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// check for any previously created transaction with the same description.
|
|
|
|
for _, btx := range btxs {
|
|
|
|
if btx.Description == desc {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = payment.service.accounts.Balances().ApplyCredit(ctx, user.ID, amount, desc)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-01-12 03:50:31 +00:00
|
|
|
// GetProjectUsagePriceModel returns the project usage price model for the user.
|
|
|
|
func (payment Payments) GetProjectUsagePriceModel(ctx context.Context) (_ *payments.ProjectUsagePriceModel, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2023-02-23 16:27:37 +00:00
|
|
|
user, err := GetUser(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
model := payment.service.accounts.GetProjectUsagePriceModel(string(user.UserAgent))
|
2023-01-12 03:50:31 +00:00
|
|
|
return &model, nil
|
|
|
|
}
|
|
|
|
|
2020-07-20 16:22:36 +01:00
|
|
|
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
|
|
|
}
|
2022-06-05 23:41:38 +01:00
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
// DeleteSession removes the session from the database.
|
|
|
|
func (s *Service) DeleteSession(ctx context.Context, sessionID uuid.UUID) (err error) {
|
2022-06-05 23:41:38 +01:00
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
return Error.Wrap(s.store.WebappSessions().DeleteBySessionID(ctx, sessionID))
|
|
|
|
}
|
|
|
|
|
2022-11-21 22:56:09 +00:00
|
|
|
// DeleteAllSessionsByUserIDExcept removes all sessions except the specified session from the database.
|
|
|
|
func (s *Service) DeleteAllSessionsByUserIDExcept(ctx context.Context, userID uuid.UUID, sessionID uuid.UUID) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
sessions, err := s.store.WebappSessions().GetAllByUserID(ctx, userID)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
if session.ID != sessionID {
|
|
|
|
err = s.DeleteSession(ctx, session.ID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
// RefreshSession resets the expiration time of the session.
|
|
|
|
func (s *Service) RefreshSession(ctx context.Context, sessionID uuid.UUID) (expiresAt time.Time, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
2023-03-07 16:40:49 +00:00
|
|
|
user, err := s.getUserAndAuditLog(ctx, "refresh session")
|
2022-06-05 23:41:38 +01:00
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return time.Time{}, Error.Wrap(err)
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
|
|
|
|
2023-03-07 16:40:49 +00:00
|
|
|
duration := time.Duration(s.config.Session.InactivityTimerDuration) * time.Second
|
|
|
|
settings, err := s.store.Users().GetSettings(ctx, user.ID)
|
|
|
|
if err != nil && !errs.Is(err, sql.ErrNoRows) {
|
|
|
|
return time.Time{}, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
if settings != nil && settings.SessionDuration != nil {
|
|
|
|
duration = *settings.SessionDuration
|
|
|
|
}
|
|
|
|
expiresAt = time.Now().Add(duration)
|
2022-07-19 10:26:18 +01:00
|
|
|
|
|
|
|
err = s.store.WebappSessions().UpdateExpiration(ctx, sessionID, expiresAt)
|
2022-06-05 23:41:38 +01:00
|
|
|
if err != nil {
|
2022-07-19 10:26:18 +01:00
|
|
|
return time.Time{}, err
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
|
|
|
|
2022-07-19 10:26:18 +01:00
|
|
|
return expiresAt, nil
|
2022-06-05 23:41:38 +01:00
|
|
|
}
|
2022-09-01 03:19:06 +01:00
|
|
|
|
|
|
|
// VerifyForgotPasswordCaptcha returns whether the given captcha response for the forgot password page is valid.
|
|
|
|
// It will return true without error if the captcha handler has not been set.
|
|
|
|
func (s *Service) VerifyForgotPasswordCaptcha(ctx context.Context, responseToken, userIP string) (valid bool, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
if s.loginCaptchaHandler != nil {
|
|
|
|
valid, _, err = s.loginCaptchaHandler.Verify(ctx, responseToken, userIP)
|
|
|
|
return valid, ErrCaptcha.Wrap(err)
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
2023-03-16 11:42:01 +00:00
|
|
|
|
|
|
|
// GetUserSettings fetches a user's settings. It creates default settings if none exists.
|
|
|
|
func (s *Service) GetUserSettings(ctx context.Context) (settings *UserSettings, err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get user settings")
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
settings, err = s.store.Users().GetSettings(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
if !errs.Is(err, sql.ErrNoRows) {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
2023-03-30 22:21:27 +01:00
|
|
|
|
|
|
|
settingsReq := UpsertUserSettingsRequest{}
|
|
|
|
// a user may have existed before a corresponding row was created in the user settings table
|
|
|
|
// to avoid showing an old user the onboarding flow again, we check to see if the user owns any projects already
|
|
|
|
// if so, set the "onboarding start" and "onboarding end" fields to "true"
|
|
|
|
projects, err := s.store.Projects().GetOwn(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
// we can still proceed with the settings upsert if there is an error retrieving projects, so log and don't return
|
|
|
|
s.log.Warn("received error trying to get user's projects", zap.Error(err))
|
|
|
|
}
|
|
|
|
if len(projects) > 0 {
|
|
|
|
t := true
|
|
|
|
settingsReq.OnboardingStart = &(t)
|
|
|
|
settingsReq.OnboardingEnd = &(t)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = s.store.Users().UpsertSettings(ctx, user.ID, settingsReq)
|
2023-03-16 11:42:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
settings, err = s.store.Users().GetSettings(ctx, user.ID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Error.Wrap(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return settings, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetUserSettings updates a user's settings.
|
|
|
|
func (s *Service) SetUserSettings(ctx context.Context, request UpsertUserSettingsRequest) (err error) {
|
|
|
|
defer mon.Task()(&ctx)(&err)
|
|
|
|
|
|
|
|
user, err := s.getUserAndAuditLog(ctx, "get user settings")
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = s.store.Users().UpsertSettings(ctx, user.ID, request)
|
|
|
|
if err != nil {
|
|
|
|
return Error.Wrap(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|