Merge branch 'main' into feat/huma-api-docs

This commit is contained in:
Elias Schneider
2026-07-26 20:39:38 +02:00
60 changed files with 9046 additions and 1313 deletions
+11 -2
View File
@@ -23,6 +23,11 @@ type oidcClientIDInput struct {
ID string `path:"id"`
}
type oidcClientSecretInput struct {
ID string `path:"id"`
Body *dto.OidcClientSecretDto `required:"false"`
}
type oidcClientListInput struct {
utils.ListRequestOptions
Search string `query:"search" required:"false"`
@@ -294,8 +299,12 @@ func (oc *OidcController) updateClientHandler(ctx context.Context, input *oidcCl
return mapOIDCClient(client)
}
func (oc *OidcController) createClientSecretHandler(ctx context.Context, input *oidcClientIDInput) (*httpapi.BodyOutput[map[string]string], error) {
secret, err := oc.oidcService.CreateClientSecret(ctx, input.ID)
func (oc *OidcController) createClientSecretHandler(ctx context.Context, input *oidcClientSecretInput) (*httpapi.BodyOutput[map[string]string], error) {
body := dto.OidcClientSecretDto{}
if input.Body != nil {
body = *input.Body
}
secret, err := oc.oidcService.CreateClientSecret(ctx, input.ID, body)
if err != nil {
return nil, err
}
+5 -163
View File
@@ -10,20 +10,15 @@ import (
"github.com/danielgtaylor/huma/v2"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/model"
"github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
"github.com/pocket-id/pocket-id/backend/internal/webauthn"
)
const defaultOneTimeAccessTokenDuration = 15 * time.Minute
type userListInput struct {
utils.ListRequestOptions
Search string `query:"search" required:"false"`
@@ -65,37 +60,10 @@ type currentUserPictureUploadInput struct {
RawBody huma.MultipartFormFiles[userPictureUploadForm]
}
type oneTimeAccessOwnInput struct {
Body dto.OneTimeAccessTokenCreateDto
}
type oneTimeAccessAdminInput struct {
ID string `path:"id"`
Body dto.OneTimeAccessTokenCreateDto
}
type oneTimeAccessEmailAdminInput struct {
ID string `path:"id"`
Body dto.OneTimeAccessEmailAsAdminDto
}
type oneTimeAccessEmailInput struct {
Body dto.OneTimeAccessEmailAsUnauthenticatedUserDto
}
type oneTimeAccessExchangeInput struct {
Token string `path:"token"`
}
type emailVerificationInput struct {
Body dto.EmailVerificationDto
}
type userCookieOutput struct {
SetCookie []http.Cookie `header:"Set-Cookie"`
Body dto.UserDto
}
type userPictureOutput struct {
ContentType string `header:"Content-Type"`
ContentLength int64 `header:"Content-Length"`
@@ -104,8 +72,8 @@ type userPictureOutput struct {
}
// NewUserController registers user management endpoints
func NewUserController(api huma.API, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, webAuthnService *webauthn.Module, appConfigService *appconfig.AppConfigService) {
controller := &UserController{userService: userService, oneTimeAccessService: oneTimeAccessService, webAuthnService: webAuthnService, appConfigService: appConfigService}
func NewUserController(api huma.API, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, appConfigService *appconfig.AppConfigService, userService *service.UserService, webAuthnService *webauthn.Module) {
controller := &UserController{appConfigService: appConfigService, userService: userService, webAuthnService: webAuthnService}
adminAuth := authMiddleware.Huma(api)
userAuth := authMiddleware.WithAdminNotRequired().Huma(api)
@@ -226,50 +194,6 @@ func NewUserController(api huma.API, authMiddleware *middleware.AuthMiddleware,
DefaultStatus: http.StatusNoContent,
}, controller.updateCurrentUserProfilePictureHandler, userAuth)
httpapi.Register(api, huma.Operation{
OperationID: "create-own-one-time-access-token",
Method: http.MethodPost,
Path: "/api/users/me/one-time-access-token",
Summary: "Create one-time access token for current user",
Tags: []string{"Users"},
DefaultStatus: http.StatusCreated,
}, controller.createOwnOneTimeAccessTokenHandler, userAuth)
httpapi.Register(api, huma.Operation{
OperationID: "create-user-one-time-access-token",
Method: http.MethodPost,
Path: "/api/users/{id}/one-time-access-token",
Summary: "Create one-time access token for user",
Tags: []string{"Users"},
DefaultStatus: http.StatusCreated,
}, controller.createAdminOneTimeAccessTokenHandler, adminAuth)
httpapi.Register(api, huma.Operation{
OperationID: "request-user-one-time-access-email",
Method: http.MethodPost,
Path: "/api/users/{id}/one-time-access-email",
Summary: "Request one-time access email for user",
Tags: []string{"Users"},
DefaultStatus: http.StatusNoContent,
}, controller.requestOneTimeAccessEmailAsAdminHandler, adminAuth)
httpapi.Register(api, huma.Operation{
OperationID: "exchange-one-time-access-token",
Method: http.MethodPost,
Path: "/api/one-time-access-token/{token}",
Summary: "Exchange one-time access token",
Tags: []string{"Users"},
}, controller.exchangeOneTimeAccessTokenHandler, httpapi.WithMiddleware(rateLimitMiddleware.Huma(api, middleware.RateLimitOneTimeAccessToken)))
httpapi.Register(api, huma.Operation{
OperationID: "request-one-time-access-email",
Method: http.MethodPost,
Path: "/api/one-time-access-email",
Summary: "Request one-time access email",
Tags: []string{"Users"},
DefaultStatus: http.StatusNoContent,
}, controller.requestOneTimeAccessEmailAsUnauthenticatedUserHandler, httpapi.WithMiddleware(rateLimitMiddleware.Huma(api, middleware.RateLimitOneTimeAccessEmail)))
httpapi.Register(api, huma.Operation{
OperationID: "reset-user-profile-picture",
Method: http.MethodDelete,
@@ -308,10 +232,9 @@ func NewUserController(api huma.API, authMiddleware *middleware.AuthMiddleware,
}
type UserController struct {
appConfigService *appconfig.AppConfigService
userService *service.UserService
oneTimeAccessService *service.OneTimeAccessService
webAuthnService *webauthn.Module
appConfigService *appconfig.AppConfigService
userService *service.UserService
webAuthnService *webauthn.Module
}
func (uc *UserController) getUserGroupsHandler(ctx context.Context, input *userIDInput) (*httpapi.BodyOutput[[]dto.UserGroupDto], error) {
@@ -460,83 +383,6 @@ func (uc *UserController) updateProfilePicture(ctx context.Context, userID strin
return &httpapi.EmptyOutput{}, nil
}
func (uc *UserController) createOwnOneTimeAccessTokenHandler(ctx context.Context, _ *oneTimeAccessOwnInput) (*httpapi.BodyOutput[map[string]string], error) {
return uc.createOneTimeAccessToken(ctx, httpapi.UserID(ctx), defaultOneTimeAccessTokenDuration)
}
func (uc *UserController) createAdminOneTimeAccessTokenHandler(ctx context.Context, input *oneTimeAccessAdminInput) (*httpapi.BodyOutput[map[string]string], error) {
ttl := input.Body.TTL.Duration
if ttl <= 0 {
ttl = defaultOneTimeAccessTokenDuration
}
return uc.createOneTimeAccessToken(ctx, input.ID, ttl)
}
func (uc *UserController) createOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (*httpapi.BodyOutput[map[string]string], error) {
if userID == "" {
return nil, &common.UserIdNotProvidedError{}
}
token, err := uc.oneTimeAccessService.CreateOneTimeAccessToken(ctx, userID, ttl)
if err != nil {
return nil, err
}
return &httpapi.BodyOutput[map[string]string]{Body: map[string]string{"token": token}}, nil
}
func (uc *UserController) requestOneTimeAccessEmailAsUnauthenticatedUserHandler(ctx context.Context, input *oneTimeAccessEmailInput) (*emptyOutputWithCookie, error) {
dbConfig, err := uc.appConfigService.GetConfig(ctx)
if err != nil {
return nil, fmt.Errorf("error loading app configuration: %w", err)
}
deviceToken, err := uc.oneTimeAccessService.RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx, dbConfig, input.Body.Email, input.Body.RedirectPath)
if err != nil {
return nil, err
}
return &emptyOutputWithCookie{SetCookie: []http.Cookie{*cookie.NewDeviceTokenCookie(deviceToken)}}, nil
}
func (uc *UserController) requestOneTimeAccessEmailAsAdminHandler(ctx context.Context, input *oneTimeAccessEmailAdminInput) (*httpapi.EmptyOutput, error) {
dbConfig, err := uc.appConfigService.GetConfig(ctx)
if err != nil {
return nil, fmt.Errorf("error loading app configuration: %w", err)
}
ttl := input.Body.TTL.Duration
if ttl <= 0 {
ttl = defaultOneTimeAccessTokenDuration
}
if err := uc.oneTimeAccessService.RequestOneTimeAccessEmailAsAdmin(ctx, dbConfig, input.ID, ttl); err != nil {
return nil, err
}
return &httpapi.EmptyOutput{}, nil
}
func (uc *UserController) exchangeOneTimeAccessTokenHandler(ctx context.Context, input *oneTimeAccessExchangeInput) (*userCookieOutput, error) {
dbConfig, err := uc.appConfigService.GetConfig(ctx)
if err != nil {
return nil, fmt.Errorf("error loading app configuration: %w", err)
}
if len(input.Token) != 6 && len(input.Token) != 16 {
return nil, &common.TokenInvalidOrExpiredError{}
}
deviceToken := ""
if requestCookie, err := httpapi.Cookie(ctx, cookie.DeviceTokenCookieName); err == nil {
deviceToken = requestCookie.Value
}
user, token, err := uc.oneTimeAccessService.ExchangeOneTimeAccessToken(ctx, dbConfig, input.Token, deviceToken, httpapi.ClientIP(ctx), httpapi.UserAgent(ctx))
if err != nil {
return nil, err
}
var output dto.UserDto
if err := dto.MapStruct(user, &output); err != nil {
return nil, err
}
maxAge := int(dbConfig.SessionDuration.AsDurationMinutes().Seconds())
return &userCookieOutput{SetCookie: []http.Cookie{*cookie.NewAccessTokenCookie(maxAge, token)}, Body: output}, nil
}
func (uc *UserController) updateUserGroups(ctx context.Context, input *userGroupsInput) (*httpapi.BodyOutput[dto.UserDto], error) {
user, err := uc.userService.UpdateUserGroups(ctx, input.ID, input.Body.UserGroupIds)
if err != nil {
@@ -578,10 +424,6 @@ func (uc *UserController) verifyEmailHandler(ctx context.Context, input *emailVe
return &httpapi.EmptyOutput{}, nil
}
type emptyOutputWithCookie struct {
SetCookie []http.Cookie `header:"Set-Cookie"`
}
func mapUser(user model.User) (*httpapi.BodyOutput[dto.UserDto], error) {
var output dto.UserDto
if err := dto.MapStruct(user, &output); err != nil {