Skip to content

Commit

Permalink
satellite/admin: add check project usage endpoint and fix some leftov…
Browse files Browse the repository at this point in the history
…er http.Error handling

Change-Id: I1ae3e7cb723a553f9c5a3a752beab0a27b0293bc
  • Loading branch information
stefanbenten committed Aug 13, 2020
1 parent 4cdba36 commit c7b86a3
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 83 deletions.
6 changes: 6 additions & 0 deletions satellite/admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ A successful response body:

Deletes the specified coupon.

## GET /api/project/{project-id}/usage

This endpoint returns whether the project has outstanding usage or not.

A project with not usage returns status code 200 and `{"result":"no project usage exist"}`.

## GET /api/project/{project-id}/limit

This endpoint returns information about project limits.
Expand Down
10 changes: 5 additions & 5 deletions satellite/admin/coupon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestAddCoupon(t *testing.T) {
body := strings.NewReader(fmt.Sprintf(`{"userId": "%s", "duration": 2, "amount": 3000, "description": "testcoupon-alice"}`, user.ID))
req, err := http.NewRequest(http.MethodPost, "http://"+address.String()+"/api/coupon", body)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)

response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
Expand Down Expand Up @@ -83,7 +83,7 @@ func TestCouponInfo(t *testing.T) {
body := strings.NewReader(fmt.Sprintf(`{"userId": "%s", "duration": 2, "amount": 3000, "description": "testcoupon-alice"}`, user.ID))
req, err := http.NewRequest(http.MethodPost, "http://"+address.String()+"/api/coupon", body)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)

response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
Expand All @@ -98,7 +98,7 @@ func TestCouponInfo(t *testing.T) {

req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("http://"+address.String()+"/api/coupon/%s", id.String()), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)

response, err = http.DefaultClient.Do(req)
require.NoError(t, err)
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestCouponDelete(t *testing.T) {
body := strings.NewReader(fmt.Sprintf(`{"userId": "%s", "duration": 2, "amount": 3000, "description": "testcoupon-alice"}`, user.ID))
req, err := http.NewRequest(http.MethodPost, "http://"+address.String()+"/api/coupon", body)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)

response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
Expand All @@ -157,7 +157,7 @@ func TestCouponDelete(t *testing.T) {

req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/coupon/%s", id), nil)
require.NoError(t, err)
req.Header.Set("Authorization", "very-secret-token")
req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken)

response, err = http.DefaultClient.Do(req)
require.NoError(t, err)
Expand Down
69 changes: 52 additions & 17 deletions satellite/admin/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package admin

import (
"context"
"database/sql"
"encoding/json"
"errors"
Expand All @@ -23,6 +24,31 @@ import (
"storj.io/storj/satellite/payments/stripecoinpayments"
)

func (server *Server) checkProjectUsage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

vars := mux.Vars(r)
projectUUIDString, ok := vars["project"]
if !ok {
httpJSONError(w, "project-uuid missing",
"", http.StatusBadRequest)
return
}

projectUUID, err := uuid.FromString(projectUUIDString)
if err != nil {
httpJSONError(w, "invalid project-uuid",
err.Error(), http.StatusBadRequest)
return
}

if !server.checkUsage(ctx, w, projectUUID) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
_, _ = w.Write([]byte(`{"result":"no project usage exist"}`))
}
}

func (server *Server) getProjectLimit(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

Expand Down Expand Up @@ -378,63 +404,72 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) {
return
}

// if usage exist, return error to client and exit
if server.checkUsage(ctx, w, projectUUID) {
return
}

err = server.db.Console().Projects().Delete(ctx, projectUUID)
if err != nil {
httpJSONError(w, "unable to delete project",
err.Error(), http.StatusInternalServerError)
return
}
}

func (server *Server) checkUsage(ctx context.Context, w http.ResponseWriter, projectID uuid.UUID) (hasUsage bool) {
// do not delete projects that have usage for the current month.
year, month, _ := time.Now().UTC().Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)

currentUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectUUID, firstOfMonth, time.Now())
currentUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectID, firstOfMonth, time.Now())
if err != nil {
httpJSONError(w, "unable to list project usage",
err.Error(), http.StatusInternalServerError)
return
return true
}
if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.ObjectCount > 0 {
httpJSONError(w, "usage for current month exists",
"", http.StatusConflict)
return
return true
}

// if usage of last month exist, make sure to look for billing records
lastMonthUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectUUID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.AddDate(0, 0, -1))
lastMonthUsage, err := server.db.ProjectAccounting().GetProjectTotal(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.AddDate(0, 0, -1))
if err != nil {
httpJSONError(w, "error getting project totals",
"", http.StatusInternalServerError)
return
return true
}

if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.ObjectCount > 0 {
err := server.db.StripeCoinPayments().ProjectRecords().Check(ctx, projectUUID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour))
err := server.db.StripeCoinPayments().ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour))
switch err {
case stripecoinpayments.ErrProjectRecordExists:
record, err := server.db.StripeCoinPayments().ProjectRecords().Get(ctx, projectUUID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour))
record, err := server.db.StripeCoinPayments().ProjectRecords().Get(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour))
if err != nil {
httpJSONError(w, "unable to get project records",
err.Error(), http.StatusInternalServerError)
return
return true
}
// state = 0 means unapplied and not invoiced yet.
if record.State == 0 {
httpJSONError(w, "unapplied project invoice record exist",
"", http.StatusConflict)
return
return true
}
case nil:
httpJSONError(w, "usage for last month exist, but is not billed yet",
"", http.StatusConflict)
return
return true
default:
httpJSONError(w, "unable to get project records",
err.Error(), http.StatusInternalServerError)
return
return true
}
}

err = server.db.Console().Projects().Delete(ctx, projectUUID)
if err != nil {
httpJSONError(w, "unable to delete project",
err.Error(), http.StatusInternalServerError)
return
}
return false
}

func bucketNames(buckets []storj.Bucket) []string {
Expand Down

0 comments on commit c7b86a3

Please sign in to comment.