diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index 3b3015a0..cd9d1fdf 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -161,7 +161,7 @@ func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services, rateLimitServices controller.NewOidcController(api, authMiddleware, fileSizeLimitMiddleware, svc.oidcService) controller.NewUserController(api, authMiddleware, rateLimitMiddleware, svc.userService, svc.oneTimeAccessService, svc.webauthnModule, svc.appConfigService) controller.NewAppConfigController(api, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService) - controller.NewAppImagesController(api, authMiddleware, svc.appImagesService) + controller.NewAppImagesController(api, authMiddleware, fileSizeLimitMiddleware, svc.appImagesService) controller.NewAuditLogController(api, svc.auditLogService, authMiddleware) controller.NewUserGroupController(api, authMiddleware, svc.userGroupService) svc.apiModule.RegisterRoutes(api, authMiddleware.Huma(api)) diff --git a/backend/internal/controller/app_images_controller.go b/backend/internal/controller/app_images_controller.go index 63440aca..563f69cc 100644 --- a/backend/internal/controller/app_images_controller.go +++ b/backend/internal/controller/app_images_controller.go @@ -43,9 +43,10 @@ type imageOutput struct { Body func(huma.Context) } -func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddleware, appImagesService *service.AppImagesService) { +func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, appImagesService *service.AppImagesService) { controller := &AppImagesController{appImagesService: appImagesService} auth := authMiddleware.Huma(api) + uploadLimit := httpapi.WithMiddleware(fileSizeLimitMiddleware.Huma(api, 2<<20)) httpapi.Register(api, huma.Operation{ OperationID: "get-application-logo", @@ -94,7 +95,7 @@ func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddlew Summary: "Update logo", Tags: []string{"Application Images"}, DefaultStatus: http.StatusNoContent, - }, controller.updateLogoHandler, auth) + }, controller.updateLogoHandler, auth, uploadLimit) httpapi.Register(api, huma.Operation{ OperationID: "update-email-logo", @@ -103,7 +104,7 @@ func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddlew Summary: "Update email logo", Tags: []string{"Application Images"}, DefaultStatus: http.StatusNoContent, - }, controller.updateEmailLogoHandler, auth) + }, controller.updateEmailLogoHandler, auth, uploadLimit) httpapi.Register(api, huma.Operation{ OperationID: "update-background-image", @@ -112,7 +113,7 @@ func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddlew Summary: "Update background image", Tags: []string{"Application Images"}, DefaultStatus: http.StatusNoContent, - }, controller.updateBackgroundImageHandler, auth) + }, controller.updateBackgroundImageHandler, auth, uploadLimit) httpapi.Register(api, huma.Operation{ OperationID: "update-favicon", @@ -121,7 +122,7 @@ func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddlew Summary: "Update favicon", Tags: []string{"Application Images"}, DefaultStatus: http.StatusNoContent, - }, controller.updateFaviconHandler, auth) + }, controller.updateFaviconHandler, auth, uploadLimit) httpapi.Register(api, huma.Operation{ OperationID: "update-default-profile-picture", @@ -130,7 +131,7 @@ func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddlew Summary: "Update default profile picture", Tags: []string{"Application Images"}, DefaultStatus: http.StatusNoContent, - }, controller.updateDefaultProfilePicture, auth) + }, controller.updateDefaultProfilePicture, auth, uploadLimit) httpapi.Register(api, huma.Operation{ OperationID: "delete-background-image", diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index 553eee7c..2734240e 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -211,6 +211,8 @@ func (s *OidcService) UpdateClient(ctx context.Context, clientID string, input d } func updateOIDCClientModelFromDto(client *model.OidcClient, input *dto.OidcClientUpdateDto) { + dto.Normalize(input) + // Base fields client.Name = input.Name client.Description = input.Description diff --git a/backend/internal/service/oidc_service_test.go b/backend/internal/service/oidc_service_test.go index 5c9b4435..2c7fca26 100644 --- a/backend/internal/service/oidc_service_test.go +++ b/backend/internal/service/oidc_service_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/text/unicode/norm" "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/dto" @@ -457,10 +458,11 @@ func TestOidcService_CreateClient_withDescription(t *testing.T) { s, err := NewOidcService(db, nil, nil, nil, nil, nil, nil) require.NoError(t, err) - description := "A test client description" + name := norm.NFD.String("Tést Client") + description := norm.NFD.String("A café client description") input := dto.OidcClientCreateDto{ OidcClientUpdateDto: dto.OidcClientUpdateDto{ - Name: "Test Client", + Name: name, Description: description, CallbackURLs: []string{"https://example.com/callback"}, }, @@ -473,7 +475,8 @@ func TestOidcService_CreateClient_withDescription(t *testing.T) { err = db.First(&fetched, "id = ?", client.ID).Error require.NoError(t, err) require.NotEmpty(t, fetched.Description) - assert.Equal(t, description, fetched.Description) + assert.Equal(t, norm.NFC.String(name), fetched.Name) + assert.Equal(t, norm.NFC.String(description), fetched.Description) } func TestOidcService_CreateClient_withoutDescription(t *testing.T) { @@ -513,7 +516,7 @@ func TestOidcService_UpdateClient_description(t *testing.T) { require.NoError(t, err) // Update with a description - description := "Updated description" + description := norm.NFD.String("Updated café description") input := dto.OidcClientUpdateDto{ Name: "Test Client", Description: description, @@ -527,7 +530,7 @@ func TestOidcService_UpdateClient_description(t *testing.T) { err = db.First(&fetched, "id = ?", client.ID).Error require.NoError(t, err) require.NotEmpty(t, fetched.Description) - assert.Equal(t, description, fetched.Description) + assert.Equal(t, norm.NFC.String(description), fetched.Description) // Update to clear the description input.Description = "" diff --git a/backend/internal/utils/huma/api_test.go b/backend/internal/utils/huma/api_test.go index 9acf6bf1..cfad589c 100644 --- a/backend/internal/utils/huma/api_test.go +++ b/backend/internal/utils/huma/api_test.go @@ -9,6 +9,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/danielgtaylor/huma/v2" "github.com/gin-gonic/gin" @@ -197,3 +198,40 @@ func TestRegisterAppliesDecoratorsInOrder(t *testing.T) { require.Equal(t, http.StatusNoContent, response.Code) require.Equal(t, []string{"first", "second", "handler"}, order) } + +func TestRegisterPreservesBodyLimitConfiguration(t *testing.T) { + router, api := newTestAPI(t) + + Register(api, huma.Operation{ + OperationID: "test-default-body-limits", + Method: http.MethodPost, + Path: "/api/test-default-body-limits", + }, func(context.Context, *testInput) (*testOutput, error) { + return &testOutput{}, nil + }) + + defaultOperation := api.OpenAPI().Paths["/api/test-default-body-limits"].Post + require.Equal(t, int64(1<<20), defaultOperation.MaxBodyBytes) + require.Equal(t, 5*time.Second, defaultOperation.BodyReadTimeout) + + request := httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/api/test-default-body-limits", strings.NewReader(`{"name":"`+strings.Repeat("x", 1<<20)+`"}`)) + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + router.ServeHTTP(response, request) + require.Equal(t, http.StatusRequestEntityTooLarge, response.Code) + require.JSONEq(t, `{"error":"Request body is too large limit=1048576 bytes"}`, response.Body.String()) + + Register(api, huma.Operation{ + OperationID: "test-unlimited-body", + Method: http.MethodPost, + Path: "/api/test-unlimited-body", + MaxBodyBytes: -1, + BodyReadTimeout: -1, + }, func(context.Context, *testInput) (*testOutput, error) { + return &testOutput{}, nil + }) + + unlimitedOperation := api.OpenAPI().Paths["/api/test-unlimited-body"].Post + require.Equal(t, int64(-1), unlimitedOperation.MaxBodyBytes) + require.Equal(t, time.Duration(-1), unlimitedOperation.BodyReadTimeout) +} diff --git a/backend/internal/utils/huma/register.go b/backend/internal/utils/huma/register.go index bb0cf6a5..e2a4bdd0 100644 --- a/backend/internal/utils/huma/register.go +++ b/backend/internal/utils/huma/register.go @@ -19,12 +19,6 @@ func Register[I, O any](api huma.API, operation huma.Operation, handler func(con decorator(&operation) } - if operation.MaxBodyBytes == 0 { - operation.MaxBodyBytes = -1 - } - if operation.BodyReadTimeout == 0 { - operation.BodyReadTimeout = -1 - } huma.Register(api, operation, func(ctx context.Context, input *I) (*O, error) { output, err := handler(ctx, input) if err != nil {