7a2be3e6f6
This change causes rate limiting errors to be returned to the client as JSON objects rather than plain text to prevent the satellite UI from encountering issues when trying to parse them. Resolves storj/customer-issues#88 Change-Id: I11abd19068927a22f1c28d18fc99e7dad8461834
396 lines
10 KiB
Go
396 lines
10 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package consoleapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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/stripecoinpayments"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// NewPayments is a constructor for api payments controller.
|
|
func NewPayments(log *zap.Logger, service *console.Service) *Payments {
|
|
return &Payments{
|
|
log: log,
|
|
service: service,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
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
|
|
}
|
|
|
|
err = json.NewEncoder(w).Encode(charges)
|
|
if err != nil {
|
|
p.log.Error("failed to write json response", zap.Error(ErrPaymentsAPI.Wrap(err)))
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
// limit the size of the body to prevent excessive memory usage
|
|
bodyBytes, err := io.ReadAll(io.LimitReader(r.Body, 1*1024*1024))
|
|
if err != nil {
|
|
p.serveJSONError(w, http.StatusInternalServerError, err)
|
|
return
|
|
}
|
|
couponCode := string(bodyBytes)
|
|
|
|
coupon, err := p.service.Payments().ApplyCouponCode(ctx, couponCode)
|
|
if err != nil {
|
|
if stripecoinpayments.ErrInvalidCoupon.Has(err) {
|
|
p.serveJSONError(w, http.StatusBadRequest, 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)))
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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)))
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|