rest-api: endpoint reworkings

Added documentation.
Replaced PUT request with POST request.
Added inline param support for PATCH request.
Replaced unix timestamps handling with RFC-3339 timestampts handling.
Added 'Bearer' method requirement for Authorization header.

Change-Id: I4faa3864051dd18826c2c583ada53666d4aaec44
This commit is contained in:
Vitalii 2022-04-27 14:52:02 +03:00 committed by Vitalii Shpital
parent 894b7b1cf3
commit 96411ba56a
7 changed files with 242 additions and 45 deletions

View File

@ -113,7 +113,12 @@ func (a *API) generateGo() ([]byte, error) {
}
p(")")
p("")
}
p("const dateLayout = \"2006-01-02T15:04:05.000Z\"")
p("")
for _, group := range a.EndpointGroups {
p("var Err%sAPI = errs.Class(\"%s %s api\")", cases.Title(language.Und).String(group.Prefix), a.PackageName, group.Prefix)
p("")
@ -199,14 +204,12 @@ func (a *API) generateGo() ([]byte, error) {
p("")
continue
case reflect.TypeOf(time.Time{}):
p("%sStamp, err := strconv.ParseInt(r.URL.Query().Get(\"%s\"), 10, 64)", param.Name, param.Name)
p("%s, err := time.Parse(dateLayout, r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
p("%s := time.Unix(%sStamp, 0).UTC()", param.Name, param.Name)
p("")
continue
case reflect.TypeOf(""):
p("%s := r.URL.Query().Get(\"%s\")", param.Name, param.Name)
@ -218,19 +221,17 @@ func (a *API) generateGo() ([]byte, error) {
continue
}
}
case http.MethodPut:
for _, param := range endpoint.Params {
p("%s := &%s{}", param.Name, param.Type)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
case http.MethodPatch:
for _, param := range endpoint.Params {
if param.Type == reflect.TypeOf(uuid.UUID{}) {
p("%s, err := uuid.FromString(r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("%sParam, ok := mux.Vars(r)[\"%s\"]", param.Name, param.Name)
p("if !ok {")
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"missing %s route param\"))", param.Name)
p("return")
p("}")
p("")
p("%s, err := uuid.FromString(%sParam)", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
@ -245,6 +246,15 @@ func (a *API) generateGo() ([]byte, error) {
p("")
}
}
case http.MethodPost:
for _, param := range endpoint.Params {
p("%s := &%s{}", param.Name, param.Type)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
}
methodFormat := "retVal, httpErr := h.service.%s(ctx, "
@ -253,10 +263,6 @@ func (a *API) generateGo() ([]byte, error) {
for _, methodParam := range endpoint.Params {
methodFormat += methodParam.Name + ", "
}
case http.MethodPut:
for _, methodParam := range endpoint.Params {
methodFormat += "*" + methodParam.Name + ", "
}
case http.MethodPatch:
for _, methodParam := range endpoint.Params {
if methodParam.Type == reflect.TypeOf(uuid.UUID{}) {
@ -265,6 +271,10 @@ func (a *API) generateGo() ([]byte, error) {
methodFormat += "*" + methodParam.Name + ", "
}
}
case http.MethodPost:
for _, methodParam := range endpoint.Params {
methodFormat += "*" + methodParam.Name + ", "
}
}
methodFormat += ")"

View File

@ -53,9 +53,9 @@ func (eg *EndpointGroup) Patch(path string, endpoint *Endpoint) {
eg.addEndpoint(path, http.MethodPatch, endpoint)
}
// Put adds new PUT endpoint to endpoints group.
func (eg *EndpointGroup) Put(path string, endpoint *Endpoint) {
eg.addEndpoint(path, http.MethodPut, endpoint)
// Post adds new POST endpoint to endpoints group.
func (eg *EndpointGroup) Post(path string, endpoint *Endpoint) {
eg.addEndpoint(path, http.MethodPost, endpoint)
}
// addEndpoint adds new endpoint to endpoints list.

View File

@ -7,7 +7,6 @@ import (
"context"
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/gorilla/mux"
@ -20,14 +19,16 @@ import (
"storj.io/storj/satellite/console"
)
const dateLayout = "2006-01-02T15:04:05.000Z"
var ErrProjectsAPI = errs.Class("consoleapi projects api")
type ProjectManagementService interface {
GenCreateProject(context.Context, console.ProjectInfo) (*console.Project, api.HTTPError)
GenUpdateProject(context.Context, uuid.UUID, console.ProjectInfo) (*console.Project, api.HTTPError)
GenGetUsersProjects(context.Context) ([]console.Project, api.HTTPError)
GenGetSingleBucketUsageRollup(context.Context, uuid.UUID, string, time.Time, time.Time) (*accounting.BucketUsageRollup, api.HTTPError)
GenGetBucketUsageRollups(context.Context, uuid.UUID, time.Time, time.Time) ([]accounting.BucketUsageRollup, api.HTTPError)
GenCreateProject(context.Context, console.ProjectInfo) (*console.Project, api.HTTPError)
GenUpdateProject(context.Context, uuid.UUID, console.ProjectInfo) (*console.Project, api.HTTPError)
}
// Handler is an api handler that exposes all projects related functionality.
@ -48,8 +49,8 @@ func NewProjectManagement(log *zap.Logger, service ProjectManagementService, rou
projectsRouter.HandleFunc("/", handler.handleGenGetUsersProjects).Methods("GET")
projectsRouter.HandleFunc("/bucket-rollup", handler.handleGenGetSingleBucketUsageRollup).Methods("GET")
projectsRouter.HandleFunc("/bucket-rollups", handler.handleGenGetBucketUsageRollups).Methods("GET")
projectsRouter.HandleFunc("/create", handler.handleGenCreateProject).Methods("PUT")
projectsRouter.HandleFunc("/update", handler.handleGenUpdateProject).Methods("PATCH")
projectsRouter.HandleFunc("/create", handler.handleGenCreateProject).Methods("POST")
projectsRouter.HandleFunc("/update/{id}", handler.handleGenUpdateProject).Methods("PATCH")
return handler
}
@ -73,22 +74,18 @@ func (h *Handler) handleGenGetBucketUsageRollups(w http.ResponseWriter, r *http.
return
}
sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
since, err := time.Parse(dateLayout, r.URL.Query().Get("since"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
since := time.Unix(sinceStamp, 0).UTC()
beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("before"), 10, 64)
before, err := time.Parse(dateLayout, r.URL.Query().Get("before"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
before := time.Unix(beforeStamp, 0).UTC()
retVal, httpErr := h.service.GenGetBucketUsageRollups(ctx, projectID, since, before)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
@ -145,7 +142,13 @@ func (h *Handler) handleGenUpdateProject(w http.ResponseWriter, r *http.Request)
return
}
id, err := uuid.FromString(r.URL.Query().Get("id"))
idParam, ok := mux.Vars(r)["id"]
if !ok {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
@ -219,22 +222,18 @@ func (h *Handler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *
return
}
sinceStamp, err := strconv.ParseInt(r.URL.Query().Get("since"), 10, 64)
since, err := time.Parse(dateLayout, r.URL.Query().Get("since"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
since := time.Unix(sinceStamp, 0).UTC()
beforeStamp, err := strconv.ParseInt(r.URL.Query().Get("before"), 10, 64)
before, err := time.Parse(dateLayout, r.URL.Query().Get("before"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
before := time.Unix(beforeStamp, 0).UTC()
retVal, httpErr := h.service.GenGetSingleBucketUsageRollup(ctx, projectID, bucket, since, before)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)

View File

@ -0,0 +1,184 @@
# Generated REST API design
Requires setting 'Authorization' header for requests.
## Successful responses
All the requests a non-empty body for the resource that we're interacting with.
Example:
```json
{
"project": {
"name": "My Awesome Project",
"description": "it is perfect"
}
}
```
## Error responses
When an API endpoint returns an error (status code 4XX) it contains a JSON error response with 1 error field:
Example:
```json
{
"error": "authorization key format is incorrect. Should be 'Bearer <key>'"
}
```
### Project Management API Endpoints
Example of request
```bash
curl -i -L \
-H "Accept: application/json" \
-H 'Authorization: Bearer <key>' \
-X GET \
"https://satellite.qa.storj.io/api/v0/projects/"
```
#### GET projects/
Gets users projects.
!!!WARNING!!! Project ID is used as encryption salt. Please don't send it to anyone. We're going to fix it soon.
A successful response body:
```json
[
{
"id":"cd8d64bd-7457-4661-b88d-2e257bd0d88a",
"name":"My First Project",
"description":"",
"partnerId":"00000000-0000-0000-0000-000000000000",
"userAgent":null,
"ownerId":"f0ef7918-c8f0-4a9c-94fe-2260fb2a7877",
"rateLimit":null,
"burstLimit":null,
"maxBuckets":null,
"createdAt":"2022-04-15T11:38:36.951306+03:00",
"memberCount":0,
"storageLimit":"150.00 GB",
"bandwidthLimit":"150.00 GB",
"segmentLimit":150000
}
]
```
#### GET /bucket-rollup/projectID={uuid string}&bucket={string}&since={Date Timestamp like '2006-01-02T15:00:00Z'}&before={Date Timestamp like '2006-01-02T15:00:00Z'}
Gets project's single bucket usage by bucket ID.
!!!WARNING!!! Project ID is used as encryption salt. Please don't send it to anyone. We're going to fix it soon.
A successful response body:
```json
{
"projectID":"f4f2688e-8dae-4401-8ff1-31d9154ba514",
"bucketName":"bucket",
"totalStoredData":0.011384611174500622,
"totalSegments":0.21667078722222222,
"objectCount":0.10833539361111111,
"metadataSize":1.2241899478055554e-8,
"repairEgress":0,
"getEgress":0,
"auditEgress":0,
"since":"2006-01-02T15:00:00Z",
"before":"2022-04-27T23:59:59Z"
}
```
#### GET /bucket-rollups/projectID={uuid string}&since={Date Timestamp like '2006-01-02T15:00:00Z'}&before={Date Timestamp like '2006-01-02T15:00:00Z'}
Gets project's all buckets usage.
!!!WARNING!!! Project ID is used as encryption salt. Please don't send it to anyone. We're going to fix it soon.
A successful response body:
```json
[
{
"projectID":"f4f2688e-8dae-4401-8ff1-31d9154ba514",
"bucketName":"bucket",
"totalStoredData":0.011384611174500622,
"totalSegments":0.21667078722222222,
"objectCount":0.10833539361111111,
"metadataSize":1.2241899478055554e-8,
"repairEgress":0,
"getEgress":0,
"auditEgress":0,
"since":"2006-01-02T15:00:00Z",
"before":"2022-04-27T23:59:59Z"
}
]
```
#### POST /create
Creates new Project with given info.
!!!WARNING!!! Project ID is used as encryption salt. Please don't send it to anyone. We're going to fix it soon.
Request body example:
```json
{
"name": "new project"
}
```
A successful response body:
```json
{
"id":"f4f2688e-8dae-4401-8ff1-31d9154ba514",
"name":"new project",
"description":"",
"partnerId":"00000000-0000-0000-0000-000000000000",
"userAgent":null,
"ownerId":"f0ef7918-c8f0-4a9c-94fe-2260fb2a7877",
"rateLimit":null,
"burstLimit":null,
"maxBuckets":null,
"createdAt":"2022-04-27T13:23:59.013381+03:00",
"memberCount":0,
"storageLimit":"15 GB",
"bandwidthLimit":"15 GB",
"segmentLimit":15000
}
```
#### PATCH /update/{uuid string}
Updates project with given info.
!!!WARNING!!! Project ID is used as encryption salt. Please don't send it to anyone. We're going to fix it soon.
Request body example:
```json
{
"name": "awesome project",
"description": "random stuff",
"bandwidthLimit": 1000000000,
"storageLimit": 1000000000
}
```
A successful response body:
```json
{
"id":"f4f2688e-8dae-4401-8ff1-31d9154ba514",
"name":"awesome project",
"description":"random stuff",
"partnerId":"00000000-0000-0000-0000-000000000000",
"userAgent":null,
"ownerId":"f0ef7918-c8f0-4a9c-94fe-2260fb2a7877",
"rateLimit":null,
"burstLimit":null,
"maxBuckets":null,
"createdAt":"2022-04-27T13:23:59.013381+03:00",
"memberCount":0,
"storageLimit":"1 GB",
"bandwidthLimit":"1 GB",
"segmentLimit":15000
}
```

View File

@ -57,7 +57,7 @@ func main() {
},
})
g.Put("/create", &apigen.Endpoint{
g.Post("/create", &apigen.Endpoint{
Name: "Create new Project",
Description: "Creates new Project with given info",
MethodName: "GenCreateProject",
@ -67,7 +67,7 @@ func main() {
},
})
g.Patch("/update", &apigen.Endpoint{
g.Patch("/update/{id}", &apigen.Endpoint{
Name: "Update Project",
Description: "Updates project with given info",
MethodName: "GenUpdateProject",

View File

@ -104,8 +104,8 @@ type Project struct {
type ProjectInfo struct {
Name string `json:"name"`
Description string `json:"description"`
StorageLimit memory.Size `json:"project specific storage limit"`
BandwidthLimit memory.Size `json:"project specific bandwidth limit"`
StorageLimit memory.Size `json:"storageLimit"`
BandwidthLimit memory.Size `json:"bandwidthLimit"`
CreatedAt time.Time `json:"createdAt"`
}

View File

@ -10,6 +10,7 @@ import (
"net/http"
"net/mail"
"sort"
"strings"
"time"
"github.com/spacemonkeygo/monkit/v3"
@ -2214,11 +2215,14 @@ func (s *Service) cookieAuth(ctx context.Context, r *http.Request) (context.Cont
// keyAuth checks if request has an authorization api key.
func (s *Service) keyAuth(ctx context.Context, r *http.Request) (context.Context, error) {
apikey := r.Header.Get("Authorization")
if apikey == "" {
return nil, errs.New("no authorization key was provided")
authToken := r.Header.Get("Authorization")
split := strings.Split(authToken, "Bearer ")
if len(split) != 2 {
return nil, errs.New("authorization key format is incorrect. Should be 'Bearer <key>'")
}
apikey := split[1]
ctx = consoleauth.WithAPIKey(ctx, []byte(apikey))
userID, exp, err := s.restKeys.GetUserAndExpirationFromKey(ctx, apikey)