From 7b5d6f5a9cebd64cf0cbe8ad984a26421d8ba580 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Sun, 12 Jul 2026 13:42:05 -0500 Subject: [PATCH] format api routes --- backend/internal/api/module.go | 82 ++++-- backend/internal/apikey/module.go | 48 ++-- .../internal/bootstrap/router_bootstrap.go | 8 +- .../internal/bootstrap/router_huma_test.go | 2 +- .../controller/app_config_controller.go | 58 ++-- .../controller/app_images_controller.go | 131 ++++++--- .../controller/audit_log_controller.go | 42 ++- .../controller/custom_claim_controller.go | 30 +- .../internal/controller/e2etest_controller.go | 49 +++- .../internal/controller/oidc_controller.go | 181 ++++++++---- .../internal/controller/scim_controller.go | 50 ++-- .../internal/controller/user_controller.go | 260 ++++++++++++------ .../controller/user_group_controller.go | 78 ++++-- .../internal/controller/version_controller.go | 7 +- .../controller/well_known_controller.go | 17 +- backend/internal/oidc/module.go | 135 +++++++-- backend/internal/usersignup/module.go | 73 +++-- backend/internal/utils/huma/api_test.go | 39 ++- backend/internal/utils/huma/raw.go | 13 +- backend/internal/utils/huma/register.go | 13 +- backend/internal/webauthn/handler_test.go | 6 +- backend/internal/webauthn/module.go | 99 ++++--- 22 files changed, 1010 insertions(+), 411 deletions(-) diff --git a/backend/internal/api/module.go b/backend/internal/api/module.go index e331f5e4..ffced91b 100644 --- a/backend/internal/api/module.go +++ b/backend/internal/api/module.go @@ -62,25 +62,69 @@ func (m *Module) DescribePermissions(ctx context.Context, audience string, keys // RegisterRoutes mounts the admin CRUD endpoints func (m *Module) RegisterRoutes(api huma.API, adminAuth func(*huma.Operation)) { - register := func(operation huma.Operation, registerHandler func(huma.Operation)) { - adminAuth(&operation) - registerHandler(operation) - } + httpapi.Register(api, huma.Operation{ + OperationID: "list-apis", + Method: http.MethodGet, + Path: "/api/apis", + Summary: "List APIs", + Tags: []string{"APIs"}, + }, m.handler.list, adminAuth) - register(apiOperation("list-apis", http.MethodGet, "/api/apis", "List APIs"), func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.list) }) - createOperation := apiOperation("create-api", http.MethodPost, "/api/apis", "Create API") - createOperation.DefaultStatus = http.StatusCreated - register(createOperation, func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.create) }) - register(apiOperation("get-api", http.MethodGet, "/api/apis/{id}", "Get API by ID"), func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.get) }) - register(apiOperation("update-api", http.MethodPut, "/api/apis/{id}", "Update API"), func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.update) }) - deleteOperation := apiOperation("delete-api", http.MethodDelete, "/api/apis/{id}", "Delete API") - deleteOperation.DefaultStatus = http.StatusNoContent - register(deleteOperation, func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.delete) }) - register(apiOperation("update-api-permissions", http.MethodPut, "/api/apis/{id}/permissions", "Update API permissions"), func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.updatePermissions) }) - register(apiOperation("get-client-api-access", http.MethodGet, "/api/api-access/{clientId}", "Get client API access"), func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.getClientAccess) }) - register(apiOperation("update-client-api-access", http.MethodPut, "/api/api-access/{clientId}", "Update client API access"), func(operation huma.Operation) { httpapi.Register(api, operation, m.handler.updateClientAccess) }) -} + httpapi.Register(api, huma.Operation{ + OperationID: "create-api", + Method: http.MethodPost, + Path: "/api/apis", + Summary: "Create API", + Tags: []string{"APIs"}, + DefaultStatus: http.StatusCreated, + }, m.handler.create, adminAuth) -func apiOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"APIs"}} + httpapi.Register(api, huma.Operation{ + OperationID: "get-api", + Method: http.MethodGet, + Path: "/api/apis/{id}", + Summary: "Get API by ID", + Tags: []string{"APIs"}, + }, m.handler.get, adminAuth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-api", + Method: http.MethodPut, + Path: "/api/apis/{id}", + Summary: "Update API", + Tags: []string{"APIs"}, + }, m.handler.update, adminAuth) + + httpapi.Register(api, huma.Operation{ + OperationID: "delete-api", + Method: http.MethodDelete, + Path: "/api/apis/{id}", + Summary: "Delete API", + Tags: []string{"APIs"}, + DefaultStatus: http.StatusNoContent, + }, m.handler.delete, adminAuth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-api-permissions", + Method: http.MethodPut, + Path: "/api/apis/{id}/permissions", + Summary: "Update API permissions", + Tags: []string{"APIs"}, + }, m.handler.updatePermissions, adminAuth) + + httpapi.Register(api, huma.Operation{ + OperationID: "get-client-api-access", + Method: http.MethodGet, + Path: "/api/api-access/{clientId}", + Summary: "Get client API access", + Tags: []string{"APIs"}, + }, m.handler.getClientAccess, adminAuth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-client-api-access", + Method: http.MethodPut, + Path: "/api/api-access/{clientId}", + Summary: "Update client API access", + Tags: []string{"APIs"}, + }, m.handler.updateClientAccess, adminAuth) } diff --git a/backend/internal/apikey/module.go b/backend/internal/apikey/module.go index 2161e4a8..4c02a66a 100644 --- a/backend/internal/apikey/module.go +++ b/backend/internal/apikey/module.go @@ -36,27 +36,39 @@ func New(ctx context.Context, deps Dependencies) (*Module, error) { // RegisterRoutes mounts the API key management endpoints // authWithoutApiKey disables API key authentication so an API key cannot be used to mint or renew further API keys func (m *Module) RegisterRoutes(api huma.API, auth, authWithoutAPIKey func(*huma.Operation)) { - listOperation := apiKeyOperation("list-api-keys", http.MethodGet, "/api/api-keys", "List API keys") - auth(&listOperation) - httpapi.Register(api, listOperation, m.handler.list) + httpapi.Register(api, huma.Operation{ + OperationID: "list-api-keys", + Method: http.MethodGet, + Path: "/api/api-keys", + Summary: "List API keys", + Tags: []string{"API Keys"}, + }, m.handler.list, auth) - createOperation := apiKeyOperation("create-api-key", http.MethodPost, "/api/api-keys", "Create API key") - createOperation.DefaultStatus = http.StatusCreated - authWithoutAPIKey(&createOperation) - httpapi.Register(api, createOperation, m.handler.create) + httpapi.Register(api, huma.Operation{ + OperationID: "create-api-key", + Method: http.MethodPost, + Path: "/api/api-keys", + Summary: "Create API key", + Tags: []string{"API Keys"}, + DefaultStatus: http.StatusCreated, + }, m.handler.create, authWithoutAPIKey) - renewOperation := apiKeyOperation("renew-api-key", http.MethodPost, "/api/api-keys/{id}/renew", "Renew API key") - authWithoutAPIKey(&renewOperation) - httpapi.Register(api, renewOperation, m.handler.renew) + httpapi.Register(api, huma.Operation{ + OperationID: "renew-api-key", + Method: http.MethodPost, + Path: "/api/api-keys/{id}/renew", + Summary: "Renew API key", + Tags: []string{"API Keys"}, + }, m.handler.renew, authWithoutAPIKey) - revokeOperation := apiKeyOperation("revoke-api-key", http.MethodDelete, "/api/api-keys/{id}", "Revoke API key") - revokeOperation.DefaultStatus = http.StatusNoContent - auth(&revokeOperation) - httpapi.Register(api, revokeOperation, m.handler.revoke) -} - -func apiKeyOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"API Keys"}} + httpapi.Register(api, huma.Operation{ + OperationID: "revoke-api-key", + Method: http.MethodDelete, + Path: "/api/api-keys/{id}", + Summary: "Revoke API key", + Tags: []string{"API Keys"}, + DefaultStatus: http.StatusNoContent, + }, m.handler.revoke, auth) } // ValidateApiKey resolves the user that owns the given raw API key diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index dac3d89b..b92f3ecc 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -177,7 +177,13 @@ func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services, rateLimitServices // These are not rate-limited. controller.NewHealthzController(r) - httpapi.AddRawOperation(api, "healthz", http.MethodGet, "/healthz", "Health check", []string{"Health"}, nil, http.StatusNoContent) + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "healthz", + Method: http.MethodGet, + Path: "/healthz", + Summary: "Health check", + Tags: []string{"Health"}, + }, http.StatusNoContent) // Receives OTLP trace payloads from the browser SPA (POST /internal/telemetry/traces) and forwards them to the collector, when trace export is enabled. // Outside /api, so it's unauthenticated and not traced, but it is rate-limited. diff --git a/backend/internal/bootstrap/router_huma_test.go b/backend/internal/bootstrap/router_huma_test.go index f78bce28..7c1502f0 100644 --- a/backend/internal/bootstrap/router_huma_test.go +++ b/backend/internal/bootstrap/router_huma_test.go @@ -28,7 +28,7 @@ func TestHumaRouterOpenAPI(t *testing.T) { require.NoError(t, err) scheduler, err := job.NewScheduler() require.NoError(t, err) - services, err := initServices(t.Context(), db, http.DefaultClient, map[string]string{}, fileStorage, scheduler) + services, err := initServices(t.Context(), db, "test-instance", http.DefaultClient, map[string]string{}, fileStorage, scheduler) require.NoError(t, err) router, err := initEngine() require.NoError(t, err) diff --git a/backend/internal/controller/app_config_controller.go b/backend/internal/controller/app_config_controller.go index 7b7587c9..ab37680e 100644 --- a/backend/internal/controller/app_config_controller.go +++ b/backend/internal/controller/app_config_controller.go @@ -28,31 +28,49 @@ func NewAppConfigController( ldapService *service.LdapService, ) { controller := &AppConfigController{appConfigService: appConfigService, emailService: emailService, ldapService: ldapService} - - httpapi.Register(api, appConfigOperation("list-public-application-configuration", http.MethodGet, "/api/application-configuration", "List public application configurations"), controller.listAppConfigHandler) - auth := authMiddleware.Huma(api) - allOperation := appConfigOperation("list-all-application-configuration", http.MethodGet, "/api/application-configuration/all", "List all application configurations") - auth(&allOperation) - httpapi.Register(api, allOperation, controller.listAllAppConfigHandler) - updateOperation := appConfigOperation("update-application-configuration", http.MethodPut, "/api/application-configuration", "Update application configurations") - auth(&updateOperation) - httpapi.Register(api, updateOperation, controller.updateAppConfigHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-public-application-configuration", + Method: http.MethodGet, + Path: "/api/application-configuration", + Summary: "List public application configurations", + Tags: []string{"Application Configuration"}, + }, controller.listAppConfigHandler) - testEmailOperation := appConfigOperation("test-email-configuration", http.MethodPost, "/api/application-configuration/test-email", "Send test email") - testEmailOperation.DefaultStatus = http.StatusNoContent - auth(&testEmailOperation) - httpapi.Register(api, testEmailOperation, controller.testEmailHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-all-application-configuration", + Method: http.MethodGet, + Path: "/api/application-configuration/all", + Summary: "List all application configurations", + Tags: []string{"Application Configuration"}, + }, controller.listAllAppConfigHandler, auth) - syncLDAPOperation := appConfigOperation("sync-ldap", http.MethodPost, "/api/application-configuration/sync-ldap", "Synchronize LDAP") - syncLDAPOperation.DefaultStatus = http.StatusNoContent - auth(&syncLDAPOperation) - httpapi.Register(api, syncLDAPOperation, controller.syncLDAPHandler) -} + httpapi.Register(api, huma.Operation{ + OperationID: "update-application-configuration", + Method: http.MethodPut, + Path: "/api/application-configuration", + Summary: "Update application configurations", + Tags: []string{"Application Configuration"}, + }, controller.updateAppConfigHandler, auth) -func appConfigOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"Application Configuration"}} + httpapi.Register(api, huma.Operation{ + OperationID: "test-email-configuration", + Method: http.MethodPost, + Path: "/api/application-configuration/test-email", + Summary: "Send test email", + Tags: []string{"Application Configuration"}, + DefaultStatus: http.StatusNoContent, + }, controller.testEmailHandler, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "sync-ldap", + Method: http.MethodPost, + Path: "/api/application-configuration/sync-ldap", + Summary: "Synchronize LDAP", + Tags: []string{"Application Configuration"}, + DefaultStatus: http.StatusNoContent, + }, controller.syncLDAPHandler, auth) } type AppConfigController struct { diff --git a/backend/internal/controller/app_images_controller.go b/backend/internal/controller/app_images_controller.go index 3147a968..63440aca 100644 --- a/backend/internal/controller/app_images_controller.go +++ b/backend/internal/controller/app_images_controller.go @@ -45,43 +45,110 @@ type imageOutput struct { func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddleware, appImagesService *service.AppImagesService) { controller := &AppImagesController{appImagesService: appImagesService} - - httpapi.Register(api, imageOperation("get-application-logo", http.MethodGet, "/api/application-images/logo", "Get logo image"), controller.getLogoHandler) - httpapi.Register(api, imageOperation("get-email-logo", http.MethodGet, "/api/application-images/email", "Get email logo image"), controller.getEmailLogoHandler) - httpapi.Register(api, imageOperation("get-background-image", http.MethodGet, "/api/application-images/background", "Get background image"), controller.getBackgroundImageHandler) - httpapi.Register(api, imageOperation("get-favicon", http.MethodGet, "/api/application-images/favicon", "Get favicon"), controller.getFaviconHandler) - auth := authMiddleware.Huma(api) - defaultPictureOperation := imageOperation("get-default-profile-picture", http.MethodGet, "/api/application-images/default-profile-picture", "Get default profile picture") - auth(&defaultPictureOperation) - httpapi.Register(api, defaultPictureOperation, controller.getDefaultProfilePicture) - logoOperation := imageOperation("update-application-logo", http.MethodPut, "/api/application-images/logo", "Update logo") - logoOperation.DefaultStatus = http.StatusNoContent - auth(&logoOperation) - httpapi.Register(api, logoOperation, controller.updateLogoHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-application-logo", + Method: http.MethodGet, + Path: "/api/application-images/logo", + Summary: "Get logo image", + Tags: []string{"Application Images"}, + }, controller.getLogoHandler) - registerUpload := func(operation huma.Operation, handler func(context.Context, *imageUploadInput) (*httpapi.EmptyOutput, error)) { - operation.DefaultStatus = http.StatusNoContent - auth(&operation) - httpapi.Register(api, operation, handler) - } - registerUpload(imageOperation("update-email-logo", http.MethodPut, "/api/application-images/email", "Update email logo"), controller.updateEmailLogoHandler) - registerUpload(imageOperation("update-background-image", http.MethodPut, "/api/application-images/background", "Update background image"), controller.updateBackgroundImageHandler) - registerUpload(imageOperation("update-favicon", http.MethodPut, "/api/application-images/favicon", "Update favicon"), controller.updateFaviconHandler) - registerUpload(imageOperation("update-default-profile-picture", http.MethodPut, "/api/application-images/default-profile-picture", "Update default profile picture"), controller.updateDefaultProfilePicture) + httpapi.Register(api, huma.Operation{ + OperationID: "get-email-logo", + Method: http.MethodGet, + Path: "/api/application-images/email", + Summary: "Get email logo image", + Tags: []string{"Application Images"}, + }, controller.getEmailLogoHandler) - registerDelete := func(operation huma.Operation, handler func(context.Context, *httpapi.EmptyInput) (*httpapi.EmptyOutput, error)) { - operation.DefaultStatus = http.StatusNoContent - auth(&operation) - httpapi.Register(api, operation, handler) - } - registerDelete(imageOperation("delete-background-image", http.MethodDelete, "/api/application-images/background", "Delete background image"), controller.deleteBackgroundImageHandler) - registerDelete(imageOperation("delete-default-profile-picture", http.MethodDelete, "/api/application-images/default-profile-picture", "Delete default profile picture"), controller.deleteDefaultProfilePicture) -} + httpapi.Register(api, huma.Operation{ + OperationID: "get-background-image", + Method: http.MethodGet, + Path: "/api/application-images/background", + Summary: "Get background image", + Tags: []string{"Application Images"}, + }, controller.getBackgroundImageHandler) -func imageOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"Application Images"}} + httpapi.Register(api, huma.Operation{ + OperationID: "get-favicon", + Method: http.MethodGet, + Path: "/api/application-images/favicon", + Summary: "Get favicon", + Tags: []string{"Application Images"}, + }, controller.getFaviconHandler) + + httpapi.Register(api, huma.Operation{ + OperationID: "get-default-profile-picture", + Method: http.MethodGet, + Path: "/api/application-images/default-profile-picture", + Summary: "Get default profile picture", + Tags: []string{"Application Images"}, + }, controller.getDefaultProfilePicture, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-application-logo", + Method: http.MethodPut, + Path: "/api/application-images/logo", + Summary: "Update logo", + Tags: []string{"Application Images"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateLogoHandler, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-email-logo", + Method: http.MethodPut, + Path: "/api/application-images/email", + Summary: "Update email logo", + Tags: []string{"Application Images"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateEmailLogoHandler, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-background-image", + Method: http.MethodPut, + Path: "/api/application-images/background", + Summary: "Update background image", + Tags: []string{"Application Images"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateBackgroundImageHandler, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-favicon", + Method: http.MethodPut, + Path: "/api/application-images/favicon", + Summary: "Update favicon", + Tags: []string{"Application Images"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateFaviconHandler, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "update-default-profile-picture", + Method: http.MethodPut, + Path: "/api/application-images/default-profile-picture", + Summary: "Update default profile picture", + Tags: []string{"Application Images"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateDefaultProfilePicture, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "delete-background-image", + Method: http.MethodDelete, + Path: "/api/application-images/background", + Summary: "Delete background image", + Tags: []string{"Application Images"}, + DefaultStatus: http.StatusNoContent, + }, controller.deleteBackgroundImageHandler, auth) + + httpapi.Register(api, huma.Operation{ + OperationID: "delete-default-profile-picture", + Method: http.MethodDelete, + Path: "/api/application-images/default-profile-picture", + Summary: "Delete default profile picture", + Tags: []string{"Application Images"}, + DefaultStatus: http.StatusNoContent, + }, controller.deleteDefaultProfilePicture, auth) } type AppImagesController struct { diff --git a/backend/internal/controller/audit_log_controller.go b/backend/internal/controller/audit_log_controller.go index 13f0cb6a..aee6dd31 100644 --- a/backend/internal/controller/audit_log_controller.go +++ b/backend/internal/controller/audit_log_controller.go @@ -16,22 +16,40 @@ import ( // NewAuditLogController registers audit log routes func NewAuditLogController(api huma.API, auditLogService *service.AuditLogService, authMiddleware *middleware.AuthMiddleware) { controller := &AuditLogController{auditLogService: auditLogService} + adminAuth := authMiddleware.Huma(api) + userAuth := authMiddleware.WithAdminNotRequired().Huma(api) - allOperation := huma.Operation{OperationID: "list-all-audit-logs", Method: http.MethodGet, Path: "/api/audit-logs/all", Summary: "List all audit logs", Tags: []string{"Audit Logs"}} - authMiddleware.Huma(api)(&allOperation) - httpapi.Register(api, allOperation, controller.listAllAuditLogsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-all-audit-logs", + Method: http.MethodGet, + Path: "/api/audit-logs/all", + Summary: "List all audit logs", + Tags: []string{"Audit Logs"}, + }, controller.listAllAuditLogsHandler, adminAuth) - userOperation := huma.Operation{OperationID: "list-current-user-audit-logs", Method: http.MethodGet, Path: "/api/audit-logs", Summary: "List audit logs for the current user", Tags: []string{"Audit Logs"}} - authMiddleware.WithAdminNotRequired().Huma(api)(&userOperation) - httpapi.Register(api, userOperation, controller.listAuditLogsForUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-current-user-audit-logs", + Method: http.MethodGet, + Path: "/api/audit-logs", + Summary: "List audit logs for the current user", + Tags: []string{"Audit Logs"}, + }, controller.listAuditLogsForUserHandler, userAuth) - clientsOperation := huma.Operation{OperationID: "list-audit-log-client-names", Method: http.MethodGet, Path: "/api/audit-logs/filters/client-names", Summary: "List client names", Tags: []string{"Audit Logs"}} - authMiddleware.Huma(api)(&clientsOperation) - httpapi.Register(api, clientsOperation, controller.listClientNamesHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-audit-log-client-names", + Method: http.MethodGet, + Path: "/api/audit-logs/filters/client-names", + Summary: "List client names", + Tags: []string{"Audit Logs"}, + }, controller.listClientNamesHandler, adminAuth) - usersOperation := huma.Operation{OperationID: "list-audit-log-users", Method: http.MethodGet, Path: "/api/audit-logs/filters/users", Summary: "List users with IDs", Tags: []string{"Audit Logs"}} - authMiddleware.Huma(api)(&usersOperation) - httpapi.Register(api, usersOperation, controller.listUserNamesWithIDsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-audit-log-users", + Method: http.MethodGet, + Path: "/api/audit-logs/filters/users", + Summary: "List users with IDs", + Tags: []string{"Audit Logs"}, + }, controller.listUserNamesWithIDsHandler, adminAuth) } type AuditLogController struct { diff --git a/backend/internal/controller/custom_claim_controller.go b/backend/internal/controller/custom_claim_controller.go index 657caf7a..bc1c0c32 100644 --- a/backend/internal/controller/custom_claim_controller.go +++ b/backend/internal/controller/custom_claim_controller.go @@ -27,17 +27,29 @@ func NewCustomClaimController(api huma.API, authMiddleware *middleware.AuthMiddl controller := &CustomClaimController{customClaimService: customClaimService} auth := authMiddleware.Huma(api) - suggestionsOperation := huma.Operation{OperationID: "list-custom-claim-suggestions", Method: http.MethodGet, Path: "/api/custom-claims/suggestions", Summary: "Get custom claim suggestions", Tags: []string{"Custom Claims"}} - auth(&suggestionsOperation) - httpapi.Register(api, suggestionsOperation, controller.getSuggestionsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-custom-claim-suggestions", + Method: http.MethodGet, + Path: "/api/custom-claims/suggestions", + Summary: "Get custom claim suggestions", + Tags: []string{"Custom Claims"}, + }, controller.getSuggestionsHandler, auth) - userOperation := huma.Operation{OperationID: "update-user-custom-claims", Method: http.MethodPut, Path: "/api/custom-claims/user/{userId}", Summary: "Update custom claims for a user", Tags: []string{"Custom Claims"}} - auth(&userOperation) - httpapi.Register(api, userOperation, controller.updateCustomClaimsForUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-user-custom-claims", + Method: http.MethodPut, + Path: "/api/custom-claims/user/{userId}", + Summary: "Update custom claims for a user", + Tags: []string{"Custom Claims"}, + }, controller.updateCustomClaimsForUserHandler, auth) - userGroupOperation := huma.Operation{OperationID: "update-user-group-custom-claims", Method: http.MethodPut, Path: "/api/custom-claims/user-group/{userGroupId}", Summary: "Update custom claims for a user group", Tags: []string{"Custom Claims"}} - auth(&userGroupOperation) - httpapi.Register(api, userGroupOperation, controller.updateCustomClaimsForUserGroupHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-user-group-custom-claims", + Method: http.MethodPut, + Path: "/api/custom-claims/user-group/{userGroupId}", + Summary: "Update custom claims for a user group", + Tags: []string{"Custom Claims"}, + }, controller.updateCustomClaimsForUserGroupHandler, auth) } type CustomClaimController struct { diff --git a/backend/internal/controller/e2etest_controller.go b/backend/internal/controller/e2etest_controller.go index d81f3c4a..6cbf1dbf 100644 --- a/backend/internal/controller/e2etest_controller.go +++ b/backend/internal/controller/e2etest_controller.go @@ -50,17 +50,46 @@ type testBytesOutput struct { func NewTestController(api huma.API, testService *service.TestService) { controller := &TestController{TestService: testService} - resetOperation := testOperation("test-reset", http.MethodPost, "/api/test/reset") - resetOperation.DefaultStatus = http.StatusNoContent - httpapi.Register(api, resetOperation, controller.resetAndSeedHandler) - httpapi.Register(api, testOperation("test-sign-access-token", http.MethodPost, "/api/test/accesstoken"), controller.signAccessToken) - httpapi.Register(api, testOperation("test-sign-refresh-token", http.MethodPost, "/api/test/refreshtoken"), controller.signRefreshToken) - httpapi.Register(api, testOperation("test-external-idp-jwks", http.MethodGet, "/api/externalidp/jwks.json"), controller.externalIDPJWKS) - httpapi.Register(api, testOperation("test-external-idp-sign", http.MethodPost, "/api/externalidp/sign"), controller.externalIDPSignToken) -} + httpapi.Register(api, huma.Operation{ + OperationID: "test-reset", + Method: http.MethodPost, + Path: "/api/test/reset", + Tags: []string{"E2E Test"}, + Hidden: true, + DefaultStatus: http.StatusNoContent, + }, controller.resetAndSeedHandler) -func testOperation(id, method, path string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Tags: []string{"E2E Test"}, Hidden: true} + httpapi.Register(api, huma.Operation{ + OperationID: "test-sign-access-token", + Method: http.MethodPost, + Path: "/api/test/accesstoken", + Tags: []string{"E2E Test"}, + Hidden: true, + }, controller.signAccessToken) + + httpapi.Register(api, huma.Operation{ + OperationID: "test-sign-refresh-token", + Method: http.MethodPost, + Path: "/api/test/refreshtoken", + Tags: []string{"E2E Test"}, + Hidden: true, + }, controller.signRefreshToken) + + httpapi.Register(api, huma.Operation{ + OperationID: "test-external-idp-jwks", + Method: http.MethodGet, + Path: "/api/externalidp/jwks.json", + Tags: []string{"E2E Test"}, + Hidden: true, + }, controller.externalIDPJWKS) + + httpapi.Register(api, huma.Operation{ + OperationID: "test-external-idp-sign", + Method: http.MethodPost, + Path: "/api/externalidp/sign", + Tags: []string{"E2E Test"}, + Hidden: true, + }, controller.externalIDPSignToken) } type TestController struct { diff --git a/backend/internal/controller/oidc_controller.go b/backend/internal/controller/oidc_controller.go index 0853910f..ec89fef7 100644 --- a/backend/internal/controller/oidc_controller.go +++ b/backend/internal/controller/oidc_controller.go @@ -85,79 +85,146 @@ func NewOidcController(api huma.API, authMiddleware *middleware.AuthMiddleware, adminAuth := authMiddleware.Huma(api) userAuth := authMiddleware.WithAdminNotRequired().Huma(api) - listClients := oidcOperation("list-oidc-clients", http.MethodGet, "/api/oidc/clients", "List OIDC clients") - adminAuth(&listClients) - httpapi.Register(api, listClients, controller.listClientsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-oidc-clients", + Method: http.MethodGet, + Path: "/api/oidc/clients", + Summary: "List OIDC clients", + Tags: []string{"OIDC"}, + }, controller.listClientsHandler, adminAuth) - createClient := oidcOperation("create-oidc-client", http.MethodPost, "/api/oidc/clients", "Create OIDC client") - createClient.DefaultStatus = http.StatusCreated - adminAuth(&createClient) - httpapi.Register(api, createClient, controller.createClientHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "create-oidc-client", + Method: http.MethodPost, + Path: "/api/oidc/clients", + Summary: "Create OIDC client", + Tags: []string{"OIDC"}, + DefaultStatus: http.StatusCreated, + }, controller.createClientHandler, adminAuth) - getClient := oidcOperation("get-oidc-client", http.MethodGet, "/api/oidc/clients/{id}", "Get OIDC client") - adminAuth(&getClient) - httpapi.Register(api, getClient, controller.getClientHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-oidc-client", + Method: http.MethodGet, + Path: "/api/oidc/clients/{id}", + Summary: "Get OIDC client", + Tags: []string{"OIDC"}, + }, controller.getClientHandler, adminAuth) - httpapi.Register(api, oidcOperation("get-oidc-client-metadata", http.MethodGet, "/api/oidc/clients/{id}/meta", "Get OIDC client metadata"), controller.getClientMetaDataHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-oidc-client-metadata", + Method: http.MethodGet, + Path: "/api/oidc/clients/{id}/meta", + Summary: "Get OIDC client metadata", + Tags: []string{"OIDC"}, + }, controller.getClientMetaDataHandler) - updateClient := oidcOperation("update-oidc-client", http.MethodPut, "/api/oidc/clients/{id}", "Update OIDC client") - adminAuth(&updateClient) - httpapi.Register(api, updateClient, controller.updateClientHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-oidc-client", + Method: http.MethodPut, + Path: "/api/oidc/clients/{id}", + Summary: "Update OIDC client", + Tags: []string{"OIDC"}, + }, controller.updateClientHandler, adminAuth) - deleteClient := oidcOperation("delete-oidc-client", http.MethodDelete, "/api/oidc/clients/{id}", "Delete OIDC client") - deleteClient.DefaultStatus = http.StatusNoContent - adminAuth(&deleteClient) - httpapi.Register(api, deleteClient, controller.deleteClientHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "delete-oidc-client", + Method: http.MethodDelete, + Path: "/api/oidc/clients/{id}", + Summary: "Delete OIDC client", + Tags: []string{"OIDC"}, + DefaultStatus: http.StatusNoContent, + }, controller.deleteClientHandler, adminAuth) - allowedGroups := oidcOperation("update-oidc-client-allowed-user-groups", http.MethodPut, "/api/oidc/clients/{id}/allowed-user-groups", "Update allowed user groups") - adminAuth(&allowedGroups) - httpapi.Register(api, allowedGroups, controller.updateAllowedUserGroupsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-oidc-client-allowed-user-groups", + Method: http.MethodPut, + Path: "/api/oidc/clients/{id}/allowed-user-groups", + Summary: "Update allowed user groups", + Tags: []string{"OIDC"}, + }, controller.updateAllowedUserGroupsHandler, adminAuth) - createSecret := oidcOperation("create-oidc-client-secret", http.MethodPost, "/api/oidc/clients/{id}/secret", "Create client secret") - adminAuth(&createSecret) - httpapi.Register(api, createSecret, controller.createClientSecretHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "create-oidc-client-secret", + Method: http.MethodPost, + Path: "/api/oidc/clients/{id}/secret", + Summary: "Create client secret", + Tags: []string{"OIDC"}, + }, controller.createClientSecretHandler, adminAuth) - httpapi.Register(api, oidcOperation("get-oidc-client-logo", http.MethodGet, "/api/oidc/clients/{id}/logo", "Get client logo"), controller.getClientLogoHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-oidc-client-logo", + Method: http.MethodGet, + Path: "/api/oidc/clients/{id}/logo", + Summary: "Get client logo", + Tags: []string{"OIDC"}, + }, controller.getClientLogoHandler) - deleteLogo := oidcOperation("delete-oidc-client-logo", http.MethodDelete, "/api/oidc/clients/{id}/logo", "Delete client logo") - deleteLogo.DefaultStatus = http.StatusNoContent - adminAuth(&deleteLogo) - httpapi.Register(api, deleteLogo, controller.deleteClientLogoHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "delete-oidc-client-logo", + Method: http.MethodDelete, + Path: "/api/oidc/clients/{id}/logo", + Summary: "Delete client logo", + Tags: []string{"OIDC"}, + DefaultStatus: http.StatusNoContent, + }, controller.deleteClientLogoHandler, adminAuth) - updateLogo := oidcOperation("update-oidc-client-logo", http.MethodPost, "/api/oidc/clients/{id}/logo", "Update client logo") - updateLogo.DefaultStatus = http.StatusNoContent - adminAuth(&updateLogo) - updateLogo.Middlewares = append(updateLogo.Middlewares, fileSizeLimitMiddleware.Huma(api, 2<<20)) - httpapi.Register(api, updateLogo, controller.updateClientLogoHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-oidc-client-logo", + Method: http.MethodPost, + Path: "/api/oidc/clients/{id}/logo", + Summary: "Update client logo", + Tags: []string{"OIDC"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateClientLogoHandler, adminAuth, httpapi.WithMiddleware(fileSizeLimitMiddleware.Huma(api, 2<<20))) - preview := oidcOperation("preview-oidc-client-data", http.MethodGet, "/api/oidc/clients/{id}/preview/{userId}", "Preview OIDC client data for user") - adminAuth(&preview) - httpapi.Register(api, preview, controller.getClientPreviewHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "preview-oidc-client-data", + Method: http.MethodGet, + Path: "/api/oidc/clients/{id}/preview/{userId}", + Summary: "Preview OIDC client data for user", + Tags: []string{"OIDC"}, + }, controller.getClientPreviewHandler, adminAuth) - ownAuthorized := oidcOperation("list-own-authorized-oidc-clients", http.MethodGet, "/api/oidc/users/me/authorized-clients", "List authorized clients for current user") - userAuth(&ownAuthorized) - httpapi.Register(api, ownAuthorized, controller.listOwnAuthorizedClientsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-own-authorized-oidc-clients", + Method: http.MethodGet, + Path: "/api/oidc/users/me/authorized-clients", + Summary: "List authorized clients for current user", + Tags: []string{"OIDC"}, + }, controller.listOwnAuthorizedClientsHandler, userAuth) - userAuthorized := oidcOperation("list-user-authorized-oidc-clients", http.MethodGet, "/api/oidc/users/{id}/authorized-clients", "List authorized clients for a user") - adminAuth(&userAuthorized) - httpapi.Register(api, userAuthorized, controller.listAuthorizedClientsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-user-authorized-oidc-clients", + Method: http.MethodGet, + Path: "/api/oidc/users/{id}/authorized-clients", + Summary: "List authorized clients for a user", + Tags: []string{"OIDC"}, + }, controller.listAuthorizedClientsHandler, adminAuth) - revokeAuthorization := oidcOperation("revoke-own-oidc-client-authorization", http.MethodDelete, "/api/oidc/users/me/authorized-clients/{clientId}", "Revoke authorization for an OIDC client") - revokeAuthorization.DefaultStatus = http.StatusNoContent - userAuth(&revokeAuthorization) - httpapi.Register(api, revokeAuthorization, controller.revokeOwnClientAuthorizationHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "revoke-own-oidc-client-authorization", + Method: http.MethodDelete, + Path: "/api/oidc/users/me/authorized-clients/{clientId}", + Summary: "Revoke authorization for an OIDC client", + Tags: []string{"OIDC"}, + DefaultStatus: http.StatusNoContent, + }, controller.revokeOwnClientAuthorizationHandler, userAuth) - accessibleClients := oidcOperation("list-own-accessible-oidc-clients", http.MethodGet, "/api/oidc/users/me/clients", "List accessible OIDC clients for current user") - userAuth(&accessibleClients) - httpapi.Register(api, accessibleClients, controller.listOwnAccessibleClientsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-own-accessible-oidc-clients", + Method: http.MethodGet, + Path: "/api/oidc/users/me/clients", + Summary: "List accessible OIDC clients for current user", + Tags: []string{"OIDC"}, + }, controller.listOwnAccessibleClientsHandler, userAuth) - clientSCIM := oidcOperation("get-oidc-client-scim-service-provider", http.MethodGet, "/api/oidc/clients/{id}/scim-service-provider", "Get SCIM service provider") - adminAuth(&clientSCIM) - httpapi.Register(api, clientSCIM, controller.getClientScimServiceProviderHandler) -} - -func oidcOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"OIDC"}} + httpapi.Register(api, huma.Operation{ + OperationID: "get-oidc-client-scim-service-provider", + Method: http.MethodGet, + Path: "/api/oidc/clients/{id}/scim-service-provider", + Summary: "Get SCIM service provider", + Tags: []string{"OIDC"}, + }, controller.getClientScimServiceProviderHandler, adminAuth) } type OidcController struct { diff --git a/backend/internal/controller/scim_controller.go b/backend/internal/controller/scim_controller.go index bfb73ea9..740f43a4 100644 --- a/backend/internal/controller/scim_controller.go +++ b/backend/internal/controller/scim_controller.go @@ -29,28 +29,40 @@ func NewScimController(api huma.API, authMiddleware *middleware.AuthMiddleware, controller := &ScimController{scimService: scimService} auth := authMiddleware.Huma(api) - createOperation := scimOperation("create-scim-service-provider", http.MethodPost, "/api/scim/service-provider", "Create SCIM service provider") - createOperation.DefaultStatus = http.StatusCreated - auth(&createOperation) - httpapi.Register(api, createOperation, controller.createServiceProviderHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "create-scim-service-provider", + Method: http.MethodPost, + Path: "/api/scim/service-provider", + Summary: "Create SCIM service provider", + Tags: []string{"SCIM"}, + DefaultStatus: http.StatusCreated, + }, controller.createServiceProviderHandler, auth) - syncOperation := scimOperation("sync-scim-service-provider", http.MethodPost, "/api/scim/service-provider/{id}/sync", "Sync SCIM service provider") - syncOperation.DefaultStatus = http.StatusOK - auth(&syncOperation) - httpapi.Register(api, syncOperation, controller.syncServiceProviderHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "sync-scim-service-provider", + Method: http.MethodPost, + Path: "/api/scim/service-provider/{id}/sync", + Summary: "Sync SCIM service provider", + Tags: []string{"SCIM"}, + DefaultStatus: http.StatusOK, + }, controller.syncServiceProviderHandler, auth) - updateOperation := scimOperation("update-scim-service-provider", http.MethodPut, "/api/scim/service-provider/{id}", "Update SCIM service provider") - auth(&updateOperation) - httpapi.Register(api, updateOperation, controller.updateServiceProviderHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-scim-service-provider", + Method: http.MethodPut, + Path: "/api/scim/service-provider/{id}", + Summary: "Update SCIM service provider", + Tags: []string{"SCIM"}, + }, controller.updateServiceProviderHandler, auth) - deleteOperation := scimOperation("delete-scim-service-provider", http.MethodDelete, "/api/scim/service-provider/{id}", "Delete SCIM service provider") - deleteOperation.DefaultStatus = http.StatusNoContent - auth(&deleteOperation) - httpapi.Register(api, deleteOperation, controller.deleteServiceProviderHandler) -} - -func scimOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"SCIM"}} + httpapi.Register(api, huma.Operation{ + OperationID: "delete-scim-service-provider", + Method: http.MethodDelete, + Path: "/api/scim/service-provider/{id}", + Summary: "Delete SCIM service provider", + Tags: []string{"SCIM"}, + DefaultStatus: http.StatusNoContent, + }, controller.deleteServiceProviderHandler, auth) } type ScimController struct { diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index db491f4d..b7a686f7 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -106,114 +106,202 @@ func NewUserController(api huma.API, authMiddleware *middleware.AuthMiddleware, adminAuth := authMiddleware.Huma(api) userAuth := authMiddleware.WithAdminNotRequired().Huma(api) - listUsers := userOperation("list-users", http.MethodGet, "/api/users", "List users") - adminAuth(&listUsers) - httpapi.Register(api, listUsers, controller.listUsersHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-users", + Method: http.MethodGet, + Path: "/api/users", + Summary: "List users", + Tags: []string{"Users"}, + }, controller.listUsersHandler, adminAuth) - getCurrentUser := userOperation("get-current-user", http.MethodGet, "/api/users/me", "Get current user") - userAuth(&getCurrentUser) - httpapi.Register(api, getCurrentUser, controller.getCurrentUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-current-user", + Method: http.MethodGet, + Path: "/api/users/me", + Summary: "Get current user", + Tags: []string{"Users"}, + }, controller.getCurrentUserHandler, userAuth) - getUser := userOperation("get-user", http.MethodGet, "/api/users/{id}", "Get user by ID") - adminAuth(&getUser) - httpapi.Register(api, getUser, controller.getUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-user", + Method: http.MethodGet, + Path: "/api/users/{id}", + Summary: "Get user by ID", + Tags: []string{"Users"}, + }, controller.getUserHandler, adminAuth) - createUser := userOperation("create-user", http.MethodPost, "/api/users", "Create user") - createUser.DefaultStatus = http.StatusCreated - adminAuth(&createUser) - httpapi.Register(api, createUser, controller.createUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "create-user", + Method: http.MethodPost, + Path: "/api/users", + Summary: "Create user", + Tags: []string{"Users"}, + DefaultStatus: http.StatusCreated, + }, controller.createUserHandler, adminAuth) - updateUser := userOperation("update-user", http.MethodPut, "/api/users/{id}", "Update user") - adminAuth(&updateUser) - httpapi.Register(api, updateUser, controller.updateUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-user", + Method: http.MethodPut, + Path: "/api/users/{id}", + Summary: "Update user", + Tags: []string{"Users"}, + }, controller.updateUserHandler, adminAuth) - updateCurrentUser := userOperation("update-current-user", http.MethodPut, "/api/users/me", "Update current user") - userAuth(&updateCurrentUser) - httpapi.Register(api, updateCurrentUser, controller.updateCurrentUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-current-user", + Method: http.MethodPut, + Path: "/api/users/me", + Summary: "Update current user", + Tags: []string{"Users"}, + }, controller.updateCurrentUserHandler, userAuth) - getGroups := userOperation("get-user-groups", http.MethodGet, "/api/users/{id}/groups", "Get user groups") - adminAuth(&getGroups) - httpapi.Register(api, getGroups, controller.getUserGroupsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-user-groups", + Method: http.MethodGet, + Path: "/api/users/{id}/groups", + Summary: "Get user groups", + Tags: []string{"Users"}, + }, controller.getUserGroupsHandler, adminAuth) - listCredentials := userOperation("list-user-webauthn-credentials", http.MethodGet, "/api/users/{id}/webauthn-credentials", "List user passkeys") - adminAuth(&listCredentials) - httpapi.Register(api, listCredentials, controller.listUserWebauthnCredentialsHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "list-user-webauthn-credentials", + Method: http.MethodGet, + Path: "/api/users/{id}/webauthn-credentials", + Summary: "List user passkeys", + Tags: []string{"Users"}, + }, controller.listUserWebauthnCredentialsHandler, adminAuth) - deleteUser := userOperation("delete-user", http.MethodDelete, "/api/users/{id}", "Delete user") - deleteUser.DefaultStatus = http.StatusNoContent - adminAuth(&deleteUser) - httpapi.Register(api, deleteUser, controller.deleteUserHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "delete-user", + Method: http.MethodDelete, + Path: "/api/users/{id}", + Summary: "Delete user", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.deleteUserHandler, adminAuth) - deleteCredential := userOperation("delete-user-webauthn-credential", http.MethodDelete, "/api/users/{id}/webauthn-credentials/{credentialId}", "Delete user passkey") - deleteCredential.DefaultStatus = http.StatusNoContent - adminAuth(&deleteCredential) - httpapi.Register(api, deleteCredential, controller.deleteUserWebauthnCredentialHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "delete-user-webauthn-credential", + Method: http.MethodDelete, + Path: "/api/users/{id}/webauthn-credentials/{credentialId}", + Summary: "Delete user passkey", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.deleteUserWebauthnCredentialHandler, adminAuth) - updateGroups := userOperation("update-user-groups", http.MethodPut, "/api/users/{id}/user-groups", "Update user groups") - adminAuth(&updateGroups) - httpapi.Register(api, updateGroups, controller.updateUserGroups) + httpapi.Register(api, huma.Operation{ + OperationID: "update-user-groups", + Method: http.MethodPut, + Path: "/api/users/{id}/user-groups", + Summary: "Update user groups", + Tags: []string{"Users"}, + }, controller.updateUserGroups, adminAuth) - httpapi.Register(api, userOperation("get-user-profile-picture", http.MethodGet, "/api/users/{id}/profile-picture.png", "Get user profile picture"), controller.getUserProfilePictureHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-user-profile-picture", + Method: http.MethodGet, + Path: "/api/users/{id}/profile-picture.png", + Summary: "Get user profile picture", + Tags: []string{"Users"}, + }, controller.getUserProfilePictureHandler) - updatePicture := userOperation("update-user-profile-picture", http.MethodPut, "/api/users/{id}/profile-picture", "Update user profile picture") - updatePicture.DefaultStatus = http.StatusNoContent - adminAuth(&updatePicture) - httpapi.Register(api, updatePicture, controller.updateUserProfilePictureHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-user-profile-picture", + Method: http.MethodPut, + Path: "/api/users/{id}/profile-picture", + Summary: "Update user profile picture", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateUserProfilePictureHandler, adminAuth) - updateCurrentPicture := userOperation("update-current-user-profile-picture", http.MethodPut, "/api/users/me/profile-picture", "Update current user profile picture") - updateCurrentPicture.DefaultStatus = http.StatusNoContent - userAuth(&updateCurrentPicture) - httpapi.Register(api, updateCurrentPicture, controller.updateCurrentUserProfilePictureHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "update-current-user-profile-picture", + Method: http.MethodPut, + Path: "/api/users/me/profile-picture", + Summary: "Update current user profile picture", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.updateCurrentUserProfilePictureHandler, userAuth) - createOwnToken := userOperation("create-own-one-time-access-token", http.MethodPost, "/api/users/me/one-time-access-token", "Create one-time access token for current user") - createOwnToken.DefaultStatus = http.StatusCreated - userAuth(&createOwnToken) - httpapi.Register(api, createOwnToken, controller.createOwnOneTimeAccessTokenHandler) + 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) - createAdminToken := userOperation("create-user-one-time-access-token", http.MethodPost, "/api/users/{id}/one-time-access-token", "Create one-time access token for user") - createAdminToken.DefaultStatus = http.StatusCreated - adminAuth(&createAdminToken) - httpapi.Register(api, createAdminToken, controller.createAdminOneTimeAccessTokenHandler) + 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) - adminEmail := userOperation("request-user-one-time-access-email", http.MethodPost, "/api/users/{id}/one-time-access-email", "Request one-time access email for user") - adminEmail.DefaultStatus = http.StatusNoContent - adminAuth(&adminEmail) - httpapi.Register(api, adminEmail, controller.requestOneTimeAccessEmailAsAdminHandler) + 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) - exchangeToken := userOperation("exchange-one-time-access-token", http.MethodPost, "/api/one-time-access-token/{token}", "Exchange one-time access token") - exchangeToken.Middlewares = append(exchangeToken.Middlewares, rateLimitMiddleware.Huma(api, middleware.RateLimitOneTimeAccessToken)) - httpapi.Register(api, exchangeToken, controller.exchangeOneTimeAccessTokenHandler) + 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))) - requestEmail := userOperation("request-one-time-access-email", http.MethodPost, "/api/one-time-access-email", "Request one-time access email") - requestEmail.DefaultStatus = http.StatusNoContent - requestEmail.Middlewares = append(requestEmail.Middlewares, rateLimitMiddleware.Huma(api, middleware.RateLimitOneTimeAccessEmail)) - httpapi.Register(api, requestEmail, controller.requestOneTimeAccessEmailAsUnauthenticatedUserHandler) + 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))) - resetPicture := userOperation("reset-user-profile-picture", http.MethodDelete, "/api/users/{id}/profile-picture", "Reset user profile picture") - resetPicture.DefaultStatus = http.StatusNoContent - adminAuth(&resetPicture) - httpapi.Register(api, resetPicture, controller.resetUserProfilePictureHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "reset-user-profile-picture", + Method: http.MethodDelete, + Path: "/api/users/{id}/profile-picture", + Summary: "Reset user profile picture", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.resetUserProfilePictureHandler, adminAuth) - resetCurrentPicture := userOperation("reset-current-user-profile-picture", http.MethodDelete, "/api/users/me/profile-picture", "Reset current user profile picture") - resetCurrentPicture.DefaultStatus = http.StatusNoContent - userAuth(&resetCurrentPicture) - httpapi.Register(api, resetCurrentPicture, controller.resetCurrentUserProfilePictureHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "reset-current-user-profile-picture", + Method: http.MethodDelete, + Path: "/api/users/me/profile-picture", + Summary: "Reset current user profile picture", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.resetCurrentUserProfilePictureHandler, userAuth) - sendVerification := userOperation("send-email-verification", http.MethodPost, "/api/users/me/send-email-verification", "Send email verification") - sendVerification.DefaultStatus = http.StatusNoContent - sendVerification.Middlewares = append(sendVerification.Middlewares, rateLimitMiddleware.Huma(api, middleware.RateLimitSendEmailVerification)) - userAuth(&sendVerification) - httpapi.Register(api, sendVerification, controller.sendEmailVerificationHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "send-email-verification", + Method: http.MethodPost, + Path: "/api/users/me/send-email-verification", + Summary: "Send email verification", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.sendEmailVerificationHandler, httpapi.WithMiddleware(rateLimitMiddleware.Huma(api, middleware.RateLimitSendEmailVerification)), userAuth) - verifyEmail := userOperation("verify-email", http.MethodPost, "/api/users/me/verify-email", "Verify email") - verifyEmail.DefaultStatus = http.StatusNoContent - verifyEmail.Middlewares = append(verifyEmail.Middlewares, rateLimitMiddleware.Huma(api, middleware.RateLimitVerifyEmail)) - userAuth(&verifyEmail) - httpapi.Register(api, verifyEmail, controller.verifyEmailHandler) -} - -func userOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"Users"}} + httpapi.Register(api, huma.Operation{ + OperationID: "verify-email", + Method: http.MethodPost, + Path: "/api/users/me/verify-email", + Summary: "Verify email", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, controller.verifyEmailHandler, httpapi.WithMiddleware(rateLimitMiddleware.Huma(api, middleware.RateLimitVerifyEmail)), userAuth) } type UserController struct { diff --git a/backend/internal/controller/user_group_controller.go b/backend/internal/controller/user_group_controller.go index 8cbb1a5e..9da2a475 100644 --- a/backend/internal/controller/user_group_controller.go +++ b/backend/internal/controller/user_group_controller.go @@ -46,39 +46,63 @@ func NewUserGroupController(api huma.API, authMiddleware *middleware.AuthMiddlew controller := &UserGroupController{UserGroupService: userGroupService} auth := authMiddleware.Huma(api) - listOperation := userGroupOperation("list-user-groups", http.MethodGet, "/api/user-groups", "List user groups") - auth(&listOperation) - httpapi.Register(api, listOperation, controller.list) + httpapi.Register(api, huma.Operation{ + OperationID: "list-user-groups", + Method: http.MethodGet, + Path: "/api/user-groups", + Summary: "List user groups", + Tags: []string{"User Groups"}, + }, controller.list, auth) - getOperation := userGroupOperation("get-user-group", http.MethodGet, "/api/user-groups/{id}", "Get user group by ID") - auth(&getOperation) - httpapi.Register(api, getOperation, controller.get) + httpapi.Register(api, huma.Operation{ + OperationID: "get-user-group", + Method: http.MethodGet, + Path: "/api/user-groups/{id}", + Summary: "Get user group by ID", + Tags: []string{"User Groups"}, + }, controller.get, auth) - createOperation := userGroupOperation("create-user-group", http.MethodPost, "/api/user-groups", "Create user group") - createOperation.DefaultStatus = http.StatusCreated - auth(&createOperation) - httpapi.Register(api, createOperation, controller.create) + httpapi.Register(api, huma.Operation{ + OperationID: "create-user-group", + Method: http.MethodPost, + Path: "/api/user-groups", + Summary: "Create user group", + Tags: []string{"User Groups"}, + DefaultStatus: http.StatusCreated, + }, controller.create, auth) - updateOperation := userGroupOperation("update-user-group", http.MethodPut, "/api/user-groups/{id}", "Update user group") - auth(&updateOperation) - httpapi.Register(api, updateOperation, controller.update) + httpapi.Register(api, huma.Operation{ + OperationID: "update-user-group", + Method: http.MethodPut, + Path: "/api/user-groups/{id}", + Summary: "Update user group", + Tags: []string{"User Groups"}, + }, controller.update, auth) - deleteOperation := userGroupOperation("delete-user-group", http.MethodDelete, "/api/user-groups/{id}", "Delete user group") - deleteOperation.DefaultStatus = http.StatusNoContent - auth(&deleteOperation) - httpapi.Register(api, deleteOperation, controller.delete) + httpapi.Register(api, huma.Operation{ + OperationID: "delete-user-group", + Method: http.MethodDelete, + Path: "/api/user-groups/{id}", + Summary: "Delete user group", + Tags: []string{"User Groups"}, + DefaultStatus: http.StatusNoContent, + }, controller.delete, auth) - usersOperation := userGroupOperation("update-user-group-users", http.MethodPut, "/api/user-groups/{id}/users", "Update users in a group") - auth(&usersOperation) - httpapi.Register(api, usersOperation, controller.updateUsers) + httpapi.Register(api, huma.Operation{ + OperationID: "update-user-group-users", + Method: http.MethodPut, + Path: "/api/user-groups/{id}/users", + Summary: "Update users in a group", + Tags: []string{"User Groups"}, + }, controller.updateUsers, auth) - clientsOperation := userGroupOperation("update-user-group-allowed-oidc-clients", http.MethodPut, "/api/user-groups/{id}/allowed-oidc-clients", "Update allowed OIDC clients") - auth(&clientsOperation) - httpapi.Register(api, clientsOperation, controller.updateAllowedOIDCClients) -} - -func userGroupOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"User Groups"}} + httpapi.Register(api, huma.Operation{ + OperationID: "update-user-group-allowed-oidc-clients", + Method: http.MethodPut, + Path: "/api/user-groups/{id}/allowed-oidc-clients", + Summary: "Update allowed OIDC clients", + Tags: []string{"User Groups"}, + }, controller.updateAllowedOIDCClients, auth) } type UserGroupController struct { diff --git a/backend/internal/controller/version_controller.go b/backend/internal/controller/version_controller.go index d19aec11..4e2c6cb5 100644 --- a/backend/internal/controller/version_controller.go +++ b/backend/internal/controller/version_controller.go @@ -22,6 +22,7 @@ type versionOutput struct { // NewVersionController registers version-related routes func NewVersionController(api huma.API, authMiddleware *middleware.AuthMiddleware, versionService *service.VersionService) { vc := &VersionController{versionService: versionService} + userAuth := authMiddleware.WithAdminNotRequired().Huma(api) httpapi.Register(api, huma.Operation{ OperationID: "get-latest-version", @@ -31,15 +32,13 @@ func NewVersionController(api huma.API, authMiddleware *middleware.AuthMiddlewar Tags: []string{"Version"}, }, vc.getLatestVersionHandler) - currentOperation := huma.Operation{ + httpapi.Register(api, huma.Operation{ OperationID: "get-current-version", Method: http.MethodGet, Path: "/api/version/current", Summary: "Get current deployed version of Pocket ID", Tags: []string{"Version"}, - } - authMiddleware.WithAdminNotRequired().Huma(api)(¤tOperation) - httpapi.Register(api, currentOperation, vc.getCurrentVersionHandler) + }, vc.getCurrentVersionHandler, userAuth) } type VersionController struct { diff --git a/backend/internal/controller/well_known_controller.go b/backend/internal/controller/well_known_controller.go index e7b8af41..2dc65546 100644 --- a/backend/internal/controller/well_known_controller.go +++ b/backend/internal/controller/well_known_controller.go @@ -32,8 +32,21 @@ func NewWellKnownController(api huma.API, jwtService *service.JwtService) { return } - httpapi.Register(api, huma.Operation{OperationID: "get-jwks", Method: http.MethodGet, Path: "/.well-known/jwks.json", Summary: "Get JSON Web Key Set", Tags: []string{"Well Known"}}, controller.jwksHandler) - httpapi.Register(api, huma.Operation{OperationID: "get-openid-configuration", Method: http.MethodGet, Path: "/.well-known/openid-configuration", Summary: "Get OpenID Connect discovery configuration", Tags: []string{"Well Known"}}, controller.openIDConfigurationHandler) + httpapi.Register(api, huma.Operation{ + OperationID: "get-jwks", + Method: http.MethodGet, + Path: "/.well-known/jwks.json", + Summary: "Get JSON Web Key Set", + Tags: []string{"Well Known"}, + }, controller.jwksHandler) + + httpapi.Register(api, huma.Operation{ + OperationID: "get-openid-configuration", + Method: http.MethodGet, + Path: "/.well-known/openid-configuration", + Summary: "Get OpenID Connect discovery configuration", + Tags: []string{"Well Known"}, + }, controller.openIDConfigurationHandler) } type WellKnownController struct { diff --git a/backend/internal/oidc/module.go b/backend/internal/oidc/module.go index ec736771..3e6755c1 100644 --- a/backend/internal/oidc/module.go +++ b/backend/internal/oidc/module.go @@ -113,32 +113,125 @@ func (m *Module) RegisterRawRoutes(rootGroup *gin.RouterGroup, apiGroup *gin.Rou apiGroup.POST("/oidc/end-session", optionalBrowserAuth, m.endSessionHandler.endSession) apiGroup.POST("/oidc/device/authorize", m.deviceHandler.authorizeDevice) - tags := []string{"OIDC Protocol"} - httpapi.AddRawOperation(api, "authorize-get", http.MethodGet, "/authorize", "Authorize", tags, nil, http.StatusOK, http.StatusFound) - httpapi.AddRawOperation(api, "authorize-post", http.MethodPost, "/authorize", "Authorize", tags, nil, http.StatusOK, http.StatusFound) - httpapi.AddRawOperation(api, "pushed-authorization-request", http.MethodPost, "/api/oidc/par", "Create pushed authorization request", tags, []map[string][]string{{"OIDCClientBasic": {}}}) - httpapi.AddRawOperation(api, "oidc-token", http.MethodPost, "/api/oidc/token", "Exchange an OIDC token", tags, []map[string][]string{{"OIDCClientBasic": {}}}) - httpapi.AddRawOperation(api, "oidc-userinfo-get", http.MethodGet, "/api/oidc/userinfo", "Get OIDC user info", tags, []map[string][]string{{"OIDCAccessToken": {}}}) - httpapi.AddRawOperation(api, "oidc-userinfo-post", http.MethodPost, "/api/oidc/userinfo", "Get OIDC user info", tags, []map[string][]string{{"OIDCAccessToken": {}}}) - httpapi.AddRawOperation(api, "oidc-introspection", http.MethodPost, "/api/oidc/introspect", "Introspect an OIDC token", tags, []map[string][]string{{"OIDCClientBasic": {}}}) - httpapi.AddRawOperation(api, "oidc-end-session-get", http.MethodGet, "/api/oidc/end-session", "End an OIDC session", tags, nil, http.StatusFound) - httpapi.AddRawOperation(api, "oidc-end-session-post", http.MethodPost, "/api/oidc/end-session", "End an OIDC session", tags, nil, http.StatusFound) - httpapi.AddRawOperation(api, "oidc-device-authorization", http.MethodPost, "/api/oidc/device/authorize", "Create device authorization", tags, []map[string][]string{{"OIDCClientBasic": {}}}) + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "authorize-get", + Method: http.MethodGet, + Path: "/authorize", + Summary: "Authorize", + Tags: []string{"OIDC Protocol"}, + }, http.StatusOK, http.StatusFound) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "authorize-post", + Method: http.MethodPost, + Path: "/authorize", + Summary: "Authorize", + Tags: []string{"OIDC Protocol"}, + }, http.StatusOK, http.StatusFound) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "pushed-authorization-request", + Method: http.MethodPost, + Path: "/api/oidc/par", + Summary: "Create pushed authorization request", + Tags: []string{"OIDC Protocol"}, + Security: []map[string][]string{{"OIDCClientBasic": {}}}, + }) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "oidc-token", + Method: http.MethodPost, + Path: "/api/oidc/token", + Summary: "Exchange an OIDC token", + Tags: []string{"OIDC Protocol"}, + Security: []map[string][]string{{"OIDCClientBasic": {}}}, + }) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "oidc-userinfo-get", + Method: http.MethodGet, + Path: "/api/oidc/userinfo", + Summary: "Get OIDC user info", + Tags: []string{"OIDC Protocol"}, + Security: []map[string][]string{{"OIDCAccessToken": {}}}, + }) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "oidc-userinfo-post", + Method: http.MethodPost, + Path: "/api/oidc/userinfo", + Summary: "Get OIDC user info", + Tags: []string{"OIDC Protocol"}, + Security: []map[string][]string{{"OIDCAccessToken": {}}}, + }) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "oidc-introspection", + Method: http.MethodPost, + Path: "/api/oidc/introspect", + Summary: "Introspect an OIDC token", + Tags: []string{"OIDC Protocol"}, + Security: []map[string][]string{{"OIDCClientBasic": {}}}, + }) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "oidc-end-session-get", + Method: http.MethodGet, + Path: "/api/oidc/end-session", + Summary: "End an OIDC session", + Tags: []string{"OIDC Protocol"}, + }, http.StatusFound) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "oidc-end-session-post", + Method: http.MethodPost, + Path: "/api/oidc/end-session", + Summary: "End an OIDC session", + Tags: []string{"OIDC Protocol"}, + }, http.StatusFound) + + httpapi.AddRawOperation(api, huma.Operation{ + OperationID: "oidc-device-authorization", + Method: http.MethodPost, + Path: "/api/oidc/device/authorize", + Summary: "Create device authorization", + Tags: []string{"OIDC Protocol"}, + Security: []map[string][]string{{"OIDCClientBasic": {}}}, + }) } // RegisterTypedRoutes mounts JSON interaction and device verification endpoints func (m *Module) RegisterTypedRoutes(api huma.API, browserAuth func(*huma.Operation)) { - httpapi.Register(api, huma.Operation{OperationID: "get-oidc-interaction", Method: http.MethodGet, Path: "/api/oidc/interactions/{id}", Summary: "Get OIDC interaction", Tags: []string{"OIDC Interactions"}}, m.authorizationHandler.getInteractionSession) + httpapi.Register(api, huma.Operation{ + OperationID: "get-oidc-interaction", + Method: http.MethodGet, + Path: "/api/oidc/interactions/{id}", + Summary: "Get OIDC interaction", + Tags: []string{"OIDC Interactions"}, + }, m.authorizationHandler.getInteractionSession) - completeInteraction := huma.Operation{OperationID: "complete-oidc-interaction", Method: http.MethodPost, Path: "/api/oidc/interactions/{id}/complete", Summary: "Complete OIDC interaction", Tags: []string{"OIDC Interactions"}} - browserAuth(&completeInteraction) - httpapi.Register(api, completeInteraction, m.authorizationHandler.completeInteraction) + httpapi.Register(api, huma.Operation{ + OperationID: "complete-oidc-interaction", + Method: http.MethodPost, + Path: "/api/oidc/interactions/{id}/complete", + Summary: "Complete OIDC interaction", + Tags: []string{"OIDC Interactions"}, + }, m.authorizationHandler.completeInteraction, browserAuth) - verifyDevice := huma.Operation{OperationID: "verify-oidc-device-code", Method: http.MethodPost, Path: "/api/oidc/device/verify", Summary: "Verify OIDC device code", Tags: []string{"OIDC Protocol"}, DefaultStatus: http.StatusNoContent} - browserAuth(&verifyDevice) - httpapi.Register(api, verifyDevice, m.deviceHandler.verifyDeviceCode) + httpapi.Register(api, huma.Operation{ + OperationID: "verify-oidc-device-code", + Method: http.MethodPost, + Path: "/api/oidc/device/verify", + Summary: "Verify OIDC device code", + Tags: []string{"OIDC Protocol"}, + DefaultStatus: http.StatusNoContent, + }, m.deviceHandler.verifyDeviceCode, browserAuth) - deviceInfo := huma.Operation{OperationID: "get-oidc-device-info", Method: http.MethodGet, Path: "/api/oidc/device/info", Summary: "Get OIDC device code info", Tags: []string{"OIDC Protocol"}} - browserAuth(&deviceInfo) - httpapi.Register(api, deviceInfo, m.deviceHandler.deviceCodeInfo) + httpapi.Register(api, huma.Operation{ + OperationID: "get-oidc-device-info", + Method: http.MethodGet, + Path: "/api/oidc/device/info", + Summary: "Get OIDC device code info", + Tags: []string{"OIDC Protocol"}, + }, m.deviceHandler.deviceCodeInfo, browserAuth) } diff --git a/backend/internal/usersignup/module.go b/backend/internal/usersignup/module.go index 6160e6ec..da03931f 100644 --- a/backend/internal/usersignup/module.go +++ b/backend/internal/usersignup/module.go @@ -53,36 +53,55 @@ func New(deps Dependencies) *Module { // RegisterRoutes mounts the signup and signup-token management endpoints // adminAuth guards the admin token-management routes; signupRateLimit throttles public self-signup func (m *Module) RegisterRoutes(api huma.API, adminAuth func(*huma.Operation), signupRateLimit func(huma.Context, func(huma.Context))) { - createTokenOperation := signupOperation("create-signup-token", http.MethodPost, "/api/signup-tokens", "Create signup token") - createTokenOperation.DefaultStatus = http.StatusCreated - adminAuth(&createTokenOperation) - httpapi.Register(api, createTokenOperation, m.handler.createSignupToken) + httpapi.Register(api, huma.Operation{ + OperationID: "create-signup-token", + Method: http.MethodPost, + Path: "/api/signup-tokens", + Summary: "Create signup token", + Tags: []string{"Users"}, + DefaultStatus: http.StatusCreated, + }, m.handler.createSignupToken, adminAuth) - listTokensOperation := signupOperation("list-signup-tokens", http.MethodGet, "/api/signup-tokens", "List signup tokens") - adminAuth(&listTokensOperation) - httpapi.Register(api, listTokensOperation, m.handler.listSignupTokens) + httpapi.Register(api, huma.Operation{ + OperationID: "list-signup-tokens", + Method: http.MethodGet, + Path: "/api/signup-tokens", + Summary: "List signup tokens", + Tags: []string{"Users"}, + }, m.handler.listSignupTokens, adminAuth) - deleteTokenOperation := signupOperation("delete-signup-token", http.MethodDelete, "/api/signup-tokens/{id}", "Delete signup token") - deleteTokenOperation.DefaultStatus = http.StatusNoContent - adminAuth(&deleteTokenOperation) - httpapi.Register(api, deleteTokenOperation, m.handler.deleteSignupToken) + httpapi.Register(api, huma.Operation{ + OperationID: "delete-signup-token", + Method: http.MethodDelete, + Path: "/api/signup-tokens/{id}", + Summary: "Delete signup token", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, m.handler.deleteSignupToken, adminAuth) - selfSignupOperation := signupOperation("signup", http.MethodPost, "/api/signup", "Sign up") - selfSignupOperation.DefaultStatus = http.StatusCreated - selfSignupOperation.Middlewares = append(selfSignupOperation.Middlewares, signupRateLimit) - httpapi.Register(api, selfSignupOperation, m.handler.signup) + httpapi.Register(api, huma.Operation{ + OperationID: "signup", + Method: http.MethodPost, + Path: "/api/signup", + Summary: "Sign up", + Tags: []string{"Users"}, + DefaultStatus: http.StatusCreated, + }, m.handler.signup, httpapi.WithMiddleware(signupRateLimit)) - setupAvailableOperation := signupOperation("check-initial-admin-setup", http.MethodGet, "/api/signup/setup", "Check initial admin setup availability") - setupAvailableOperation.DefaultStatus = http.StatusNoContent - httpapi.Register(api, setupAvailableOperation, m.handler.checkInitialAdminSetupAvailable) + httpapi.Register(api, huma.Operation{ + OperationID: "check-initial-admin-setup", + Method: http.MethodGet, + Path: "/api/signup/setup", + Summary: "Check initial admin setup availability", + Tags: []string{"Users"}, + DefaultStatus: http.StatusNoContent, + }, m.handler.checkInitialAdminSetupAvailable) - httpapi.Register(api, signupOperationForInitialAdmin(), m.handler.signUpInitialAdmin) -} - -func signupOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"Users"}} -} - -func signupOperationForInitialAdmin() huma.Operation { - return signupOperation("signup-initial-admin", http.MethodPost, "/api/signup/setup", "Sign up initial admin user") + httpapi.Register(api, huma.Operation{ + OperationID: "signup-initial-admin", + Method: http.MethodPost, + Path: "/api/signup/setup", + Summary: "Sign up initial admin user", + Tags: []string{"Users"}, + }, m.handler.signUpInitialAdmin) } diff --git a/backend/internal/utils/huma/api_test.go b/backend/internal/utils/huma/api_test.go index 3f195ee5..9acf6bf1 100644 --- a/backend/internal/utils/huma/api_test.go +++ b/backend/internal/utils/huma/api_test.go @@ -132,7 +132,13 @@ func TestCookiesStreamingAndOpenAPI(t *testing.T) { _, _ = io.Copy(ctx.BodyWriter(), reader) }}, nil }) - AddRawOperation(api, "test-raw", http.MethodPost, "/api/test-raw", "Raw test", []string{"Test"}, nil) + AddRawOperation(api, huma.Operation{ + OperationID: "test-raw", + Method: http.MethodPost, + Path: "/api/test-raw", + Summary: "Raw test", + Tags: []string{"Test"}, + }) response := httptest.NewRecorder() router.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodPost, "/api/test-cookies", nil)) @@ -160,3 +166,34 @@ func TestCookiesStreamingAndOpenAPI(t *testing.T) { require.Contains(t, response.Header().Get("Content-Security-Policy"), "worker-src blob:") require.NotContains(t, response.Header().Get("Content-Security-Policy"), "script-src 'unsafe-inline'") } + +func TestRegisterAppliesDecoratorsInOrder(t *testing.T) { + router, api := newTestAPI(t) + var order []string + + first := func(operation *huma.Operation) { + operation.Middlewares = append(operation.Middlewares, func(ctx huma.Context, next func(huma.Context)) { + order = append(order, "first") + next(ctx) + }) + } + second := func(ctx huma.Context, next func(huma.Context)) { + order = append(order, "second") + next(ctx) + } + + Register(api, huma.Operation{ + OperationID: "test-decorator-order", + Method: http.MethodGet, + Path: "/api/test-decorator-order", + DefaultStatus: http.StatusNoContent, + }, func(context.Context, *struct{}) (*struct{}, error) { + order = append(order, "handler") + return &struct{}{}, nil + }, first, WithMiddleware(second)) + + response := httptest.NewRecorder() + router.ServeHTTP(response, httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/test-decorator-order", nil)) + require.Equal(t, http.StatusNoContent, response.Code) + require.Equal(t, []string{"first", "second", "handler"}, order) +} diff --git a/backend/internal/utils/huma/raw.go b/backend/internal/utils/huma/raw.go index 54088648..c62a6c79 100644 --- a/backend/internal/utils/huma/raw.go +++ b/backend/internal/utils/huma/raw.go @@ -8,7 +8,7 @@ import ( ) // AddRawOperation documents a Gin endpoint that must retain direct response control -func AddRawOperation(api huma.API, operationID, method, path, summary string, tags []string, security []map[string][]string, statuses ...int) { +func AddRawOperation(api huma.API, operation huma.Operation, statuses ...int) { if len(statuses) == 0 { statuses = []int{http.StatusOK} } @@ -16,13 +16,6 @@ func AddRawOperation(api huma.API, operationID, method, path, summary string, ta for _, status := range statuses { responses[strconv.Itoa(status)] = &huma.Response{Description: http.StatusText(status)} } - api.OpenAPI().AddOperation(&huma.Operation{ - OperationID: operationID, - Method: method, - Path: path, - Summary: summary, - Tags: tags, - Security: security, - Responses: responses, - }) + operation.Responses = responses + api.OpenAPI().AddOperation(&operation) } diff --git a/backend/internal/utils/huma/register.go b/backend/internal/utils/huma/register.go index 96c5e0ed..bb0cf6a5 100644 --- a/backend/internal/utils/huma/register.go +++ b/backend/internal/utils/huma/register.go @@ -6,8 +6,19 @@ import ( "github.com/danielgtaylor/huma/v2" ) +// WithMiddleware appends operation middleware at the point the decorator is applied +func WithMiddleware(middleware func(huma.Context, func(huma.Context))) func(*huma.Operation) { + return func(operation *huma.Operation) { + operation.Middlewares = append(operation.Middlewares, middleware) + } +} + // Register adds a typed operation while preserving Pocket ID error and body-reading behavior -func Register[I, O any](api huma.API, operation huma.Operation, handler func(context.Context, *I) (*O, error)) { +func Register[I, O any](api huma.API, operation huma.Operation, handler func(context.Context, *I) (*O, error), decorators ...func(*huma.Operation)) { + for _, decorator := range decorators { + decorator(&operation) + } + if operation.MaxBodyBytes == 0 { operation.MaxBodyBytes = -1 } diff --git a/backend/internal/webauthn/handler_test.go b/backend/internal/webauthn/handler_test.go index 3447c9f9..4934e0a8 100644 --- a/backend/internal/webauthn/handler_test.go +++ b/backend/internal/webauthn/handler_test.go @@ -26,7 +26,11 @@ func TestRequestWithBodyReconstructsUnderlyingRequest(t *testing.T) { type output struct { Body map[string]string } - httpapi.Register(api, huma.Operation{OperationID: "reconstruct-request", Method: http.MethodPost, Path: "/api/reconstruct"}, func(ctx context.Context, _ *input) (*output, error) { + httpapi.Register(api, huma.Operation{ + OperationID: "reconstruct-request", + Method: http.MethodPost, + Path: "/api/reconstruct", + }, func(ctx context.Context, _ *input) (*output, error) { request := requestWithBody(ctx, []byte(`{"credential":"value"}`)) body, err := io.ReadAll(request.Body) require.NoError(t, err) diff --git a/backend/internal/webauthn/module.go b/backend/internal/webauthn/module.go index a448b002..723cb1e6 100644 --- a/backend/internal/webauthn/module.go +++ b/backend/internal/webauthn/module.go @@ -56,47 +56,80 @@ func New(deps Dependencies) (*Module, error) { // RegisterRoutes mounts the WebAuthn registration, login and reauthentication endpoints func (m *Module) RegisterRoutes(api huma.API, userAuth func(*huma.Operation), loginRateLimit, reauthRateLimit func(huma.Context, func(huma.Context))) { - beginRegistration := webauthnOperation("begin-webauthn-registration", http.MethodGet, "/api/webauthn/register/start", "Begin WebAuthn registration") - userAuth(&beginRegistration) - httpapi.Register(api, beginRegistration, m.handler.beginRegistration) + httpapi.Register(api, huma.Operation{ + OperationID: "begin-webauthn-registration", + Method: http.MethodGet, + Path: "/api/webauthn/register/start", + Summary: "Begin WebAuthn registration", + Tags: []string{"WebAuthn"}, + }, m.handler.beginRegistration, userAuth) - verifyRegistration := webauthnOperation("finish-webauthn-registration", http.MethodPost, "/api/webauthn/register/finish", "Finish WebAuthn registration") - userAuth(&verifyRegistration) - httpapi.Register(api, verifyRegistration, m.handler.verifyRegistration) + httpapi.Register(api, huma.Operation{ + OperationID: "finish-webauthn-registration", + Method: http.MethodPost, + Path: "/api/webauthn/register/finish", + Summary: "Finish WebAuthn registration", + Tags: []string{"WebAuthn"}, + }, m.handler.verifyRegistration, userAuth) - httpapi.Register(api, webauthnOperation("begin-webauthn-login", http.MethodGet, "/api/webauthn/login/start", "Begin WebAuthn login"), m.handler.beginLogin) + httpapi.Register(api, huma.Operation{ + OperationID: "begin-webauthn-login", + Method: http.MethodGet, + Path: "/api/webauthn/login/start", + Summary: "Begin WebAuthn login", + Tags: []string{"WebAuthn"}, + }, m.handler.beginLogin) - verifyLogin := webauthnOperation("finish-webauthn-login", http.MethodPost, "/api/webauthn/login/finish", "Finish WebAuthn login") - verifyLogin.Middlewares = append(verifyLogin.Middlewares, loginRateLimit) - httpapi.Register(api, verifyLogin, m.handler.verifyLogin) + httpapi.Register(api, huma.Operation{ + OperationID: "finish-webauthn-login", + Method: http.MethodPost, + Path: "/api/webauthn/login/finish", + Summary: "Finish WebAuthn login", + Tags: []string{"WebAuthn"}, + }, m.handler.verifyLogin, httpapi.WithMiddleware(loginRateLimit)) - logout := webauthnOperation("webauthn-logout", http.MethodPost, "/api/webauthn/logout", "Log out") - logout.DefaultStatus = http.StatusNoContent - userAuth(&logout) - httpapi.Register(api, logout, m.handler.logout) + httpapi.Register(api, huma.Operation{ + OperationID: "webauthn-logout", + Method: http.MethodPost, + Path: "/api/webauthn/logout", + Summary: "Log out", + Tags: []string{"WebAuthn"}, + DefaultStatus: http.StatusNoContent, + }, m.handler.logout, userAuth) - reauthenticate := webauthnOperation("webauthn-reauthenticate", http.MethodPost, "/api/webauthn/reauthenticate", "Reauthenticate") - reauthenticate.DefaultStatus = http.StatusNoContent - userAuth(&reauthenticate) - reauthenticate.Middlewares = append(reauthenticate.Middlewares, reauthRateLimit) - httpapi.Register(api, reauthenticate, m.handler.reauthenticate) + httpapi.Register(api, huma.Operation{ + OperationID: "webauthn-reauthenticate", + Method: http.MethodPost, + Path: "/api/webauthn/reauthenticate", + Summary: "Reauthenticate", + Tags: []string{"WebAuthn"}, + DefaultStatus: http.StatusNoContent, + }, m.handler.reauthenticate, userAuth, httpapi.WithMiddleware(reauthRateLimit)) - listCredentials := webauthnOperation("list-webauthn-credentials", http.MethodGet, "/api/webauthn/credentials", "List WebAuthn credentials") - userAuth(&listCredentials) - httpapi.Register(api, listCredentials, m.handler.listCredentials) + httpapi.Register(api, huma.Operation{ + OperationID: "list-webauthn-credentials", + Method: http.MethodGet, + Path: "/api/webauthn/credentials", + Summary: "List WebAuthn credentials", + Tags: []string{"WebAuthn"}, + }, m.handler.listCredentials, userAuth) - updateCredential := webauthnOperation("update-webauthn-credential", http.MethodPatch, "/api/webauthn/credentials/{id}", "Update WebAuthn credential") - userAuth(&updateCredential) - httpapi.Register(api, updateCredential, m.handler.updateCredential) + httpapi.Register(api, huma.Operation{ + OperationID: "update-webauthn-credential", + Method: http.MethodPatch, + Path: "/api/webauthn/credentials/{id}", + Summary: "Update WebAuthn credential", + Tags: []string{"WebAuthn"}, + }, m.handler.updateCredential, userAuth) - deleteCredential := webauthnOperation("delete-webauthn-credential", http.MethodDelete, "/api/webauthn/credentials/{id}", "Delete WebAuthn credential") - deleteCredential.DefaultStatus = http.StatusNoContent - userAuth(&deleteCredential) - httpapi.Register(api, deleteCredential, m.handler.deleteCredential) -} - -func webauthnOperation(id, method, path, summary string) huma.Operation { - return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"WebAuthn"}} + httpapi.Register(api, huma.Operation{ + OperationID: "delete-webauthn-credential", + Method: http.MethodDelete, + Path: "/api/webauthn/credentials/{id}", + Summary: "Delete WebAuthn credential", + Tags: []string{"WebAuthn"}, + DefaultStatus: http.StatusNoContent, + }, m.handler.deleteCredential, userAuth) } // ConsumeReauthenticationToken implements the OIDC module's ReauthenticationTokenConsumer interface