Skip to content

Commit

Permalink
feat(admin): add user list/get/remove method (#67)
Browse files Browse the repository at this point in the history
* feat(admin): add user list/get/remove method

* add admin user service to add user get, list and remove method
* rename misspelled file
* add router for user requests
* extend webauthn user persister
* update spec

Closes: #22

* fix(review): fix review findings

* update public spec for transaction list
* cleanup public spec
  * switch tenant_id path entries with reference to component
  * add transaction tag
  * add user_id as path param
* add paging to user list in admin api call
* add paging to admin spec
* rename userid to user_id in transaction list handler

---------

Co-authored-by: Stefan Jacobi <[email protected]>
  • Loading branch information
shentschel and Stefan Jacobi authored Jun 3, 2024
1 parent 49f70ff commit 3a1c27b
Show file tree
Hide file tree
Showing 12 changed files with 729 additions and 41 deletions.
7 changes: 7 additions & 0 deletions server/api/dto/admin/request/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package request

type UserListRequest struct {
PerPage int `query:"per_page"`
Page int `query:"page"`
SortDirection string `query:"sort_direction"`
}
49 changes: 49 additions & 0 deletions server/api/dto/admin/response/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package response

import (
"github.com/gofrs/uuid"
"github.com/teamhanko/passkey-server/api/dto/response"
"github.com/teamhanko/passkey-server/persistence/models"
)

type UserListDto struct {
ID uuid.UUID `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
Icon string `json:"icon"`
DisplayName string `json:"display_name"`
}

func UserListDtoFromModel(user models.WebauthnUser) UserListDto {
return UserListDto{
ID: user.ID,
UserID: user.UserID,
Name: user.Name,
Icon: user.Icon,
DisplayName: user.DisplayName,
}
}

type UserGetDto struct {
UserListDto
Credentials []response.CredentialDto `json:"credentials"`
Transactions []response.TransactionDto `json:"transactions"`
}

func UserGetDtoFromModel(user models.WebauthnUser) UserGetDto {
dto := UserGetDto{
UserListDto: UserListDtoFromModel(user),
Credentials: make([]response.CredentialDto, 0),
Transactions: make([]response.TransactionDto, 0),
}

for _, credential := range user.WebauthnCredentials {
dto.Credentials = append(dto.Credentials, response.CredentialDtoFromModel(credential))
}

for _, transaction := range user.Transactions {
dto.Transactions = append(dto.Transactions, response.TransactionDtoFromModel(transaction))
}

return dto
}
File renamed without changes.
19 changes: 19 additions & 0 deletions server/api/dto/response/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,22 @@ func CredentialDtoFromModel(credential models.WebauthnCredential) CredentialDto
IsMFA: credential.IsMFA,
}
}

type TransactionDto struct {
ID string `json:"id"`
Identifier string `json:"identifier"`
Data string `json:"data"`

CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

func TransactionDtoFromModel(transaction models.Transaction) TransactionDto {
return TransactionDto{
ID: transaction.ID.String(),
Identifier: transaction.Identifier,
Data: transaction.Data,
CreatedAt: transaction.CreatedAt,
UpdatedAt: transaction.UpdatedAt,
}
}
152 changes: 152 additions & 0 deletions server/api/handler/admin/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package admin

import (
"fmt"
"github.com/gobuffalo/pop/v6"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
adminRequest "github.com/teamhanko/passkey-server/api/dto/admin/request"
"github.com/teamhanko/passkey-server/api/helper"
"github.com/teamhanko/passkey-server/api/pagination"
"github.com/teamhanko/passkey-server/api/services/admin"
"github.com/teamhanko/passkey-server/persistence"
"net/http"
"net/url"
"strconv"
"strings"
)

type UserHandler interface {
List(ctx echo.Context) error
Get(ctx echo.Context) error
Remove(ctx echo.Context) error
}

type userHandler struct {
persister persistence.Persister
}

func NewUserHandler(persister persistence.Persister) UserHandler {
return &userHandler{persister: persister}
}

func (uh *userHandler) List(ctx echo.Context) error {
var request adminRequest.UserListRequest
err := (&echo.DefaultBinder{}).BindQueryParams(ctx, &request)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "unable to parse request")
}

if request.Page == 0 {
request.Page = 1
}

if request.PerPage == 0 {
request.PerPage = 20
}

if request.SortDirection == "" {
request.SortDirection = "desc"
}

switch strings.ToLower(request.SortDirection) {
case "desc", "asc":
default:
return echo.NewHTTPError(http.StatusBadRequest, "sort_direction must be desc or asc")
}

h, err := helper.GetHandlerContext(ctx)
if err != nil {
ctx.Logger().Error(err)
return err
}

return uh.persister.GetConnection().Transaction(func(tx *pop.Connection) error {
userPersister := uh.persister.GetWebauthnUserPersister(tx)
userService := admin.NewUserService(admin.CreateUserServiceParams{
Ctx: ctx,
Tenant: *h.Tenant,
UserPersister: userPersister,
})

users, count, err := userService.List(request)
if err != nil {
return err
}

u, _ := url.Parse(fmt.Sprintf("%s://%s%s", ctx.Scheme(), ctx.Request().Host, ctx.Request().RequestURI))

ctx.Response().Header().Set("Link", pagination.CreateHeader(u, count, request.Page, request.PerPage))
ctx.Response().Header().Set("X-Total-Count", strconv.FormatInt(int64(count), 10))

return ctx.JSON(http.StatusOK, users)
})
}

func (uh *userHandler) Get(ctx echo.Context) error {
h, err := helper.GetHandlerContext(ctx)
if err != nil {
ctx.Logger().Error(err)
return err
}

userIdString := ctx.Param("user_id")
if userIdString == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing user_id")
}

userId, err := uuid.FromString(userIdString)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid user_id")
}

return uh.persister.GetConnection().Transaction(func(tx *pop.Connection) error {
userPersister := uh.persister.GetWebauthnUserPersister(tx)
userService := admin.NewUserService(admin.CreateUserServiceParams{
Ctx: ctx,
Tenant: *h.Tenant,
UserPersister: userPersister,
})

user, err := userService.Get(userId)
if err != nil {
return err
}

return ctx.JSON(http.StatusOK, user)
})
}

func (uh *userHandler) Remove(ctx echo.Context) error {
h, err := helper.GetHandlerContext(ctx)
if err != nil {
ctx.Logger().Error(err)
return err
}

userIdString := ctx.Param("user_id")
if userIdString == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing user_id")
}

userId, err := uuid.FromString(userIdString)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid user_id")
}

return uh.persister.GetConnection().Transaction(func(tx *pop.Connection) error {
userPersister := uh.persister.GetWebauthnUserPersister(tx)
userService := admin.NewUserService(admin.CreateUserServiceParams{
Ctx: ctx,
Tenant: *h.Tenant,
UserPersister: userPersister,
})

err := userService.Delete(userId)
if err != nil {
return err
}

return ctx.NoContent(http.StatusNoContent)
})
}
39 changes: 38 additions & 1 deletion server/api/handler/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ import (
"net/http"
)

type TransactionHandler interface {
WebauthnHandler
List(ctx echo.Context) error
}

type transactionHandler struct {
*webauthnHandler
}

func NewTransactionHandler(persister persistence.Persister) WebauthnHandler {
func NewTransactionHandler(persister persistence.Persister) TransactionHandler {
webauthnHandler := newWebAuthnHandler(persister, false)

return &transactionHandler{webauthnHandler}
Expand Down Expand Up @@ -128,6 +133,38 @@ func (t *transactionHandler) Finish(ctx echo.Context) error {
})
}

func (t *transactionHandler) List(ctx echo.Context) error {
userId := ctx.Param("user_id")
userUUID, err := uuid.FromString(userId)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusBadRequest, "invalid user id")
}

h, err := helper.GetHandlerContext(ctx)
if err != nil {
ctx.Logger().Error(err)
return err
}

transactions, err := t.persister.GetTransactionPersister(nil).ListByUserId(userUUID, h.Tenant.ID)
if err != nil {
ctx.Logger().Error(err)
return echo.NewHTTPError(http.StatusInternalServerError, "unable to list transactions").SetInternal(err)
}

if transactions == nil {
return echo.NewHTTPError(http.StatusNotFound, "transactions for user not found")
}

transactionList := make([]response.TransactionDto, 0)
for _, trans := range *transactions {
transactionList = append(transactionList, response.TransactionDtoFromModel(trans))
}

return ctx.JSON(http.StatusOK, transactionList)
}

func (t *transactionHandler) withTransaction(transactionId string, transactionDataJson string) webauthn.LoginOption {
return func(options *protocol.PublicKeyCredentialRequestOptions) {
transaction := []byte(transactionId)
Expand Down
8 changes: 8 additions & 0 deletions server/api/router/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh
singleGroup.DELETE("", tenantHandler.Remove)
singleGroup.PUT("/config", tenantHandler.UpdateConfig)
singleGroup.GET("/audit_logs", tenantHandler.ListAuditLog)

secretHandler := admin.NewSecretsHandler(persister)
apiKeyGroup := singleGroup.Group("/secrets/api")
apiKeyGroup.GET("", secretHandler.ListAPIKeys)
Expand All @@ -74,5 +75,12 @@ func NewAdminRouter(cfg *config.Config, persister persistence.Persister, prometh
jwkKeyGroup.POST("", secretHandler.CreateJWKKey)
jwkKeyGroup.DELETE("/:secret_id", secretHandler.RemoveJWKKey)

userHandler := admin.NewUserHandler(persister)
userGroup := singleGroup.Group("/users")
userGroup.GET("", userHandler.List)

userGroup.GET("/:user_id", userHandler.Get)
userGroup.DELETE("/:user_id", userHandler.Remove)

return main
}
1 change: 1 addition & 0 deletions server/api/router/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func RouteTransaction(parent *echo.Group, persister persistence.Persister) {
transactionHandler := handler.NewTransactionHandler(persister)

group := parent.Group("/transaction", passkeyMiddleware.ApiKeyMiddleware())
group.GET("/:user_id", transactionHandler.List)
group.POST(InitEndpoint, transactionHandler.Init)
group.POST(FinishEndpoint, transactionHandler.Finish)
}
Expand Down
Loading

0 comments on commit 3a1c27b

Please sign in to comment.