satellite/admin: add user deletion
Change-Id: Ideea978698183e25f5e0d73128c4f93c2caa1577
This commit is contained in:
parent
0a32ba0e6b
commit
133fda4bef
@ -22,6 +22,8 @@ import (
|
||||
"storj.io/storj/private/version/checker"
|
||||
"storj.io/storj/satellite/admin"
|
||||
"storj.io/storj/satellite/metainfo"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
)
|
||||
|
||||
// Admin is the satellite core process that runs chores
|
||||
@ -46,6 +48,12 @@ type Admin struct {
|
||||
Service *checker.Service
|
||||
}
|
||||
|
||||
Payments struct {
|
||||
Accounts payments.Accounts
|
||||
Service *stripecoinpayments.Service
|
||||
Stripe stripecoinpayments.StripeClient
|
||||
}
|
||||
|
||||
Admin struct {
|
||||
Listener net.Listener
|
||||
Server *admin.Server
|
||||
@ -104,6 +112,42 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
})
|
||||
}
|
||||
|
||||
{ // setup payments
|
||||
pc := config.Payments
|
||||
|
||||
var stripeClient stripecoinpayments.StripeClient
|
||||
var err error
|
||||
switch pc.Provider {
|
||||
default:
|
||||
stripeClient = stripecoinpayments.NewStripeMock()
|
||||
case "stripecoinpayments":
|
||||
stripeClient = stripecoinpayments.NewStripeClient(log, pc.StripeCoinPayments)
|
||||
}
|
||||
|
||||
peer.Payments.Service, err = stripecoinpayments.NewService(
|
||||
peer.Log.Named("payments.stripe:service"),
|
||||
stripeClient,
|
||||
pc.StripeCoinPayments,
|
||||
peer.DB.StripeCoinPayments(),
|
||||
peer.DB.Console().Projects(),
|
||||
peer.DB.ProjectAccounting(),
|
||||
pc.StorageTBPrice,
|
||||
pc.EgressTBPrice,
|
||||
pc.ObjectPrice,
|
||||
pc.BonusRate,
|
||||
pc.CouponValue,
|
||||
pc.CouponDuration,
|
||||
pc.CouponProjectLimit,
|
||||
pc.MinCoinPayment)
|
||||
|
||||
if err != nil {
|
||||
return nil, errs.Combine(err, peer.Close())
|
||||
}
|
||||
|
||||
peer.Payments.Stripe = stripeClient
|
||||
peer.Payments.Accounts = peer.Payments.Service.Accounts()
|
||||
}
|
||||
|
||||
{ // setup debug
|
||||
var err error
|
||||
peer.Admin.Listener, err = net.Listen("tcp", config.Admin.Address)
|
||||
@ -114,7 +158,7 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
adminConfig := config.Admin
|
||||
adminConfig.AuthorizationToken = config.Console.AuthToken
|
||||
|
||||
peer.Admin.Server = admin.NewServer(log.Named("admin"), peer.Admin.Listener, peer.DB, adminConfig)
|
||||
peer.Admin.Server = admin.NewServer(log.Named("admin"), peer.Admin.Listener, peer.DB, peer.Payments.Accounts.Invoices(), adminConfig)
|
||||
peer.Servers.Add(lifecycle.Item{
|
||||
Name: "admin",
|
||||
Run: peer.Admin.Server.Run,
|
||||
|
@ -80,6 +80,10 @@ A successful response body:
|
||||
}
|
||||
```
|
||||
|
||||
## DELETE /api/user/{user-email}
|
||||
|
||||
Deletes the user.
|
||||
|
||||
## POST /api/coupon
|
||||
|
||||
Adds a coupon for specific user.
|
||||
|
@ -19,6 +19,7 @@ import (
|
||||
"storj.io/storj/satellite/accounting"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/metainfo"
|
||||
"storj.io/storj/satellite/payments"
|
||||
"storj.io/storj/satellite/payments/stripecoinpayments"
|
||||
)
|
||||
|
||||
@ -50,17 +51,21 @@ type Server struct {
|
||||
mux *mux.Router
|
||||
|
||||
db DB
|
||||
invoices payments.Invoices
|
||||
}
|
||||
|
||||
// NewServer returns a new debug.Server.
|
||||
func NewServer(log *zap.Logger, listener net.Listener, db DB, config Config) *Server {
|
||||
func NewServer(log *zap.Logger, listener net.Listener, db DB, invoices payments.Invoices, config Config) *Server {
|
||||
server := &Server{
|
||||
log: log,
|
||||
|
||||
listener: listener,
|
||||
mux: mux.NewRouter(),
|
||||
|
||||
db: db,
|
||||
invoices: invoices,
|
||||
}
|
||||
|
||||
server.db = db
|
||||
server.listener = listener
|
||||
server.mux = mux.NewRouter()
|
||||
server.server.Handler = &protectedServer{
|
||||
allowedAuthorization: config.AuthorizationToken,
|
||||
next: server.mux,
|
||||
@ -70,6 +75,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, config Config) *Se
|
||||
server.mux.HandleFunc("/api/user", server.addUser).Methods("POST")
|
||||
server.mux.HandleFunc("/api/user/{useremail}", server.updateUser).Methods("PUT")
|
||||
server.mux.HandleFunc("/api/user/{useremail}", server.userInfo).Methods("GET")
|
||||
server.mux.HandleFunc("/api/user/{useremail}", server.deleteUser).Methods("DELETE")
|
||||
server.mux.HandleFunc("/api/coupon", server.addCoupon).Methods("POST")
|
||||
server.mux.HandleFunc("/api/coupon/{couponid}", server.couponInfo).Methods("GET")
|
||||
server.mux.HandleFunc("/api/coupon/{couponid}", server.deleteCoupon).Methods("DELETE")
|
||||
|
@ -231,3 +231,90 @@ func (server *Server) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
vars := mux.Vars(r)
|
||||
userEmail, ok := vars["useremail"]
|
||||
if !ok {
|
||||
http.Error(w, "user-email missing", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := server.db.Console().Users().GetByEmail(ctx, userEmail)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
http.Error(w, fmt.Sprintf("user with email %q not found", userEmail), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to get user %q: %v", userEmail, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure user has no own projects any longer
|
||||
projects, err := server.db.Console().Projects().GetByUserID(ctx, user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to list buckets: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(projects) > 0 {
|
||||
http.Error(w, fmt.Sprintf("some projects still exist: %v", projects), http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete memberships in foreign projects
|
||||
members, err := server.db.Console().ProjectMembers().GetByMemberID(ctx, user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to search for user project memberships: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(members) > 0 {
|
||||
for _, project := range members {
|
||||
err := server.db.Console().ProjectMembers().Delete(ctx, user.ID, project.ProjectID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to delete user project membership: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure no unpaid invoices exist.
|
||||
invoices, err := server.invoices.List(ctx, user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to list user invoices: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(invoices) > 0 {
|
||||
for _, invoice := range invoices {
|
||||
if invoice.Status != "paid" {
|
||||
http.Error(w, "user has unpaid/pending invoices", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasItems, err := server.invoices.CheckPendingItems(ctx, user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to list pending invoice items: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if hasItems {
|
||||
http.Error(w, "user has pending invoice items", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo := &console.User{
|
||||
ID: user.ID,
|
||||
FullName: "",
|
||||
ShortName: "",
|
||||
Email: fmt.Sprintf("deactivated+%s@storj.io", user.ID.String()),
|
||||
Status: console.Deleted,
|
||||
}
|
||||
|
||||
err = server.db.Console().Users().Update(ctx, userInfo)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to delete user: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -141,3 +141,49 @@ func TestUpdateUser(t *testing.T) {
|
||||
require.Equal(t, user.Status, updatedUser.Status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteUser(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1,
|
||||
StorageNodeCount: 0,
|
||||
UplinkCount: 1,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Admin.Address = "127.0.0.1:0"
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
address := planet.Satellites[0].Admin.Admin.Listener.Addr()
|
||||
user, err := planet.Satellites[0].DB.Console().Users().GetByEmail(ctx, planet.Uplinks[0].Projects[0].Owner.Email)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Deleting the user should fail, as project exists
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/user/%s", user.Email), nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "very-secret-token")
|
||||
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusConflict, response.StatusCode)
|
||||
responseBody, err := ioutil.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, response.Body.Close())
|
||||
require.Greater(t, len(responseBody), 0)
|
||||
|
||||
err = planet.Satellites[0].DB.Console().Projects().Delete(ctx, planet.Uplinks[0].Projects[0].ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Deleting the user should pass, as no project exists for given user
|
||||
req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/user/%s", user.Email), nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "very-secret-token")
|
||||
|
||||
response, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, response.StatusCode)
|
||||
responseBody, err = ioutil.ReadAll(response.Body)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, response.Body.Close())
|
||||
require.Equal(t, len(responseBody), 0)
|
||||
})
|
||||
}
|
@ -16,6 +16,8 @@ import (
|
||||
type Invoices interface {
|
||||
// List returns a list of invoices for a given payment account.
|
||||
List(ctx context.Context, userID uuid.UUID) ([]Invoice, error)
|
||||
// CheckPendingItems returns if pending invoice items for a given payment account exist.
|
||||
CheckPendingItems(ctx context.Context, userID uuid.UUID) (existingItems bool, err error)
|
||||
}
|
||||
|
||||
// Invoice holds all public information about invoice.
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/stripe/stripe-go/client"
|
||||
"github.com/stripe/stripe-go/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/invoice"
|
||||
"github.com/stripe/stripe-go/invoiceitem"
|
||||
"github.com/stripe/stripe-go/paymentmethod"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@ -48,6 +49,7 @@ type StripeInvoices interface {
|
||||
// StripeInvoiceItems Stripe InvoiceItems interface.
|
||||
type StripeInvoiceItems interface {
|
||||
New(params *stripe.InvoiceItemParams) (*stripe.InvoiceItem, error)
|
||||
List(listParams *stripe.InvoiceItemListParams) *invoiceitem.Iter
|
||||
}
|
||||
|
||||
// StripeCharges Stripe Charges interface.
|
||||
|
@ -62,3 +62,32 @@ func (invoices *invoices) List(ctx context.Context, userID uuid.UUID) (invoicesL
|
||||
|
||||
return invoicesList, nil
|
||||
}
|
||||
|
||||
// CheckPendingItems returns if pending invoice items for a given payment account exist.
|
||||
func (invoices *invoices) CheckPendingItems(ctx context.Context, userID uuid.UUID) (existingItems bool, err error) {
|
||||
defer mon.Task()(&ctx, userID)(&err)
|
||||
|
||||
customerID, err := invoices.service.db.Customers().GetCustomerID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, Error.Wrap(err)
|
||||
}
|
||||
|
||||
params := &stripe.InvoiceItemListParams{
|
||||
Customer: &customerID,
|
||||
Pending: stripe.Bool(true),
|
||||
}
|
||||
|
||||
itemIterator := invoices.service.stripeClient.InvoiceItems().List(params)
|
||||
for itemIterator.Next() {
|
||||
item := itemIterator.InvoiceItem()
|
||||
if item != nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err = itemIterator.Err(); err != nil {
|
||||
return false, Error.Wrap(err)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/stripe/stripe-go/customerbalancetransaction"
|
||||
"github.com/stripe/stripe-go/form"
|
||||
"github.com/stripe/stripe-go/invoice"
|
||||
"github.com/stripe/stripe-go/invoiceitem"
|
||||
"github.com/stripe/stripe-go/paymentmethod"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
@ -177,6 +178,10 @@ func (m *mockInvoiceItems) New(params *stripe.InvoiceItemParams) (*stripe.Invoic
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockInvoiceItems) List(listParams *stripe.InvoiceItemListParams) *invoiceitem.Iter {
|
||||
return &invoiceitem.Iter{Iter: &stripe.Iter{}}
|
||||
}
|
||||
|
||||
type mockCustomerBalanceTransactions struct {
|
||||
transactions map[string][]*stripe.CustomerBalanceTransaction
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user