Skip to content

Commit

Permalink
Merge branch 'main' into fix/SPV-000-regression-tests-workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubmkowalski authored Sep 12, 2024
2 parents edcf14d + ba50ac3 commit 5c4e45b
Show file tree
Hide file tree
Showing 171 changed files with 11,540 additions and 5,616 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Get Golang for builder
FROM golang:1.23.0 as builder
FROM golang:1.23.1 as builder

# Set the working directory
WORKDIR /go/src/github.com/bitcoin-sv/spv-wallet
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@ auth:
admin_key: xpub661MyMwAqRbcFgfmdkPgE2m5UjHXu9dj124DbaGLSjaqVESTWfCD4VuNmEbVPkbYLCkykwVZvmA8Pbf8884TQr1FgdG2nPoHR8aB36YdDQh
require_signing: false
scheme: xpub
signing_disabled: true
```
To override admin_key in auth config, use the path with "_" as a path delimiter and SPVWALLET\_ as prefix. So:
Expand Down
73 changes: 73 additions & 0 deletions actions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
## 1. How to define an endpoint for standard/registered user?

Consider following example:

```golang
func RegisterRoutes(handlersManager *routes.Manager) {
group := handlersManager.Group(routes.GroupAPI, "/utxo")
group.GET("", handlers.AsUser(get))
}
```

- There is a concept of `RegisterRoutes` func (which is defined for all action's packages, e.g. transactions, utxo, admin, ...)
- The `RegisterRoutes` function has one required argument `handlersManager *routes.Manager` which is necessary to properly register endpoints and groups.
- To create a new gin group based on proper prefix and stack of middlewares, use the `handlersManager.Group` method, with desired "group type" and relative path.
- There are following "group types" available:
- `GroupRoot` - with no prefix and only "global" middlewares:
- `logging.GinMiddleware(&httpLogger)`
- `gin.Recovery()`
- `middleware.AppContextMiddleware`
- `middleware.CorsMiddleware`
- `metrics.requestMetricsMiddleware`
- `GroupOldAPI` with `/<api_version>` prefix and `auth_middleware` (besides global middlewares)
- `GroupAPI` with `/api/<api_version> prefix and also `auth_middleware` (besides global middlewares)
- `GroupTransactionCallback` with no prefix but with special `middleware.CallbackTokenMiddleware`
- So... for a new endpoint you'll most probably choose the `GroupAPI`
- For "registered" user enpoints you should wrap your handler with `handlers.AsUser(yourHandler)`
- Additionally, the handler func should have following arguments

```golang
func someHandler(c *gin.Context, userContext *reqctx.UserContext)
```

- Use the `userContext` object to get XPubID of current request.
- You can also retrieve the raw XPub by calling `xpub, err := userContext.ShouldGet()`
- Keep in mind that this is only possible when authorization is done via `xPub` (not via an `access key`).
- For access key authorization, this will return an error.
- Additionally, you cannot confuse "user" handlers with others - because of unique set of arguments.

## 2. How to define an endpoint for admin

- There is no additional middleware for admin (The `auth_middleware` takes care of both `admins` and `"regular" users`)
- To define a new "admin" endpoint add `handlers.AsAdmin(youAdminHandler)`, as in following example:

```golang
someGroup.POST("/access-keys/count", handlers.AsAdmin(accessKeysCount))
```

- There is no special "admin" group needed.
- But admin handlers have unique argument lists:

```golang
func someAdminHandler(c *gin.Context, _ *reqctx.AdminContext)
```

- `_ *reqctx.AdminContext` doesn't pass any information but helps to distinguish "admin" handlers from other (e.g. "user" handlers or root handlers, like "status")

## 3. How to get "global" objects in a handler

- The objects are stored in a current request context
- To get them, use those helper methods:

```golang
func someHandler(c *gin.Context) {
engine := reqctx.Engine(c)
logger := reqctx.Logger(c)
config := reqctx.AppConfig(c)
}
```

## 4. Where the RegisterRoutes functions are called?

