diff --git a/satellite/admin/README.md b/satellite/admin/README.md index 62a852e80..6b12a3d9d 100644 --- a/satellite/admin/README.md +++ b/satellite/admin/README.md @@ -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. diff --git a/satellite/admin/coupon_test.go b/satellite/admin/coupon_test.go index 97010f084..53f77b59b 100644 --- a/satellite/admin/coupon_test.go +++ b/satellite/admin/coupon_test.go @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/satellite/admin/project.go b/satellite/admin/project.go index 4185f9b08..19d16175b 100644 --- a/satellite/admin/project.go +++ b/satellite/admin/project.go @@ -4,6 +4,7 @@ package admin import ( + "context" "database/sql" "encoding/json" "errors" @@ -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() @@ -378,56 +404,10 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) { return } - // 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()) - if err != nil { - httpJSONError(w, "unable to list project usage", - err.Error(), http.StatusInternalServerError) + // if usage exist, return error to client and exit + if server.checkUsage(ctx, w, projectUUID) { return } - if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.ObjectCount > 0 { - httpJSONError(w, "usage for current month exists", - "", http.StatusConflict) - return - } - - // 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)) - if err != nil { - httpJSONError(w, "error getting project totals", - "", http.StatusInternalServerError) - return - } - - 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)) - switch err { - case stripecoinpayments.ErrProjectRecordExists: - record, err := server.db.StripeCoinPayments().ProjectRecords().Get(ctx, projectUUID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour)) - if err != nil { - httpJSONError(w, "unable to get project records", - err.Error(), http.StatusInternalServerError) - return - } - // state = 0 means unapplied and not invoiced yet. - if record.State == 0 { - httpJSONError(w, "unapplied project invoice record exist", - "", http.StatusConflict) - return - } - case nil: - httpJSONError(w, "usage for last month exist, but is not billed yet", - "", http.StatusConflict) - return - default: - httpJSONError(w, "unable to get project records", - err.Error(), http.StatusInternalServerError) - return - } - } err = server.db.Console().Projects().Delete(ctx, projectUUID) if err != nil { @@ -437,6 +417,61 @@ func (server *Server) deleteProject(w http.ResponseWriter, r *http.Request) { } } +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, projectID, firstOfMonth, time.Now()) + if err != nil { + httpJSONError(w, "unable to list project usage", + err.Error(), http.StatusInternalServerError) + return true + } + if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.ObjectCount > 0 { + httpJSONError(w, "usage for current month exists", + "", http.StatusConflict) + return true + } + + // if usage of last month exist, make sure to look for billing records + 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 true + } + + if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.ObjectCount > 0 { + 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, 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 true + } + // state = 0 means unapplied and not invoiced yet. + if record.State == 0 { + httpJSONError(w, "unapplied project invoice record exist", + "", http.StatusConflict) + return true + } + case nil: + httpJSONError(w, "usage for last month exist, but is not billed yet", + "", http.StatusConflict) + return true + default: + httpJSONError(w, "unable to get project records", + err.Error(), http.StatusInternalServerError) + return true + } + } + + return false +} + func bucketNames(buckets []storj.Bucket) []string { var xs []string for _, b := range buckets { diff --git a/satellite/admin/project_test.go b/satellite/admin/project_test.go index 96cea55be..0640b82ea 100644 --- a/satellite/admin/project_test.go +++ b/satellite/admin/project_test.go @@ -44,14 +44,14 @@ func TestAPI(t *testing.T) { link := "http://" + address.String() + "/api/project/" + project.ID.String() + "/limit" t.Run("GetProject", func(t *testing.T) { - assertGet(t, link, `{"usage":{"amount":"0 B","bytes":0},"bandwidth":{"amount":"0 B","bytes":0},"rate":{"rps":0},"maxBuckets":0}`) + assertGet(t, link, `{"usage":{"amount":"0 B","bytes":0},"bandwidth":{"amount":"0 B","bytes":0},"rate":{"rps":0},"maxBuckets":0}`, planet.Satellites[0].Config.Console.AuthToken) }) t.Run("UpdateUsage", func(t *testing.T) { data := url.Values{"usage": []string{"1TiB"}} req, err := http.NewRequest(http.MethodPost, link, strings.NewReader(data.Encode())) require.NoError(t, err) - req.Header.Set("Authorization", "very-secret-token") + req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") response, err := http.DefaultClient.Do(req) @@ -59,56 +59,56 @@ func TestAPI(t *testing.T) { require.Equal(t, http.StatusOK, response.StatusCode) require.NoError(t, response.Body.Close()) - assertGet(t, link, `{"usage":{"amount":"1.0 TiB","bytes":1099511627776},"bandwidth":{"amount":"0 B","bytes":0},"rate":{"rps":0},"maxBuckets":0}`) + assertGet(t, link, `{"usage":{"amount":"1.0 TiB","bytes":1099511627776},"bandwidth":{"amount":"0 B","bytes":0},"rate":{"rps":0},"maxBuckets":0}`, planet.Satellites[0].Config.Console.AuthToken) req, err = http.NewRequest(http.MethodPut, link+"?usage=1GB", 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) require.Equal(t, http.StatusOK, response.StatusCode) require.NoError(t, response.Body.Close()) - assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"0 B","bytes":0},"rate":{"rps":0},"maxBuckets":0}`) + assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"0 B","bytes":0},"rate":{"rps":0},"maxBuckets":0}`, planet.Satellites[0].Config.Console.AuthToken) }) t.Run("UpdateBandwidth", func(t *testing.T) { req, err := http.NewRequest(http.MethodPut, link+"?bandwidth=1MB", 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) require.Equal(t, http.StatusOK, response.StatusCode) require.NoError(t, response.Body.Close()) - assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"1.00 MB","bytes":1000000},"rate":{"rps":0},"maxBuckets":0}`) + assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"1.00 MB","bytes":1000000},"rate":{"rps":0},"maxBuckets":0}`, planet.Satellites[0].Config.Console.AuthToken) }) t.Run("UpdateRate", func(t *testing.T) { req, err := http.NewRequest(http.MethodPut, link+"?rate=100", 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) require.Equal(t, http.StatusOK, response.StatusCode) require.NoError(t, response.Body.Close()) - assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"1.00 MB","bytes":1000000},"rate":{"rps":100},"maxBuckets":0}`) + assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"1.00 MB","bytes":1000000},"rate":{"rps":100},"maxBuckets":0}`, planet.Satellites[0].Config.Console.AuthToken) }) t.Run("UpdateBuckets", func(t *testing.T) { req, err := http.NewRequest(http.MethodPut, link+"?buckets=2000", 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) require.Equal(t, http.StatusOK, response.StatusCode) require.NoError(t, response.Body.Close()) - assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"1.00 MB","bytes":1000000},"rate":{"rps":100},"maxBuckets":2000}`) + assertGet(t, link, `{"usage":{"amount":"1.00 GB","bytes":1000000000},"bandwidth":{"amount":"1.00 MB","bytes":1000000},"rate":{"rps":100},"maxBuckets":2000}`, planet.Satellites[0].Config.Console.AuthToken) }) }) } @@ -130,7 +130,7 @@ func TestAddProject(t *testing.T) { body := strings.NewReader(fmt.Sprintf(`{"ownerId":"%s","projectName":"Test Project"}`, userID.ID.String())) req, err := http.NewRequest(http.MethodPost, "http://"+address.String()+"/api/project", 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) @@ -174,7 +174,7 @@ func TestRenameProject(t *testing.T) { body := strings.NewReader(fmt.Sprintf(`{"projectName":"%s","description":"This project got renamed"}`, newName)) req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("http://"+address.String()+"/api/project/%s", project.ID.String()), 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) @@ -217,7 +217,7 @@ func TestDeleteProject(t *testing.T) { // the deletion with an existing API key should fail req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/project/%s", projectID), 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) @@ -229,7 +229,7 @@ func TestDeleteProject(t *testing.T) { req, err = http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/project/%s", projectID), 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) @@ -242,6 +242,112 @@ func TestDeleteProject(t *testing.T) { }) } +func TestCheckUsageWithoutUsage(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() + projectID := planet.Uplinks[0].Projects[0].ID + + apiKeys, err := planet.Satellites[0].DB.Console().APIKeys().GetPagedByProjectID(ctx, projectID, console.APIKeyCursor{ + Page: 1, + Limit: 2, + Search: "", + }) + require.NoError(t, err) + require.Len(t, apiKeys.APIKeys, 1) + + err = planet.Satellites[0].DB.Console().APIKeys().Delete(ctx, apiKeys.APIKeys[0].ID) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://"+address.String()+"/api/project/%s/usage", projectID), nil) + require.NoError(t, err) + req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) + + response, err := http.DefaultClient.Do(req) + require.NoError(t, err) + responseBody, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + require.Equal(t, "{\"result\":\"no project usage exist\"}", string(responseBody)) + require.NoError(t, response.Body.Close()) + require.Equal(t, http.StatusOK, response.StatusCode) + }) +} + +func TestCheckUsageWithUsage(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() + projectID := planet.Uplinks[0].Projects[0].ID + + apiKeys, err := planet.Satellites[0].DB.Console().APIKeys().GetPagedByProjectID(ctx, projectID, console.APIKeyCursor{ + Page: 1, + Limit: 2, + Search: "", + }) + require.NoError(t, err) + require.Len(t, apiKeys.APIKeys, 1) + + err = planet.Satellites[0].DB.Console().APIKeys().Delete(ctx, apiKeys.APIKeys[0].ID) + require.NoError(t, err) + + accTime := time.Now().UTC().AddDate(0, 0, -1) + tally := accounting.BucketStorageTally{ + BucketName: "test", + ProjectID: projectID, + IntervalStart: accTime, + ObjectCount: 1, + InlineSegmentCount: 1, + RemoteSegmentCount: 1, + InlineBytes: 10, + RemoteBytes: 640000, + MetadataSize: 2, + } + err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally) + require.NoError(t, err) + tally = accounting.BucketStorageTally{ + BucketName: "test", + ProjectID: projectID, + IntervalStart: accTime.AddDate(0, 0, 1), + ObjectCount: 1, + InlineSegmentCount: 1, + RemoteSegmentCount: 1, + InlineBytes: 10, + RemoteBytes: 640000, + MetadataSize: 2, + } + err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://"+address.String()+"/api/project/%s/usage", projectID), nil) + require.NoError(t, err) + req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) + + response, err := http.DefaultClient.Do(req) + require.NoError(t, err) + responseBody, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + require.Equal(t, "{\"error\":\"usage for current month exists\",\"detail\":\"\"}", string(responseBody)) + require.NoError(t, response.Body.Close()) + require.Equal(t, http.StatusConflict, response.StatusCode) + }) +} + func TestDeleteProjectWithUsageCurrentMonth(t *testing.T) { testplanet.Run(t, testplanet.Config{ SatelliteCount: 1, @@ -295,23 +401,9 @@ func TestDeleteProjectWithUsageCurrentMonth(t *testing.T) { err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally) require.NoError(t, err) - inline, remote, err := planet.Satellites[0].DB.ProjectAccounting().GetStorageTotals(ctx, projectID) - require.NoError(t, err) - require.Equal(t, int64(10), inline) - require.Equal(t, int64(640000), remote) - - bw, err := planet.Satellites[0].DB.ProjectAccounting().GetAllocatedBandwidthTotal(ctx, projectID, accTime.AddDate(0, 0, -1)) - require.NoError(t, err) - require.EqualValues(t, 0, bw) - - usage, err := planet.Satellites[0].DB.ProjectAccounting().GetProjectTotal(ctx, projectID, accTime.AddDate(0, 0, -1), accTime.AddDate(0, 0, 2)) - require.NoError(t, err) - require.NotEqual(t, 0, usage.Egress) - require.NotEqual(t, float64(0), usage.Storage) - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/project/%s", projectID), 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) @@ -379,23 +471,9 @@ func TestDeleteProjectWithUsagePreviousMonth(t *testing.T) { err = planet.Satellites[0].DB.ProjectAccounting().CreateStorageTally(ctx, tally) require.NoError(t, err) - inline, remote, err := planet.Satellites[0].DB.ProjectAccounting().GetStorageTotals(ctx, projectID) - require.NoError(t, err) - require.Equal(t, int64(10), inline) - require.Equal(t, int64(640000), remote) - - bw, err := planet.Satellites[0].DB.ProjectAccounting().GetAllocatedBandwidthTotal(ctx, projectID, accTime.AddDate(0, 0, -1)) - require.NoError(t, err) - require.EqualValues(t, 0, bw) - - usage, err := planet.Satellites[0].DB.ProjectAccounting().GetProjectTotal(ctx, projectID, accTime.AddDate(0, 0, -1), accTime.AddDate(0, 0, 2)) - require.NoError(t, err) - require.NotEqual(t, 0, usage.Egress) - require.NotEqual(t, float64(0), usage.Storage) - req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("http://"+address.String()+"/api/project/%s", projectID), 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) @@ -407,13 +485,13 @@ func TestDeleteProjectWithUsagePreviousMonth(t *testing.T) { }) } -func assertGet(t *testing.T, link string, expected string) { +func assertGet(t *testing.T, link string, expected string, authToken string) { t.Helper() req, err := http.NewRequest(http.MethodGet, link, nil) require.NoError(t, err) - req.Header.Set("Authorization", "very-secret-token") + req.Header.Set("Authorization", authToken) response, err := http.DefaultClient.Do(req) require.NoError(t, err) diff --git a/satellite/admin/server.go b/satellite/admin/server.go index 43a2e22b5..6e3120d77 100644 --- a/satellite/admin/server.go +++ b/satellite/admin/server.go @@ -42,7 +42,7 @@ type DB interface { Buckets() metainfo.BucketsDB } -// Server provides endpoints for debugging. +// Server provides endpoints for administrative tasks. type Server struct { log *zap.Logger @@ -54,7 +54,7 @@ type Server struct { payments payments.Accounts } -// NewServer returns a new debug.Server. +// NewServer returns a new administration Server. func NewServer(log *zap.Logger, listener net.Listener, db DB, accounts payments.Accounts, config Config) *Server { server := &Server{ log: log, @@ -79,6 +79,7 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, accounts payments. 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") + server.mux.HandleFunc("/api/project/{project}/usage", server.checkProjectUsage).Methods("GET") server.mux.HandleFunc("/api/project/{project}/limit", server.getProjectLimit).Methods("GET") server.mux.HandleFunc("/api/project/{project}/limit", server.putProjectLimit).Methods("PUT", "POST") server.mux.HandleFunc("/api/project/{project}", server.renameProject).Methods("PUT") @@ -96,7 +97,8 @@ type protectedServer struct { func (server *protectedServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if server.allowedAuthorization == "" { - http.Error(w, "Authorization not enabled.", http.StatusForbidden) + httpJSONError(w, "Authorization not enabled.", + "", http.StatusForbidden) return } @@ -105,7 +107,8 @@ func (server *protectedServer) ServeHTTP(w http.ResponseWriter, r *http.Request) []byte(server.allowedAuthorization), ) if equality != 1 { - http.Error(w, "Forbidden", http.StatusForbidden) + httpJSONError(w, "Forbidden", + "", http.StatusForbidden) return } @@ -114,7 +117,7 @@ func (server *protectedServer) ServeHTTP(w http.ResponseWriter, r *http.Request) server.next.ServeHTTP(w, r) } -// Run starts the debug endpoint. +// Run starts the admin endpoint. func (server *Server) Run(ctx context.Context) error { if server.listener == nil { return nil diff --git a/satellite/admin/server_test.go b/satellite/admin/server_test.go index 7042e787c..cf16c40eb 100644 --- a/satellite/admin/server_test.go +++ b/satellite/admin/server_test.go @@ -52,7 +52,7 @@ func TestBasic(t *testing.T) { t.Run("WithAccess", func(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://"+address.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) diff --git a/satellite/admin/user.go b/satellite/admin/user.go index 9e41ea003..fd562f7a7 100644 --- a/satellite/admin/user.go +++ b/satellite/admin/user.go @@ -40,10 +40,12 @@ func (server *Server) addUser(w http.ResponseWriter, r *http.Request) { switch { case input.Email == "": - http.Error(w, "Email is not set", http.StatusBadRequest) + httpJSONError(w, "email is not set", + "", http.StatusBadRequest) return case input.Password == "": - http.Error(w, "Password is not set", http.StatusBadRequest) + httpJSONError(w, "password is not set", + "", http.StatusBadRequest) return } @@ -297,12 +299,13 @@ func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) { // Ensure user has no own projects any longer projects, err := server.db.Console().Projects().GetByUserID(ctx, user.ID) if err != nil { - httpJSONError(w, "unable to list buckets", + httpJSONError(w, "unable to list projects", err.Error(), http.StatusInternalServerError) return } if len(projects) > 0 { - http.Error(w, fmt.Sprintf("some projects still exist: %v", projects), http.StatusConflict) + httpJSONError(w, "some projects still exist", + fmt.Sprintf("%v", projects), http.StatusConflict) return } @@ -317,7 +320,8 @@ func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) { 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) + httpJSONError(w, "unable to delete user project membership", + err.Error(), http.StatusInternalServerError) return } } @@ -333,7 +337,8 @@ func (server *Server) deleteUser(w http.ResponseWriter, r *http.Request) { if len(invoices) > 0 { for _, invoice := range invoices { if invoice.Status != "paid" { - http.Error(w, "user has unpaid/pending invoices", http.StatusConflict) + httpJSONError(w, "user has unpaid/pending invoices", + "", http.StatusConflict) return } } diff --git a/satellite/admin/user_test.go b/satellite/admin/user_test.go index 615d496f3..dae82d9b8 100644 --- a/satellite/admin/user_test.go +++ b/satellite/admin/user_test.go @@ -51,7 +51,7 @@ func TestGetUser(t *testing.T) { req, err := http.NewRequest(http.MethodGet, userLink, 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) @@ -83,7 +83,7 @@ func TestAddUser(t *testing.T) { body := strings.NewReader(fmt.Sprintf(`{"email":"%s","fullName":"Alice Test","password":"123a123"}`, email)) req, err := http.NewRequest(http.MethodPost, "http://"+address.String()+"/api/user", 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) @@ -120,7 +120,7 @@ func TestAddUserSameEmail(t *testing.T) { body := strings.NewReader(fmt.Sprintf(`{"email":"%s","fullName":"Alice Test","password":"123a123"}`, email)) req, err := http.NewRequest(http.MethodPost, "http://"+address.String()+"/api/user", 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) @@ -142,7 +142,7 @@ func TestAddUserSameEmail(t *testing.T) { body = strings.NewReader(fmt.Sprintf(`{"email":"%s","fullName":"Alice Test","password":"123a123"}`, email)) req, err = http.NewRequest(http.MethodPost, "http://"+address.String()+"/api/user", 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) @@ -169,7 +169,7 @@ func TestUpdateUser(t *testing.T) { body := strings.NewReader(`{"email":"alice+2@mail.test", "shortName":"Newbie"}`) req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("http://"+address.String()+"/api/user/%s", user.Email), 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) @@ -208,7 +208,7 @@ func TestDeleteUser(t *testing.T) { // 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") + req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) response, err := http.DefaultClient.Do(req) require.NoError(t, err) @@ -224,7 +224,7 @@ func TestDeleteUser(t *testing.T) { // 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") + req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) response, err = http.DefaultClient.Do(req) require.NoError(t, err)