storj/satellite/console/consoleweb/consoleapi/payments.go
Wilfred Asomani 2e3ed973de satellite/{consoleapi,web}: limit user input
This change limits the length of user input fields like search, email,
username. It also limits the receivable size of request payloads.
This is to prevent potential DDoS attacks resulting from receiving
large payloads.
Improvements are also made to the accounts page and register success
pages to display long names/emails better.

Issue: https://github.com/storj/storj-private/issues/201

Change-Id: I5d36eb83609b3605335a8d150b275c89deaf3b43
2023-06-05 11:43:56 +00:00

573 lines
15 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package consoleapi
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
"github.com/spacemonkeygo/monkit/v3"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/storj/private/web"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/billing"
"storj.io/storj/satellite/payments/paymentsconfig"
)
var (
// ErrPaymentsAPI - console payments api error type.
ErrPaymentsAPI = errs.Class("consoleapi payments")
mon = monkit.Package()
)
// Payments is an api controller that exposes all payment related functionality.
type Payments struct {
log *zap.Logger
service *console.Service
accountFreezeService *console.AccountFreezeService
packagePlans paymentsconfig.PackagePlans
}
// NewPayments is a constructor for api payments controller.
func NewPayments(log *zap.Logger, service *console.Service, accountFreezeService *console.AccountFreezeService, packagePlans paymentsconfig.PackagePlans) *Payments {
return &Payments{
log: log,
service: service,
accountFreezeService: accountFreezeService,
packagePlans: packagePlans,
}
}
// SetupAccount creates a payment account for the user.
func (p *Payments) SetupAccount(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
couponType, err := p.service.Payments().SetupAccount(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = json.NewEncoder(w).Encode(couponType)
if err != nil {
p.log.Error("failed to write json token deposit response", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// AccountBalance returns an integer amount in cents that represents the current balance of payment account.
func (p *Payments) AccountBalance(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
balance, err := p.service.Payments().AccountBalance(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = json.NewEncoder(w).Encode(&balance)
if err != nil {
p.log.Error("failed to write json balance response", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// ProjectsCharges returns how much money current user will be charged for each project which he owns.
func (p *Payments) ProjectsCharges(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
var response struct {
PriceModels map[string]payments.ProjectUsagePriceModel `json:"priceModels"`
Charges payments.ProjectChargesResponse `json:"charges"`
}
w.Header().Set("Content-Type", "application/json")
sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("from"), 10, 64)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("to"), 10, 64)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
since := time.Unix(sinceStamp, 0).UTC()
before := time.Unix(beforeStamp, 0).UTC()
charges, err := p.service.Payments().ProjectsCharges(ctx, since, before)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
response.Charges = charges
response.PriceModels = make(map[string]payments.ProjectUsagePriceModel)
seen := make(map[string]struct{})
for _, partnerCharges := range charges {
for partner := range partnerCharges {
if _, ok := seen[partner]; ok {
continue
}
response.PriceModels[partner] = *p.service.Payments().GetProjectUsagePriceModel(partner)
seen[partner] = struct{}{}
}
}
err = json.NewEncoder(w).Encode(response)
if err != nil {
p.log.Error("failed to write json project usage and charges response", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// triggerAttemptPaymentIfFrozenOrWarned checks if the account is frozen and if frozen, will trigger attempt to pay outstanding invoices.
func (p *Payments) triggerAttemptPaymentIfFrozenOrWarned(ctx context.Context) (err error) {
defer mon.Task()(&ctx)(&err)
userID, err := p.service.GetUserID(ctx)
if err != nil {
return err
}
freeze, warning, err := p.accountFreezeService.GetAll(ctx, userID)
if err != nil {
return err
}
if freeze != nil || warning != nil {
err = p.service.Payments().AttemptPayOverdueInvoices(ctx)
if err != nil {
return err
}
}
if freeze != nil {
err = p.accountFreezeService.UnfreezeUser(ctx, userID)
if err != nil {
return err
}
} else if warning != nil {
err = p.accountFreezeService.UnWarnUser(ctx, userID)
if err != nil {
return err
}
}
return nil
}
// AddCreditCard is used to save new credit card and attach it to payment account.
func (p *Payments) AddCreditCard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
token := string(bodyBytes)
_, err = p.service.Payments().AddCreditCard(ctx, token)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = p.triggerAttemptPaymentIfFrozenOrWarned(ctx)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
}
// ListCreditCards returns a list of credit cards for a given payment account.
func (p *Payments) ListCreditCards(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
cards, err := p.service.Payments().ListCreditCards(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
if cards == nil {
_, err = w.Write([]byte("[]"))
} else {
err = json.NewEncoder(w).Encode(cards)
}
if err != nil {
p.log.Error("failed to write json list cards response", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// MakeCreditCardDefault makes a credit card default payment method.
func (p *Payments) MakeCreditCardDefault(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
cardID, err := io.ReadAll(r.Body)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
err = p.service.Payments().MakeCreditCardDefault(ctx, string(cardID))
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
err = p.triggerAttemptPaymentIfFrozenOrWarned(ctx)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
}
// RemoveCreditCard is used to detach a credit card from payment account.
func (p *Payments) RemoveCreditCard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
vars := mux.Vars(r)
cardID := vars["cardId"]
if cardID == "" {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
err = p.service.Payments().RemoveCreditCard(ctx, cardID)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
}
// BillingHistory returns a list of invoices, transactions and all others billing history items for payment account.
func (p *Payments) BillingHistory(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
billingHistory, err := p.service.Payments().BillingHistory(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
if billingHistory == nil {
_, err = w.Write([]byte("[]"))
} else {
err = json.NewEncoder(w).Encode(billingHistory)
}
if err != nil {
p.log.Error("failed to write json billing history response", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// ApplyCouponCode applies a coupon code to the user's account.
func (p *Payments) ApplyCouponCode(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
couponCode := string(bodyBytes)
coupon, err := p.service.Payments().ApplyCouponCode(ctx, couponCode)
if err != nil {
status := http.StatusInternalServerError
if payments.ErrInvalidCoupon.Has(err) {
status = http.StatusBadRequest
} else if payments.ErrCouponConflict.Has(err) {
status = http.StatusConflict
}
p.serveJSONError(w, status, err)
return
}
if err = json.NewEncoder(w).Encode(coupon); err != nil {
p.log.Error("failed to encode coupon", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// GetCoupon returns the coupon applied to the user's account.
func (p *Payments) GetCoupon(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
coupon, err := p.service.Payments().GetCoupon(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
if err = json.NewEncoder(w).Encode(coupon); err != nil {
p.log.Error("failed to encode coupon", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// GetWallet returns the wallet address (with balance) already assigned to the user.
func (p *Payments) GetWallet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
walletInfo, err := p.service.Payments().GetWallet(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
if errs.Is(err, billing.ErrNoWallet) {
p.serveJSONError(w, http.StatusNotFound, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
if err = json.NewEncoder(w).Encode(walletInfo); err != nil {
p.log.Error("failed to encode wallet info", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// ClaimWallet will claim a new wallet address. Returns with existing if it's already claimed.
func (p *Payments) ClaimWallet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
walletInfo, err := p.service.Payments().ClaimWallet(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
if err = json.NewEncoder(w).Encode(walletInfo); err != nil {
p.log.Error("failed to encode wallet info", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// WalletPayments returns with the list of storjscan transactions for user`s wallet.
func (p *Payments) WalletPayments(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
walletPayments, err := p.service.Payments().WalletPayments(ctx)
if err != nil {
if console.ErrUnauthorized.Has(err) {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
if err = json.NewEncoder(w).Encode(walletPayments); err != nil {
p.log.Error("failed to encode payments", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// GetProjectUsagePriceModel returns the project usage price model for the user.
func (p *Payments) GetProjectUsagePriceModel(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
user, err := console.GetUser(ctx)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
pricing := p.service.Payments().GetProjectUsagePriceModel(string(user.UserAgent))
if err = json.NewEncoder(w).Encode(pricing); err != nil {
p.log.Error("failed to encode project usage price model", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// PurchasePackage purchases one of the configured paymentsconfig.PackagePlans.
func (p *Payments) PurchasePackage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
p.serveJSONError(w, http.StatusBadRequest, err)
return
}
token := string(bodyBytes)
u, err := console.GetUser(ctx)
if err != nil {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
pkg, err := p.packagePlans.Get(u.UserAgent)
if err != nil {
p.serveJSONError(w, http.StatusNotFound, err)
return
}
card, err := p.service.Payments().AddCreditCard(ctx, token)
if err != nil {
switch {
case console.ErrUnauthorized.Has(err):
p.serveJSONError(w, http.StatusUnauthorized, err)
default:
p.serveJSONError(w, http.StatusInternalServerError, err)
}
return
}
description := fmt.Sprintf("%s package plan", string(u.UserAgent))
err = p.service.Payments().UpdatePackage(ctx, description, time.Now())
if err != nil {
if !console.ErrAlreadyHasPackage.Has(err) {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
}
err = p.service.Payments().Purchase(ctx, pkg.Price, description, card.ID)
if err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
if err = p.service.Payments().ApplyCredit(ctx, pkg.Credit, description); err != nil {
p.serveJSONError(w, http.StatusInternalServerError, err)
return
}
}
// PackageAvailable returns whether a package plan is configured for the user's partner.
func (p *Payments) PackageAvailable(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
u, err := console.GetUser(ctx)
if err != nil {
p.serveJSONError(w, http.StatusUnauthorized, err)
return
}
pkg, err := p.packagePlans.Get(u.UserAgent)
hasPkg := err == nil && pkg != payments.PackagePlan{}
if err = json.NewEncoder(w).Encode(hasPkg); err != nil {
p.log.Error("failed to encode package plan checking response", zap.Error(ErrPaymentsAPI.Wrap(err)))
}
}
// serveJSONError writes JSON error to response output stream.
func (p *Payments) serveJSONError(w http.ResponseWriter, status int, err error) {
web.ServeJSONError(p.log, w, status, err)
}