Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Group traffic #50

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
70 changes: 35 additions & 35 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,100 +64,100 @@ returned. If they don't exist in the DB, an account will be created on the Free

* Requires valid JWT: `true`
* Returns:
- 200 JSON object
- 200 JSON object
```json
{
"tier": 1
}
```
- 401 (missing JWT)
- 424 (when there is no such user, and we fail to create it)
- 500 (on any other error)
- 401 (missing JWT)
- 424 (when there is no such user, and we fail to create it)
- 500 (on any other error)

### PUT `/user` (TODO)

This endpoint allows us to update the user's tier, membership expiration dates, etc.

* Requires valid JWT: `true`
* POST params:
- TBD
- TBD
* Returns:
- 200 JSON object
- 200 JSON object
```json
{
"tier": 1
}
```
- 400
- 401 (missing JWT)
- 500
- 400
- 401 (missing JWT)
- 500

### GET `/user/uploads`

Returns a list of all skylinks uploaded by the user.

* Requires valid JWT: `true`
* Returns:
- 200 JSON Array (TBD)
- 401 (missing JWT)
- 424 (when there is no such user, and we fail to create it)
- 500 (on any other error)
- 200 JSON Array (TBD)
- 401 (missing JWT)
- 424 (when there is no such user, and we fail to create it)
- 500 (on any other error)

### GET `/user/downloads`

Returns a list of all skylinks downloads by the user.

* Requires valid JWT: `true`
* Returns:
- 200 JSON Array (TBD)
- 401 (missing JWT)
- 424 (when there is no such user, and we fail to create it)
- 500 (on any other error)
- 200 JSON Array (TBD)
- 401 (missing JWT)
- 424 (when there is no such user, and we fail to create it)
- 500 (on any other error)

## Reports endpoints

### POST `/track/upload/:skylink`

* Requires valid JWT: `true`
* GET params:
- skylink: just the skylink hash, no path, no protocol
- skylink: just the skylink hash, no path, no protocol
* POST params: none
* Returns:
- 204
- 400
- 401 (missing JWT)
- 500
- 204
- 400
- 401 (missing JWT)
- 500

### POST `/track/download/:skylink`

* Requires valid JWT: `true`
* GET params:
- skylink: just the skylink hash, no path, no protocol
- skylink: just the skylink hash, no path, no protocol
* POST params: none
* Returns:
- 204
- 400
- 401 (missing JWT)
- 500
- 204
- 400
- 401 (missing JWT)
- 500

### POST `/track/registry/read`

* Requires valid JWT: `true`
* GET params: none
* POST params: none
* Returns:
- 204
- 400
- 401 (missing JWT)
- 500
- 204
- 400
- 401 (missing JWT)
- 500

### POST `/track/registry/write`

* Requires valid JWT: `true`
* GET params: none
* POST params: none
* Returns:
- 204
- 400
- 401 (missing JWT)
- 500
- 204
- 400
- 401 (missing JWT)
- 500
119 changes: 113 additions & 6 deletions api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import (
"io/ioutil"
"net/http"
"net/url"
"reflect"
"sort"
"strconv"
"strings"
"time"

"github.com/NebulousLabs/skynet-accounts/database"
Expand Down Expand Up @@ -203,6 +206,61 @@ func (api *API) userPutHandler(w http.ResponseWriter, req *http.Request, _ httpr
api.WriteJSON(w, u)
}

// userTopReferrersHandler returns a breakdown of the traffic caused by the top
// referrers for this user.
func (api *API) userTopReferrersHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
sub, _, _, err := jwt.TokenFromContext(req.Context())
if err != nil {
api.WriteError(w, err, http.StatusUnauthorized)
return
}
u, err := api.staticDB.UserBySub(req.Context(), sub, true)
if err != nil {
api.WriteError(w, err, http.StatusInternalServerError)
return
}
if err = req.ParseForm(); err != nil {
api.WriteError(w, err, http.StatusBadRequest)
return
}
page, _ := strconv.Atoi(req.Form.Get("page"))
if page < 1 {
page = 1
}
pageSize, _ := strconv.Atoi(req.Form.Get("pageSize"))
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
sortBy := req.Form.Get("sortBy")
if sortBy == "" {
sortBy = "uploadSize"
}
sortAsc := strings.ToLower(req.Form.Get("sortAsc")) == "true"

traffic, err := api.staticDB.UserTrafficByReferrer(req.Context(), *u, time.Now().Add(-1*time.Hour*24*30*100))
if err != nil {
api.WriteError(w, err, 500)
return
}
start := (page - 1) * pageSize
end := (page) * pageSize
l := len(traffic)
if start > l {
// The requested page doesn't exist, so there's no need to sort the data.
api.WriteJSON(w, []*database.TrafficDTO{})
return
}
if end > l {
end = l
}
err = sortTrafficBy(traffic, sortBy, sortAsc)
if err != nil {
api.WriteError(w, err, http.StatusBadRequest)
return
}
api.WriteJSON(w, traffic[start:end])
}