- The `RegisterRoutes` functions (one for each action group) are called in `/actions/register.go` file.
- If you want to create a new action group you should add the newly created `RegisterRoutes` function into the `Register` function in that file.
9 changes: 3 additions & 6 deletions actions/access_keys/access_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/bitcoin-sv/spv-wallet/config"
"github.com/bitcoin-sv/spv-wallet/server/handlers"
"github.com/bitcoin-sv/spv-wallet/tests"
"github.com/stretchr/testify/suite"
)
Expand All @@ -26,12 +27,8 @@ func (ts *TestSuite) TearDownSuite() {
// SetupTest runs before each test
func (ts *TestSuite) SetupTest() {
ts.BaseSetupTest()

oldRoutes := OldAccessKeysHandler(ts.AppConfig, ts.Services)
oldRoutes.RegisterOldAPIEndpoints(ts.Router.Group("/" + config.APIVersion))

routes := NewHandler(ts.AppConfig, ts.Services)
routes.RegisterAPIEndpoints(ts.Router.Group("/api/" + config.APIVersion))
handlersManager := handlers.NewManager(ts.Router, config.APIVersion)
RegisterRoutes(handlersManager)
}

// TearDownTest runs after each test
Expand Down
21 changes: 10 additions & 11 deletions actions/access_keys/count.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,38 @@ import (
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/mappings"
"github.com/bitcoin-sv/spv-wallet/models/filter"
"github.com/bitcoin-sv/spv-wallet/server/auth"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
)

// count will fetch a count of access keys filtered by metadata
// Count of access keys godoc
// @Summary Count of access keys
// @Description Count of access keys
// @Summary Count of access keys - Use (GET) /api/v1/users/current/keys instead.
// @Description This endpoint has been deprecated. Use (GET) /api/v1/users/current/keys instead.
// @Tags Access-key
// @Produce json
// @Param CountAccessKeys body filter.CountAccessKeys false "Enables filtering of elements to be counted"
// @Success 200 {number} int64 "Count of access keys"
// @Failure 400 "Bad request - Error while parsing CountAccessKeys from request body"
// @Failure 500 "Internal Server Error - Error while fetching count of access keys"
// @Router /v1/access-key/count [post]
// @DeprecatedRouter /v1/access-key/count [post]
// @Security x-auth-xpub
func (a *Action) count(c *gin.Context) {
reqXPubID := c.GetString(auth.ParamXPubHashKey)

func count(c *gin.Context, userContext *reqctx.UserContext) {
logger := reqctx.Logger(c)
var reqParams filter.CountAccessKeys
if err := c.Bind(&reqParams); err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, a.Services.Logger)
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, logger)
return
}

count, err := a.Services.SpvWalletEngine.GetAccessKeysByXPubIDCount(
count, err := reqctx.Engine(c).GetAccessKeysByXPubIDCount(
c.Request.Context(),
reqXPubID,
userContext.GetXPubID(),
mappings.MapToMetadata(reqParams.Metadata),
reqParams.Conditions.ToDbConditions(),
)
if err != nil {
spverrors.ErrorResponse(c, err, a.Services.Logger)
spverrors.ErrorResponse(c, err, logger)
return
}

Expand Down
35 changes: 22 additions & 13 deletions actions/access_keys/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/bitcoin-sv/spv-wallet/engine"
"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/mappings"
"github.com/bitcoin-sv/spv-wallet/server/auth"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
)

