From 3a1c27b3157e83e331549af776596172a1d29294 Mon Sep 17 00:00:00 2001 From: Stefan Jacobi Date: Mon, 3 Jun 2024 11:04:05 +0200 Subject: [PATCH] feat(admin): add user list/get/remove method (#67) * 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 --- server/api/dto/admin/request/user.go | 7 + server/api/dto/admin/response/user.go | 49 ++++ .../response/{webatuhn.go => webauthn.go} | 0 server/api/dto/response/responses.go | 19 ++ server/api/handler/admin/user.go | 152 ++++++++++++ server/api/handler/transaction.go | 39 ++- server/api/router/admin.go | 8 + server/api/router/main.go | 1 + server/api/services/admin/user_service.go | 95 +++++++ .../persisters/webauthn_user_persister.go | 61 ++++- spec/passkey-server-admin.yaml | 234 +++++++++++++++++- spec/passkey-server.yaml | 105 ++++++-- 12 files changed, 729 insertions(+), 41 deletions(-) create mode 100644 server/api/dto/admin/request/user.go create mode 100644 server/api/dto/admin/response/user.go rename server/api/dto/admin/response/{webatuhn.go => webauthn.go} (100%) create mode 100644 server/api/handler/admin/user.go create mode 100644 server/api/services/admin/user_service.go diff --git a/server/api/dto/admin/request/user.go b/server/api/dto/admin/request/user.go new file mode 100644 index 0000000..83b2fea --- /dev/null +++ b/server/api/dto/admin/request/user.go @@ -0,0 +1,7 @@ +package request + +type UserListRequest struct { + PerPage int `query:"per_page"` + Page int `query:"page"` + SortDirection string `query:"sort_direction"` +} diff --git a/server/api/dto/admin/response/user.go b/server/api/dto/admin/response/user.go new file mode 100644 index 0000000..62eba6b --- /dev/null +++ b/server/api/dto/admin/response/user.go @@ -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 +} diff --git a/server/api/dto/admin/response/webatuhn.go b/server/api/dto/admin/response/webauthn.go similarity index 100% rename from server/api/dto/admin/response/webatuhn.go rename to server/api/dto/admin/response/webauthn.go diff --git a/server/api/dto/response/responses.go b/server/api/dto/response/responses.go index deac1ff..cff1c0f 100644 --- a/server/api/dto/response/responses.go +++ b/server/api/dto/response/responses.go @@ -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, + } +} diff --git a/server/api/handler/admin/user.go b/server/api/handler/admin/user.go new file mode 100644 index 0000000..04b6519 --- /dev/null +++ b/server/api/handler/admin/user.go @@ -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) + }) +} diff --git a/server/api/handler/transaction.go b/server/api/handler/transaction.go index 9253f9a..dc2caeb 100644 --- a/server/api/handler/transaction.go +++ b/server/api/handler/transaction.go @@ -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} @@ -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) diff --git a/server/api/router/admin.go b/server/api/router/admin.go index a7547dd..c77e25a 100644 --- a/server/api/router/admin.go +++ b/server/api/router/admin.go @@ -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) @@ -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 } diff --git a/server/api/router/main.go b/server/api/router/main.go index e0f1749..865fc7a 100644 --- a/server/api/router/main.go +++ b/server/api/router/main.go @@ -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) } diff --git a/server/api/services/admin/user_service.go b/server/api/services/admin/user_service.go new file mode 100644 index 0000000..4fa668b --- /dev/null +++ b/server/api/services/admin/user_service.go @@ -0,0 +1,95 @@ +package admin + +import ( + "github.com/gofrs/uuid" + "github.com/labstack/echo/v4" + "github.com/teamhanko/passkey-server/api/dto/admin/request" + "github.com/teamhanko/passkey-server/api/dto/admin/response" + "github.com/teamhanko/passkey-server/persistence/models" + "github.com/teamhanko/passkey-server/persistence/persisters" + "net/http" +) + +type UserService interface { + List(request request.UserListRequest) ([]response.UserListDto, int, error) + Get(userId uuid.UUID) (*response.UserGetDto, error) + Delete(userId uuid.UUID) error +} + +type CreateUserServiceParams struct { + Ctx echo.Context + Tenant models.Tenant + + UserPersister persisters.WebauthnUserPersister +} + +type userService struct { + ctx echo.Context + tenant models.Tenant + userPersister persisters.WebauthnUserPersister +} + +func NewUserService(params CreateUserServiceParams) UserService { + return &userService{ + ctx: params.Ctx, + tenant: params.Tenant, + userPersister: params.UserPersister, + } +} + +func (us *userService) List(listRequest request.UserListRequest) ([]response.UserListDto, int, error) { + list := make([]response.UserListDto, 0) + + count, err := us.userPersister.Count(us.tenant.ID) + if err != nil { + us.ctx.Logger().Error(err) + return nil, 0, echo.NewHTTPError(http.StatusInternalServerError, "unable to count users").SetInternal(err) + } + + users, err := us.userPersister.AllForTenant(us.tenant.ID, listRequest.Page, listRequest.PerPage, listRequest.SortDirection) + if err != nil { + us.ctx.Logger().Error(err) + return nil, 0, echo.NewHTTPError(http.StatusInternalServerError, "unable to list users").SetInternal(err) + } + + for _, user := range users { + list = append(list, response.UserListDtoFromModel(user)) + } + + return list, count, nil +} + +func (us *userService) Get(userId uuid.UUID) (*response.UserGetDto, error) { + user, err := us.userPersister.GetById(userId) + if err != nil { + us.ctx.Logger().Error(err) + return nil, echo.NewHTTPError(http.StatusInternalServerError, "unable to get user from db").SetInternal(err) + } + + if user == nil { + return nil, echo.NewHTTPError(http.StatusNotFound, "user not found") + } + + dto := response.UserGetDtoFromModel(*user) + return &dto, nil +} + +func (us *userService) Delete(userId uuid.UUID) error { + user, err := us.userPersister.GetById(userId) + if err != nil { + us.ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError, "unable to get user from db").SetInternal(err) + } + + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + + err = us.userPersister.Delete(user) + if err != nil { + us.ctx.Logger().Error(err) + return echo.NewHTTPError(http.StatusInternalServerError, "unable to delete user from db").SetInternal(err) + } + + return nil +} diff --git a/server/persistence/persisters/webauthn_user_persister.go b/server/persistence/persisters/webauthn_user_persister.go index b044670..7481d11 100644 --- a/server/persistence/persisters/webauthn_user_persister.go +++ b/server/persistence/persisters/webauthn_user_persister.go @@ -12,8 +12,12 @@ import ( type WebauthnUserPersister interface { Create(webauthnUser *models.WebauthnUser) error + AllForTenant(tenantId uuid.UUID, page int, perPage int, sort string) (models.WebauthnUsers, error) + Count(tenantId uuid.UUID) (int, error) + GetById(id uuid.UUID) (*models.WebauthnUser, error) GetByUserId(userId string, tenantId uuid.UUID) (*models.WebauthnUser, error) Update(webauthnUser *models.WebauthnUser) error + Delete(user *models.WebauthnUser) error } type webauthnUserPersister struct { @@ -38,9 +42,42 @@ func (p *webauthnUserPersister) Create(webauthnUser *models.WebauthnUser) error return nil } +func (p *webauthnUserPersister) AllForTenant(tenantId uuid.UUID, page int, perPage int, sort string) (models.WebauthnUsers, error) { + webauthnUsers := models.WebauthnUsers{} + err := p.database. + Where("tenant_id = ?", tenantId). + Order(fmt.Sprintf("webauthn_users.created_at %s", sort)). + Paginate(page, perPage). + All(&webauthnUsers) + + if err != nil && errors.Is(err, sql.ErrNoRows) { + return webauthnUsers, nil + } + + if err != nil { + return webauthnUsers, fmt.Errorf("failed to get webauthn users for tenant %w", err) + } + + return webauthnUsers, nil +} + +func (p *webauthnUserPersister) GetById(id uuid.UUID) (*models.WebauthnUser, error) { + webauthnUser := models.WebauthnUser{} + err := p.database.Eager().Find(&webauthnUser, id) + if err != nil && errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + + if err != nil { + return nil, fmt.Errorf("failed to get webauthn user by id: %w", err) + } + + return &webauthnUser, nil +} + func (p *webauthnUserPersister) GetByUserId(userId string, tenantId uuid.UUID) (*models.WebauthnUser, error) { - weauthnUser := models.WebauthnUser{} - err := p.database.Eager().Where("user_id = ? AND tenant_id = ?", userId, tenantId).First(&weauthnUser) + webauthnUser := models.WebauthnUser{} + err := p.database.Eager().Where("user_id = ? AND tenant_id = ?", userId, tenantId).First(&webauthnUser) if err != nil && errors.Is(err, sql.ErrNoRows) { return nil, nil } @@ -48,7 +85,7 @@ func (p *webauthnUserPersister) GetByUserId(userId string, tenantId uuid.UUID) ( return nil, fmt.Errorf("failed to get webauthn user by user id: %w", err) } - return &weauthnUser, nil + return &webauthnUser, nil } func (p *webauthnUserPersister) Update(webauthnUser *models.WebauthnUser) error { @@ -63,3 +100,21 @@ func (p *webauthnUserPersister) Update(webauthnUser *models.WebauthnUser) error return nil } + +func (p *webauthnUserPersister) Delete(user *models.WebauthnUser) error { + err := p.database.Destroy(user) + if err != nil { + return fmt.Errorf("failed to delete webauthn user: %w", err) + } + + return nil +} + +func (p *webauthnUserPersister) Count(tenantId uuid.UUID) (int, error) { + count, err := p.database.Where("tenant_id = ?", tenantId).Count(&models.WebauthnUser{}) + if err != nil { + return 0, fmt.Errorf("failed to get user count: %w", err) + } + + return count, nil +} diff --git a/spec/passkey-server-admin.yaml b/spec/passkey-server-admin.yaml index 2479417..29328fe 100644 --- a/spec/passkey-server-admin.yaml +++ b/spec/passkey-server-admin.yaml @@ -3,7 +3,7 @@ info: version: '1.2' title: passkey-server-admin summary: Admin API for Passkey Server - description: 'ADmin API for Hanko Passkey Server. Allows creation and configiration of tenants and api keys, ' + description: 'Admin API for Hanko Passkey Server. Allows creation and configiration of tenants and api keys, ' termsOfService: 'https://www.hanko.io/terms' contact: name: Hanko Dev Team @@ -422,6 +422,60 @@ paths: default: localhost path_prefix: default: '' + '/tenants/{tenant_id}/users': + get: + summary: List Users + description: Lists all webauthn users for a given tenant. + operationId: get-tenants-tenant_id-users + parameters: + - name: page + in: query + description: Page of user list + schema: + type: number + default: 1 + - name: per_page + in: query + description: How many entries should be shown per page + schema: + type: number + default: 20 + - name: sort_direction + in: query + description: Sort entries ascending or descending + schema: + type: string + enum: + - asc + - desc + - name: tenant_id + in: path + description: ID of the tenant for which the users will be listed. + required: true + schema: + type: string + responses: + '200': + description: List of webauthn users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/webauthn_user' + '400': + $ref: '#/components/responses/error' + '404': + $ref: '#/components/responses/error' + '500': + $ref: '#/components/responses/error' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + default: localhost + path_prefix: + default: '' /health/alive: get: summary: Get alive status @@ -470,6 +524,91 @@ paths: default: localhost path_prefix: default: '' + '/tenants/{tenant_id}/users/{user_id}': + get: + summary: Get single user + description: Get a detailed user object for a single user. + operationId: get-tenants-tenant_id-users-user_id + parameters: + - name: tenant_id + in: path + description: ID of the tenant + required: true + schema: + type: string + - name: user_id + in: path + description: ID of the user. + required: true + schema: + type: string + responses: + '200': + description: Detailed user object for a single user. + content: + application/json: + schema: + allOf: + - $ref: '#/components/schemas/webauthn_user' + - type: object + properties: + credentials: + type: array + items: + $ref: '#/components/schemas/credential' + transactions: + type: array + items: + $ref: '#/components/schemas/transaction' + required: + - credentials + - transactions + '400': + $ref: '#/components/responses/error' + '404': + $ref: '#/components/responses/error' + '500': + $ref: '#/components/responses/error' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + default: localhost + path_prefix: + default: '' + delete: + summary: Remove single user + description: Removes a single webauthn user + operationId: delete-tenants-tenant_id-users-user_id + parameters: + - name: tenant_id + in: path + description: ID of the tenant + required: true + schema: + type: string + - name: user_id + in: path + description: ID of the user. + required: true + schema: + type: string + responses: + '204': + description: No Content + '400': + $ref: '#/components/responses/error' + '404': + $ref: '#/components/responses/error' + '500': + $ref: '#/components/responses/error' + servers: + - url: 'http://{host}:8001/{path_prefix}' + variables: + host: + default: localhost + path_prefix: + default: '' tags: - name: admin api description: Hanko Passkey Server Admin API @@ -485,7 +624,7 @@ components: format: uuid minLength: 36 maxLength: 36 - examples: + example: - 1f496bcd-49da-4839-a02f-7ce681ccb488 secret_id: name: secret_id @@ -529,6 +668,7 @@ components: $ref: '#/components/schemas/config' responses: error: + description: Error Response with detailed information content: application/json: @@ -539,13 +679,13 @@ components: type: - string - 'null' - examples: + example: - explanatory title details: type: - string - 'null' - examples: + example: - Information which helps resolving the problem status: type: @@ -610,7 +750,6 @@ components: title: tenant allOf: - type: object - additionalProperties: false properties: config: $ref: '#/components/schemas/config' @@ -641,7 +780,7 @@ components: minItems: 1 items: type: string - examples: + example: - '*.example.local' allow_unsafe_wildcard: type: boolean @@ -658,7 +797,7 @@ components: timeout: type: number default: 60000 - examples: + example: - 60000 user_verification: type: string @@ -699,17 +838,17 @@ components: id: type: string default: localhost - examples: + example: - localhost display_name: type: string default: Hanko Passkey Server - examples: + example: - Hanko Passkey Server icon: type: string format: uri - examples: + example: - 'http://link.to/icon' origins: type: array @@ -801,3 +940,78 @@ components: - created_at - updated_at - tenant_id + webauthn_user: + type: object + title: webauthn_user + properties: + id: + type: string + user_id: + type: string + name: + type: string + icon: + type: string + display_name: + type: string + required: + - id + - user_id + - name + - icon + - display_name + credential: + type: object + title: credential + properties: + id: + type: string + name: + type: string + public_key: + type: string + attestation_type: + type: string + aaguid: + type: string + last_used_at: + type: string + created_at: + type: string + transports: + type: array + items: + type: string + backup_eligible: + type: boolean + backup_state: + type: boolean + required: + - id + - public_key + - attestation_type + - aaguid + - created_at + - transports + - backup_eligible + - backup_state + transaction: + type: object + title: transaction + properties: + id: + type: string + identifier: + type: string + data: + type: string + created_at: + type: string + updated_at: + type: string + required: + - id + - identifier + - data + - created_at + - updated_at diff --git a/spec/passkey-server.yaml b/spec/passkey-server.yaml index 1ce9f9e..672763d 100644 --- a/spec/passkey-server.yaml +++ b/spec/passkey-server.yaml @@ -312,12 +312,7 @@ paths: operationId: post-mfa-registration-initialize parameters: - $ref: '#/components/parameters/X-API-KEY' - - name: tenant_id - in: path - description: Tenant ID - required: true - schema: - type: string + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/post-registration-initialize' responses: @@ -347,12 +342,7 @@ paths: operationId: post-mfa-registration-finalize parameters: - $ref: '#/components/parameters/X-API-KEY' - - name: tenant_id - in: path - description: Tenant ID - required: true - schema: - type: string + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/post-registration-finalize' responses: @@ -384,12 +374,7 @@ paths: operationId: post-mfa-login-initialize parameters: - $ref: '#/components/parameters/X-API-KEY' - - name: tenant_id - in: path - description: Tenant ID - required: true - schema: - type: string + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/post-mfa-login-initialize' responses: @@ -421,12 +406,7 @@ paths: operationId: post-mfa-login-finalize parameters: - $ref: '#/components/parameters/X-API-KEY' - - name: tenant_id - in: path - description: Tenant ID - required: true - schema: - type: string + - $ref: '#/components/parameters/tenant_id' requestBody: $ref: '#/components/requestBodies/post-login-finalize' responses: @@ -448,11 +428,49 @@ paths: default: localhost path_prefix: default: '' + '/{tenant_id}/transaction/{user_id}': + get: + tags: + - transaction + summary: List transactions for a user + description: Lists all transactions for a given user. + operationId: get-tenant_id-transaction-user_id + parameters: + - $ref: '#/components/parameters/X-API-KEY' + - $ref: '#/components/parameters/tenant_id' + - $ref: '#/components/parameters/path_user_id' + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + uniqueItems: true + items: + $ref: '#/components/schemas/transaction' + '400': + $ref: '#/components/responses/error' + '401': + description: Unauthorized + '404': + $ref: '#/components/responses/error' + '500': + description: Internal Server Error + servers: + - url: 'http://{host}:8000/{path_prefix}' + variables: + host: + default: localhost + path_prefix: + default: '' tags: - name: credentials description: Represents all objects which are related to WebAuthn credentials - name: mfa description: Represents all objects which are related to MFA in common + - name: transaction + description: Represents all objects which are related to Transactions in common - name: webauthn description: Represents all objects which are related to WebAuthn in common components: @@ -488,6 +506,16 @@ components: maxLength: 36 example: - 1f496bcd-49da-4839-a02f-7ce681ccb488 + path_user_id: + name: user_id + in: path + description: ID of the user + required: true + schema: + type: string + format: uuid + minLength: 36 + maxLength: 36 X-API-KEY: name: apiKey in: header @@ -833,6 +861,31 @@ components: type: string minProperties: 1 schemas: + transaction: + type: object + title: transaction + properties: + id: + type: string + format: uuid + minLength: 36 + maxLength: 36 + identifier: + type: string + data: + type: string + description: stringified data object + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + required: + - id + - identifier + - data + - created_at public-key-credential: title: public-key-credential allOf: @@ -846,7 +899,6 @@ components: enum: - cross-platform - platform - - null required: - rawId credential: @@ -894,8 +946,7 @@ components: type: string signature: type: string - userHandle: - type: string | null + userHandle: {} required: - authenticatorData - signature