// userUploadsHandler returns all uploads made by the current user.
func (api *API) userUploadsHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
sub, _, _, err := jwt.TokenFromContext(req.Context())
Expand All @@ -225,7 +283,7 @@ func (api *API) userUploadsHandler(w http.ResponseWriter, req *http.Request, _ h
api.WriteError(w, err, http.StatusBadRequest)
return
}
ups, total, err := api.staticDB.UploadsByUser(req.Context(), *u, offset, pageSize)
ups, total, err := api.staticDB.UploadsByUser(req.Context(), *u, offset, pageSize, req.Form.Get("referrer"))
if err != nil {
api.WriteError(w, err, http.StatusInternalServerError)
return
Expand Down Expand Up @@ -291,7 +349,7 @@ func (api *API) userDownloadsHandler(w http.ResponseWriter, req *http.Request, _
api.WriteError(w, err, http.StatusBadRequest)
return
}
downs, total, err := api.staticDB.DownloadsByUser(req.Context(), *u, offset, pageSize)
downs, total, err := api.staticDB.DownloadsByUser(req.Context(), *u, offset, pageSize, req.Form.Get("referrer"))
if err != nil {
api.WriteError(w, err, http.StatusInternalServerError)
return
Expand Down Expand Up @@ -331,7 +389,7 @@ func (api *API) trackUploadHandler(w http.ResponseWriter, req *http.Request, ps
api.WriteError(w, err, http.StatusInternalServerError)
return
}
_, err = api.staticDB.UploadCreate(req.Context(), *u, *skylink)
_, err = api.staticDB.UploadCreate(req.Context(), *u, *skylink, req.Referer())
if err != nil {
api.WriteError(w, err, http.StatusInternalServerError)
return
Expand Down Expand Up @@ -398,7 +456,7 @@ func (api *API) trackDownloadHandler(w http.ResponseWriter, req *http.Request, p
api.WriteError(w, err, http.StatusInternalServerError)
return
}
err = api.staticDB.DownloadCreate(req.Context(), *u, *skylink, downloadedBytes)
err = api.staticDB.DownloadCreate(req.Context(), *u, *skylink, downloadedBytes, req.Referer())
if err != nil {
api.WriteError(w, err, http.StatusInternalServerError)
return
Expand Down Expand Up @@ -429,7 +487,7 @@ func (api *API) trackRegistryReadHandler(w http.ResponseWriter, req *http.Reques
api.WriteError(w, err, http.StatusInternalServerError)
return
}
_, err = api.staticDB.RegistryReadCreate(req.Context(), *u)
_, err = api.staticDB.RegistryReadCreate(req.Context(), *u, req.Referer())
if err != nil {
api.WriteError(w, err, http.StatusInternalServerError)
return
Expand All @@ -449,7 +507,7 @@ func (api *API) trackRegistryWriteHandler(w http.ResponseWriter, req *http.Reque
api.WriteError(w, err, http.StatusInternalServerError)
return
}
_, err = api.staticDB.RegistryWriteCreate(req.Context(), *u)
_, err = api.staticDB.RegistryWriteCreate(req.Context(), *u, req.Referer())
if err != nil {
api.WriteError(w, err, http.StatusInternalServerError)
return
Expand Down Expand Up @@ -538,3 +596,52 @@ func fetchPageSize(form url.Values) (int, error) {
}
return pageSize, nil
}

// sortTrafficBy sorts a slice of `database.TrafficDTO`s by a specific metric.
// The metric is specified by its JSON representation and it is taken from the
// `Total` field of each element in the slice. This allows the client to sort
// the data by a tag that makes sense to them.
func sortTrafficBy(t []*database.TrafficDTO, jsonTag string, sortAsc bool) (err error) {
if len(t) == 0 {
return
}
defer func() {
if panicErr := recover(); panicErr != nil {
err = panicErr.(error)
}
}()
field, err := fieldNameByJsonTag(*t[0].Total, jsonTag)
if err != nil {
return
}
sort.Slice(t, func(i, j int) bool {
a := reflect.ValueOf(*t[i].Total).FieldByName(field)
b := reflect.ValueOf(*t[j].Total).FieldByName(field)
if sortAsc {
return a.Int() < b.Int()
}
return a.Int() > b.Int()
})
return
}

// fieldNameByJsonTag inspects the given struct and returns the name of the
// field that has the given json tag.
func fieldNameByJsonTag(a interface{}, jsonTag string) (string, error) {
if a == nil {
return "", errors.New("nil value passed")
}
jsonTag = strings.ToLower(jsonTag)
rt := reflect.TypeOf(a)
if rt.Kind() != reflect.Struct {
return "", errors.New("type is not a struct")
}
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
v := strings.Split(field.Tag.Get("json"), ",")[0]
if strings.ToLower(v) == jsonTag {
return field.Name, nil
}
}
return "", errors.New("field not found")
}
15 changes: 8 additions & 7 deletions api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,24 @@ func (api *API) buildHTTPRoutes() {
api.staticRouter.POST("/login", api.noValidate(api.loginHandler))
api.staticRouter.POST("/logout", api.validate(api.logoutHandler))

api.staticRouter.POST("/track/upload/:skylink", api.validate(api.trackUploadHandler))
api.staticRouter.DELETE("/skylink/:skylink", api.validate(api.skylinkDeleteHandler))

api.staticRouter.GET("/stripe/prices", api.noValidate(api.stripePricesHandler))
api.staticRouter.POST("/stripe/webhook", api.noValidate(api.stripeWebhookHandler))

api.staticRouter.POST("/track/download/:skylink", api.validate(api.trackDownloadHandler))
api.staticRouter.POST("/track/upload/:skylink", api.validate(api.trackUploadHandler))
api.staticRouter.POST("/track/registry/read", api.validate(api.trackRegistryReadHandler))
api.staticRouter.POST("/track/registry/write", api.validate(api.trackRegistryWriteHandler))

api.staticRouter.GET("/user", api.validate(api.userHandler))
api.staticRouter.PUT("/user", api.validate(api.userPutHandler))
api.staticRouter.GET("/user/downloads", api.validate(api.userDownloadsHandler))
api.staticRouter.GET("/user/limits", api.noValidate(api.userLimitsHandler))
api.staticRouter.GET("/user/stats", api.validate(api.userStatsHandler))
api.staticRouter.GET("/user/uploads", api.validate(api.userUploadsHandler))
api.staticRouter.GET("/user/top_referrers", api.validate(api.userTopReferrersHandler))
api.staticRouter.DELETE("/user/uploads/:uploadId", api.validate(api.userUploadDeleteHandler))
api.staticRouter.GET("/user/downloads", api.validate(api.userDownloadsHandler))

api.staticRouter.DELETE("/skylink/:skylink", api.validate(api.skylinkDeleteHandler))

api.staticRouter.POST("/stripe/webhook", api.noValidate(api.stripeWebhookHandler))
api.staticRouter.GET("/stripe/prices", api.noValidate(api.stripePricesHandler))
}

// noValidate is a pass-through method used for decorating the request and
Expand Down
Loading