Expand All @@ -22,8 +22,13 @@ import (
// @Failure 500 "Internal server error - Error while creating new access key"
// @DeprecatedRouter /v1/access-key [post]
// @Security x-auth-xpub
func (a *Action) oldCreate(c *gin.Context) {
a.createHelper(c, true)
func oldCreate(c *gin.Context, userContext *reqctx.UserContext) {
xpub, err := userContext.ShouldGetXPub()
if err != nil {
spverrors.AbortWithErrorResponse(c, err, reqctx.Logger(c))
return
}
createHelper(c, true, xpub)
}

// create will make a new model using the services defined in the action object
Expand All @@ -33,32 +38,36 @@ func (a *Action) oldCreate(c *gin.Context) {
// @Tags Access-key
// @Produce json
// @Param CreateAccessKey body CreateAccessKey true " "
// @Success 201 {object} models.AccessKey "Created AccessKey"
// @Success 201 {object} response.AccessKey "Created AccessKey"
// @Failure 400 "Bad request - Error while parsing CreateAccessKey from request body"
// @Failure 500 "Internal server error - Error while creating new access key"
// @Router /api/v1/users/current/keys [post]
// @Security x-auth-xpub
func (a *Action) create(c *gin.Context) {
a.createHelper(c, false)
func create(c *gin.Context, userContext *reqctx.UserContext) {
xpub, err := userContext.ShouldGetXPub()
if err != nil {
spverrors.AbortWithErrorResponse(c, err, reqctx.Logger(c))
return
}
createHelper(c, false, xpub)
}

func (a *Action) createHelper(c *gin.Context, snakeCase bool) {
reqXPub := c.GetString(auth.ParamXPubKey)

func createHelper(c *gin.Context, snakeCase bool, xpub string) {
logger := reqctx.Logger(c)
var requestBody CreateAccessKey
if err := c.Bind(&requestBody); err != nil {
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, a.Services.Logger)
spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, logger)
return
}

// Create a new accessKey
accessKey, err := a.Services.SpvWalletEngine.NewAccessKey(
accessKey, err := reqctx.Engine(c).NewAccessKey(
c.Request.Context(),
reqXPub,
xpub,
engine.WithMetadatas(requestBody.Metadata),
)
if err != nil {
spverrors.ErrorResponse(c, err, a.Services.Logger)
spverrors.ErrorResponse(c, err, logger)
return
}

Expand Down
26 changes: 14 additions & 12 deletions actions/access_keys/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (

"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/mappings"
"github.com/bitcoin-sv/spv-wallet/server/auth"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
)

Expand All @@ -22,9 +22,10 @@ import (
// @Failure 500 "Internal server error - Error while getting access key"
// @DeprecatedRouter /v1/access-key [get]
// @Security x-auth-xpub
func (a *Action) oldGet(c *gin.Context) {
func oldGet(c *gin.Context, userContext *reqctx.UserContext) {
id := c.Query("id")
a.getHelper(c, id, true)

getHelper(c, id, true, userContext.GetXPubID())
}

// get will get an existing model
Expand All @@ -34,36 +35,37 @@ func (a *Action) oldGet(c *gin.Context) {
// @Tags Access-key
// @Produce json
// @Param id path string true "id of the access key"
// @Success 200 {object} models.AccessKey "AccessKey with given id"
// @Success 200 {object} response.AccessKey "AccessKey with given id"
// @Failure 400 "Bad request - Missing required field: id"
// @Failure 403 "Forbidden - Access key is not owned by the user"
// @Failure 500 "Internal server error - Error while getting access key"
// @Router /api/v1/users/current/keys/{id} [get]
// @Security x-auth-xpub
func (a *Action) get(c *gin.Context) {
func get(c *gin.Context, userContext *reqctx.UserContext) {
id := c.Params.ByName("id")
a.getHelper(c, id, false)

getHelper(c, id, false, userContext.GetXPubID())
}

func (a *Action) getHelper(c *gin.Context, id string, snakeCase bool) {
reqXPubID := c.GetString(auth.ParamXPubHashKey)
func getHelper(c *gin.Context, id string, snakeCase bool, reqXPubID string) {
logger := reqctx.Logger(c)

if id == "" {
spverrors.ErrorResponse(c, spverrors.ErrMissingFieldID, a.Services.Logger)
spverrors.ErrorResponse(c, spverrors.ErrMissingFieldID, logger)
return
}

// Get access key
accessKey, err := a.Services.SpvWalletEngine.GetAccessKey(
accessKey, err := reqctx.Engine(c).GetAccessKey(
c.Request.Context(), reqXPubID, id,
)
if err != nil {
spverrors.ErrorResponse(c, err, a.Services.Logger)
spverrors.ErrorResponse(c, err, logger)
return
}

if accessKey.XpubID != reqXPubID {
spverrors.ErrorResponse(c, spverrors.ErrAuthorization, a.Services.Logger)
spverrors.ErrorResponse(c, spverrors.ErrAuthorization, logger)
return
}

Expand Down
35 changes: 23 additions & 12 deletions actions/access_keys/revoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (

"github.com/bitcoin-sv/spv-wallet/engine/spverrors"
"github.com/bitcoin-sv/spv-wallet/mappings"
"github.com/bitcoin-sv/spv-wallet/server/auth"
"github.com/bitcoin-sv/spv-wallet/server/reqctx"
"github.com/gin-gonic/gin"
)

Expand All @@ -21,9 +21,15 @@ import (
// @Failure 500 "Internal server error - Error while revoking access key"
// @DeprecatedRouter /v1/access-key [delete]
// @Security x-auth-xpub
func (a *Action) oldRevoke(c *gin.Context) {
func oldRevoke(c *gin.Context, userContext *reqctx.UserContext) {
id := c.Query("id")
a.revokeHelper(c, id, true)
xpub, err := userContext.ShouldGetXPub()
if err != nil {
spverrors.AbortWithErrorResponse(c, err, reqctx.Logger(c))
return
}

revokeHelper(c, id, true, xpub)
}

// revoke will revoke the intended model by id
Expand All @@ -33,31 +39,36 @@ func (a *Action) oldRevoke(c *gin.Context) {
// @Tags Access-key
// @Produce json
// @Param id path string true "id of the access key"
// @Success 200 {object} models.AccessKey "Revoked AccessKey"
// @Success 200 {object} response.AccessKey "Revoked AccessKey"
// @Failure 400 "Bad request - Missing required field: id"
// @Failure 500 "Internal server error - Error while revoking access key"
// @Router /api/v1/users/current/keys/{id} [delete]
// @Security x-auth-xpub
func (a *Action) revoke(c *gin.Context) {
func revoke(c *gin.Context, userContext *reqctx.UserContext) {
id := c.Params.ByName("id")
a.revokeHelper(c, id, false)
xpub, err := userContext.ShouldGetXPub()
if err != nil {
spverrors.AbortWithErrorResponse(c, err, reqctx.Logger(c))
return
}
revokeHelper(c, id, false, xpub)
}

func (a *Action) revokeHelper(c *gin.Context, id string, snakeCase bool) {
reqXPub := c.GetString(auth.ParamXPubKey)
func revokeHelper(c *gin.Context, id string, snakeCase bool, xpub string) {
logger := reqctx.Logger(c)

if id == "" {
spverrors.ErrorResponse(c, spverrors.ErrMissingFieldID, a.Services.Logger)
spverrors.ErrorResponse(c, spverrors.ErrMissingFieldID, logger)
return
}

accessKey, err := a.Services.SpvWalletEngine.RevokeAccessKey(
accessKey, err := reqctx.Engine(c).RevokeAccessKey(
c.Request.Context(),
reqXPub,
xpub,
id,
)
if err != nil {
spverrors.ErrorResponse(c, err, a.Services.Logger)
spverrors.ErrorResponse(c, err, logger)
return
}

Expand Down
Loading

0 comments on commit 5c4e45b

Please sign in to comment.