From 86cf73b86c682ba5e627cfd83642e810ad0bad86 Mon Sep 17 00:00:00 2001 From: Anthony Clerici Date: Tue, 11 Aug 2026 10:26:48 -0700 Subject: [PATCH] docs: add error responses to openapi spec (#1684) --- .../controller/app_config_controller.go | 4 ++++ .../controller/app_images_controller.go | 14 +++++++++++++ .../controller/audit_log_controller.go | 4 ++++ .../controller/custom_claim_controller.go | 3 +++ .../internal/controller/healthz_controller.go | 3 +++ .../internal/controller/oidc_controller.go | 20 +++++++++++++++++++ .../internal/controller/scim_controller.go | 4 ++++ .../internal/controller/user_controller.go | 16 +++++++++++++++ .../controller/user_group_controller.go | 7 +++++++ .../internal/controller/version_controller.go | 3 +++ .../controller/well_known_controller.go | 3 +++ backend/internal/dto/error_dto.go | 11 ++++++++++ backend/internal/middleware/error_handler.go | 9 +-------- 13 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 backend/internal/dto/error_dto.go diff --git a/backend/internal/controller/app_config_controller.go b/backend/internal/controller/app_config_controller.go index 07c675e9..f1886d53 100644 --- a/backend/internal/controller/app_config_controller.go +++ b/backend/internal/controller/app_config_controller.go @@ -52,6 +52,7 @@ type AppConfigController struct { // @Accept json // @Produce json // @Success 200 {array} dto.PublicAppConfigVariableDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-configuration [get] func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) error { dbConfig, err := acc.appConfigService.GetConfig(c.Request.Context()) @@ -89,6 +90,7 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) error { // @Accept json // @Produce json // @Success 200 {array} dto.AppConfigVariableDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-configuration/all [get] func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) error { dbConfig, err := acc.appConfigService.GetConfig(c.Request.Context()) @@ -114,6 +116,7 @@ func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) error { // @Produce json // @Param body body dto.AppConfigUpdateDto true "Application Configuration" // @Success 200 {array} dto.AppConfigVariableDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-configuration [put] func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) error { var input dto.AppConfigUpdateDto @@ -140,6 +143,7 @@ func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) error { // @Description Send a test email to verify email configuration // @Tags Application Configuration // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-configuration/test-email [post] func (acc *AppConfigController) testEmailHandler(c *gin.Context) error { dbConfig, err := acc.appConfigService.GetConfig(c.Request.Context()) diff --git a/backend/internal/controller/app_images_controller.go b/backend/internal/controller/app_images_controller.go index 22866345..133b47f9 100644 --- a/backend/internal/controller/app_images_controller.go +++ b/backend/internal/controller/app_images_controller.go @@ -10,6 +10,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pocket-id/pocket-id/backend/internal/apperror" + _ "github.com/pocket-id/pocket-id/backend/internal/dto" "github.com/pocket-id/pocket-id/backend/internal/httpserver" "github.com/pocket-id/pocket-id/backend/internal/middleware" "github.com/pocket-id/pocket-id/backend/internal/service" @@ -55,6 +56,7 @@ type AppImagesController struct { // @Produce image/jpeg // @Produce image/svg+xml // @Success 200 {file} binary "Logo image" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/logo [get] func (c *AppImagesController) getLogoHandler(ctx *gin.Context) error { return c.getImage(ctx, logoImageName(ctx)) @@ -67,6 +69,7 @@ func (c *AppImagesController) getLogoHandler(ctx *gin.Context) error { // @Produce image/png // @Produce image/jpeg // @Success 200 {file} binary "Email logo image" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/email [get] func (c *AppImagesController) getEmailLogoHandler(ctx *gin.Context) error { return c.getImage(ctx, "logoEmail") @@ -79,6 +82,7 @@ func (c *AppImagesController) getEmailLogoHandler(ctx *gin.Context) error { // @Produce image/png // @Produce image/jpeg // @Success 200 {file} binary "Background image" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/background [get] func (c *AppImagesController) getBackgroundImageHandler(ctx *gin.Context) error { return c.getImage(ctx, "background") @@ -90,6 +94,7 @@ func (c *AppImagesController) getBackgroundImageHandler(ctx *gin.Context) error // @Tags Application Images // @Produce image/x-icon // @Success 200 {file} binary "Favicon image" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/favicon [get] func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) error { return c.getImage(ctx, "favicon") @@ -102,6 +107,7 @@ func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) error { // @Produce image/png // @Produce image/jpeg // @Success 200 {file} binary "Default profile picture image" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/default-profile-picture [get] func (c *AppImagesController) getDefaultProfilePicture(ctx *gin.Context) error { return c.getImage(ctx, "default-profile-picture") @@ -115,6 +121,7 @@ func (c *AppImagesController) getDefaultProfilePicture(ctx *gin.Context) error { // @Param light query boolean false "Light mode logo (true) or dark mode logo (false)" // @Param file formData file true "Logo image file" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/logo [put] func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) error { file, err := httpserver.FormFile(ctx, "file") @@ -136,6 +143,7 @@ func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) error { // @Tags Application Images // @Param light query boolean false "Light mode logo (true) or dark mode logo (false)" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/logo [delete] func (c *AppImagesController) deleteLogoHandler(ctx *gin.Context) error { if err := c.appImagesService.DeleteImage(ctx.Request.Context(), logoImageName(ctx)); err != nil { @@ -161,6 +169,7 @@ func logoImageName(ctx *gin.Context) string { // @Accept multipart/form-data // @Param file formData file true "Email logo image file" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/email [put] func (c *AppImagesController) updateEmailLogoHandler(ctx *gin.Context) error { file, err := httpserver.FormFile(ctx, "file") @@ -190,6 +199,7 @@ func (c *AppImagesController) updateEmailLogoHandler(ctx *gin.Context) error { // @Accept multipart/form-data // @Param file formData file true "Background image file" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/background [put] func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) error { file, err := httpserver.FormFile(ctx, "file") @@ -210,6 +220,7 @@ func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) err // @Description Delete the application background image // @Tags Application Images // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/background [delete] func (c *AppImagesController) deleteBackgroundImageHandler(ctx *gin.Context) error { if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "background"); err != nil { @@ -227,6 +238,7 @@ func (c *AppImagesController) deleteBackgroundImageHandler(ctx *gin.Context) err // @Accept multipart/form-data // @Param file formData file true "Favicon file (.svg/.png/.ico)" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/favicon [put] func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) error { file, err := httpserver.FormFile(ctx, "file") @@ -268,6 +280,7 @@ func (c *AppImagesController) getImage(ctx *gin.Context, name string) error { // @Accept multipart/form-data // @Param file formData file true "Profile picture image file" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/default-profile-picture [put] func (c *AppImagesController) updateDefaultProfilePicture(ctx *gin.Context) error { file, err := httpserver.FormFile(ctx, "file") @@ -288,6 +301,7 @@ func (c *AppImagesController) updateDefaultProfilePicture(ctx *gin.Context) erro // @Description Delete the default profile picture image // @Tags Application Images // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/application-images/default-profile-picture [delete] func (c *AppImagesController) deleteDefaultProfilePicture(ctx *gin.Context) error { if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "default-profile-picture"); err != nil { diff --git a/backend/internal/controller/audit_log_controller.go b/backend/internal/controller/audit_log_controller.go index b483e174..a6ac201d 100644 --- a/backend/internal/controller/audit_log_controller.go +++ b/backend/internal/controller/audit_log_controller.go @@ -40,6 +40,7 @@ type AuditLogController struct { // @Param sort[column] query string false "Column to sort by" // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Success 200 {object} dto.Paginated[dto.AuditLogDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/audit-logs [get] func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) error { listRequestOptions := utils.ParseListRequestOptions(c) @@ -82,6 +83,7 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) error // @Param sort[column] query string false "Column to sort by" // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Success 200 {object} dto.Paginated[dto.AuditLogDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/audit-logs/all [get] func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) error { listRequestOptions := utils.ParseListRequestOptions(c) @@ -116,6 +118,7 @@ func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) error { // @Description Get a list of all client names for audit log filtering // @Tags Audit Logs // @Success 200 {array} string "List of client names" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/audit-logs/filters/client-names [get] func (alc *AuditLogController) listClientNamesHandler(c *gin.Context) error { names, err := alc.auditLogService.ListClientNames(c.Request.Context()) @@ -132,6 +135,7 @@ func (alc *AuditLogController) listClientNamesHandler(c *gin.Context) error { // @Description Get a list of all usernames with their IDs for audit log filtering // @Tags Audit Logs // @Success 200 {object} map[string]string "Map of user IDs to usernames" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/audit-logs/filters/users [get] func (alc *AuditLogController) listUserNamesWithIdsHandler(c *gin.Context) error { users, err := alc.auditLogService.ListUsernamesWithIds(c.Request.Context()) diff --git a/backend/internal/controller/custom_claim_controller.go b/backend/internal/controller/custom_claim_controller.go index 93858520..831e7183 100644 --- a/backend/internal/controller/custom_claim_controller.go +++ b/backend/internal/controller/custom_claim_controller.go @@ -36,6 +36,7 @@ type CustomClaimController struct { // @Tags Custom Claims // @Produce json // @Success 200 {array} string "List of suggested custom claim names" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/custom-claims/suggestions [get] func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) error { claims, err := ccc.customClaimService.GetSuggestions(c.Request.Context()) @@ -56,6 +57,7 @@ func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) error { // @Param userId path string true "User ID" // @Param claims body []dto.CustomClaimCreateDto true "List of custom claims to set for the user" // @Success 200 {array} dto.CustomClaimDto "Updated custom claims" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/custom-claims/user/{userId} [put] func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Context) error { var input []dto.CustomClaimCreateDto @@ -88,6 +90,7 @@ func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Contex // @Param userGroupId path string true "User Group ID" // @Param claims body []dto.CustomClaimCreateDto true "List of custom claims to set for the user group" // @Success 200 {array} dto.CustomClaimDto "Updated custom claims" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/custom-claims/user-group/{userGroupId} [put] func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.Context) error { var input []dto.CustomClaimCreateDto diff --git a/backend/internal/controller/healthz_controller.go b/backend/internal/controller/healthz_controller.go index 9fac0426..a25204be 100644 --- a/backend/internal/controller/healthz_controller.go +++ b/backend/internal/controller/healthz_controller.go @@ -4,6 +4,8 @@ import ( "net/http" "github.com/gin-gonic/gin" + + _ "github.com/pocket-id/pocket-id/backend/internal/dto" ) // NewHealthzController creates a new controller for the healthcheck endpoints @@ -23,6 +25,7 @@ type HealthzController struct{} // @Description Responds with a successful status code to healthcheck requests // @Tags Health // @Success 204 "" +// @Failure default {object} dto.ErrorDto "Error" // @Router /healthz [get] func (hc *HealthzController) healthzHandler(c *gin.Context) { c.Status(http.StatusNoContent) diff --git a/backend/internal/controller/oidc_controller.go b/backend/internal/controller/oidc_controller.go index ef9b06ae..9df00fac 100644 --- a/backend/internal/controller/oidc_controller.go +++ b/backend/internal/controller/oidc_controller.go @@ -66,6 +66,7 @@ type OidcController struct { // @Produce json // @Param id path string true "Client ID" // @Success 200 {object} dto.OidcClientMetaDataDto "Client metadata" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/meta [get] func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) error { clientId := c.Param("id") @@ -91,6 +92,7 @@ func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) error { // @Produce json // @Param id path string true "Client ID" // @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Client information" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id} [get] func (oc *OidcController) getClientHandler(c *gin.Context) error { clientId := c.Param("id") @@ -119,6 +121,7 @@ func (oc *OidcController) getClientHandler(c *gin.Context) error { // @Param sort[column] query string false "Column to sort by" // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Success 200 {object} dto.Paginated[dto.OidcClientWithAllowedGroupsCountDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients [get] func (oc *OidcController) listClientsHandler(c *gin.Context) error { searchTerm := c.Query("search") @@ -160,6 +163,7 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) error { // @Produce json // @Param client body dto.OidcClientCreateDto true "Client information" // @Success 201 {object} dto.OidcClientWithAllowedUserGroupsDto "Created client" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients [post] func (oc *OidcController) createClientHandler(c *gin.Context) error { var input dto.OidcClientCreateDto @@ -189,6 +193,7 @@ func (oc *OidcController) createClientHandler(c *gin.Context) error { // @Tags OIDC // @Param id path string true "Client ID" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id} [delete] func (oc *OidcController) deleteClientHandler(c *gin.Context) error { err := oc.oidcService.DeleteClient(c.Request.Context(), c.Param("id")) @@ -209,6 +214,7 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) error { // @Param id path string true "Client ID" // @Param client body dto.OidcClientUpdateDto true "Client information" // @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id} [put] func (oc *OidcController) updateClientHandler(c *gin.Context) error { var input dto.OidcClientUpdateDto @@ -239,6 +245,7 @@ func (oc *OidcController) updateClientHandler(c *gin.Context) error { // @Produce json // @Param id path string true "Client ID" // @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Refreshed client" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/refresh [post] func (oc *OidcController) refreshClientMetadataHandler(c *gin.Context) error { client, err := oc.oidcService.RefreshClientMetadata(c.Request.Context(), c.Param("id")) @@ -264,6 +271,7 @@ func (oc *OidcController) refreshClientMetadataHandler(c *gin.Context) error { // @Produce json // @Param id path string true "Client ID" // @Success 200 {array} dto.OidcClientSecretDto "Client secrets" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/secrets [get] func (oc *OidcController) listClientSecretsHandler(c *gin.Context) error { secrets, err := oc.oidcService.ListClientSecrets(c.Request.Context(), c.Param("id")) @@ -290,6 +298,7 @@ func (oc *OidcController) listClientSecretsHandler(c *gin.Context) error { // @Param id path string true "Client ID" // @Param payload body dto.OidcClientSecretCreateDto false "Client secret" // @Success 201 {object} dto.OidcClientSecretCreatedDto "Created client secret" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/secrets [post] func (oc *OidcController) createClientSecretHandler(c *gin.Context) error { var input dto.OidcClientSecretCreateDto @@ -321,6 +330,7 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) error { // @Param id path string true "Client ID" // @Param secretId path string true "Client secret ID" // @Success 204 "No content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/secrets/{secretId} [delete] func (oc *OidcController) deleteClientSecretHandler(c *gin.Context) error { err := oc.oidcService.DeleteClientSecret(c.Request.Context(), c.Param("id"), c.Param("secretId")) @@ -342,6 +352,7 @@ func (oc *OidcController) deleteClientSecretHandler(c *gin.Context) error { // @Param id path string true "Client ID" // @Param light query boolean false "Light mode logo (true) or dark mode logo (false)" // @Success 200 {file} binary "Logo image" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/logo [get] func (oc *OidcController) getClientLogoHandler(c *gin.Context) error { lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true")) @@ -368,6 +379,7 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) error { // @Param file formData file true "Logo image file (PNG, JPG, or SVG)" // @Param light query boolean false "Light mode logo (true) or dark mode logo (false)" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/logo [post] func (oc *OidcController) updateClientLogoHandler(c *gin.Context) error { file, err := httpserver.FormFile(c, "file") @@ -393,6 +405,7 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) error { // @Param id path string true "Client ID" // @Param light query boolean false "Light mode logo (true) or dark mode logo (false)" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/logo [delete] func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) error { var err error @@ -421,6 +434,7 @@ func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) error { // @Param id path string true "Client ID" // @Param groups body dto.OidcUpdateAllowedUserGroupsDto true "User group IDs" // @Success 200 {object} dto.OidcClientDto "Updated client" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/allowed-user-groups [put] func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) error { var input dto.OidcUpdateAllowedUserGroupsDto @@ -453,6 +467,7 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) error { // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Param filters[hasLaunchURL] query bool false "Filter clients by whether a launch URL is configured" // @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/users/me/authorized-clients [get] func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) error { userID := c.GetString("userID") @@ -470,6 +485,7 @@ func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) error // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Param filters[hasLaunchURL] query bool false "Filter clients by whether a launch URL is configured" // @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/users/{id}/authorized-clients [get] func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) error { userID := c.Param("id") @@ -503,6 +519,7 @@ func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) e // @Tags OIDC // @Param clientId path string true "Client ID to revoke authorization for" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/users/me/authorized-clients/{clientId} [delete] func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) error { clientID := c.Param("clientId") @@ -528,6 +545,7 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) er // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Param filters[hasLaunchURL] query bool false "Filter clients by whether a launch URL is configured" // @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/users/me/clients [get] func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) error { listRequestOptions := utils.ParseListRequestOptions(c) @@ -556,6 +574,7 @@ func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) error // @Param scopes query string false "Scopes to include in the preview (comma-separated)" // @Success 200 {object} dto.OidcClientPreviewDto "Preview data including ID token, access token, and userinfo payloads" // @Security BearerAuth +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/preview/{userId} [get] func (oc *OidcController) getClientPreviewHandler(c *gin.Context) error { clientID := c.Param("id") @@ -596,6 +615,7 @@ func (oc *OidcController) getClientPreviewHandler(c *gin.Context) error { // @Produce json // @Param id path string true "Client ID" // @Success 200 {object} dto.ScimServiceProviderDTO "SCIM service provider configuration" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/oidc/clients/{id}/scim-service-provider [get] func (oc *OidcController) getClientScimServiceProviderHandler(c *gin.Context) error { clientID := c.Param("id") diff --git a/backend/internal/controller/scim_controller.go b/backend/internal/controller/scim_controller.go index 9cda41ee..14ab9828 100644 --- a/backend/internal/controller/scim_controller.go +++ b/backend/internal/controller/scim_controller.go @@ -31,6 +31,7 @@ type ScimController struct { // @Tags SCIM // @Param id path string true "Service Provider ID" // @Success 200 "OK" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/scim/service-provider/{id}/sync [post] func (c *ScimController) syncServiceProviderHandler(ctx *gin.Context) error { err := c.scimService.SyncServiceProvider(ctx.Request.Context(), ctx.Param("id")) @@ -50,6 +51,7 @@ func (c *ScimController) syncServiceProviderHandler(ctx *gin.Context) error { // @Produce json // @Param serviceProvider body dto.ScimServiceProviderCreateDTO true "SCIM service provider information" // @Success 201 {object} dto.ScimServiceProviderDTO "Created SCIM service provider" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/scim/service-provider [post] func (c *ScimController) createServiceProviderHandler(ctx *gin.Context) error { var input dto.ScimServiceProviderCreateDTO @@ -80,6 +82,7 @@ func (c *ScimController) createServiceProviderHandler(ctx *gin.Context) error { // @Param id path string true "Service Provider ID" // @Param serviceProvider body dto.ScimServiceProviderCreateDTO true "SCIM service provider information" // @Success 200 {object} dto.ScimServiceProviderDTO "Updated SCIM service provider" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/scim/service-provider/{id} [put] func (c *ScimController) updateServiceProviderHandler(ctx *gin.Context) error { var input dto.ScimServiceProviderCreateDTO @@ -107,6 +110,7 @@ func (c *ScimController) updateServiceProviderHandler(ctx *gin.Context) error { // @Tags SCIM // @Param id path string true "Service Provider ID" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/scim/service-provider/{id} [delete] func (c *ScimController) deleteServiceProviderHandler(ctx *gin.Context) error { err := c.scimService.DeleteServiceProvider(ctx.Request.Context(), ctx.Param("id")) diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index 33d12b9f..ac3f3b06 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -61,6 +61,7 @@ type UserController struct { // @Tags Users,User Groups // @Param id path string true "User ID" // @Success 200 {array} dto.UserGroupDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id}/groups [get] func (uc *UserController) getUserGroupsHandler(c *gin.Context) error { userID := c.Param("id") @@ -84,6 +85,7 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) error { // @Tags Users // @Param id path string true "User ID" // @Success 200 {array} dto.WebauthnCredentialDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id}/webauthn-credentials [get] func (uc *UserController) listUserWebauthnCredentialsHandler(c *gin.Context) error { userID := c.Param("id") @@ -116,6 +118,7 @@ func (uc *UserController) listUserWebauthnCredentialsHandler(c *gin.Context) err // @Param sort[column] query string false "Column to sort by" // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Success 200 {object} dto.Paginated[dto.UserDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users [get] func (uc *UserController) listUsersHandler(c *gin.Context) error { searchTerm := c.Query("search") @@ -144,6 +147,7 @@ func (uc *UserController) listUsersHandler(c *gin.Context) error { // @Tags Users // @Param id path string true "User ID" // @Success 200 {object} dto.UserDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id} [get] func (uc *UserController) getUserHandler(c *gin.Context) error { user, err := uc.userService.GetUser(c.Request.Context(), c.Param("id")) @@ -165,6 +169,7 @@ func (uc *UserController) getUserHandler(c *gin.Context) error { // @Description Retrieve information about the currently authenticated user // @Tags Users // @Success 200 {object} dto.UserDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/me [get] func (uc *UserController) getCurrentUserHandler(c *gin.Context) error { user, err := uc.userService.GetUser(c.Request.Context(), c.GetString("userID")) @@ -187,6 +192,7 @@ func (uc *UserController) getCurrentUserHandler(c *gin.Context) error { // @Tags Users // @Param id path string true "User ID" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id} [delete] func (uc *UserController) deleteUserHandler(c *gin.Context) error { dbConfig, err := uc.appConfigService.GetConfig(c.Request.Context()) @@ -209,6 +215,7 @@ func (uc *UserController) deleteUserHandler(c *gin.Context) error { // @Param id path string true "User ID" // @Param credentialId path string true "Credential ID" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id}/webauthn-credentials/{credentialId} [delete] func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) error { err := uc.webAuthnService.DeleteCredential( @@ -233,6 +240,7 @@ func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) er // @Tags Users // @Param user body dto.UserCreateDto true "User information" // @Success 201 {object} dto.UserDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users [post] func (uc *UserController) createUserHandler(c *gin.Context) error { dbConfig, err := uc.appConfigService.GetConfig(c.Request.Context()) @@ -266,6 +274,7 @@ func (uc *UserController) createUserHandler(c *gin.Context) error { // @Param id path string true "User ID" // @Param user body dto.UserCreateDto true "User information" // @Success 200 {object} dto.UserDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id} [put] func (uc *UserController) updateUserHandler(c *gin.Context) error { return uc.updateUser(c, false) @@ -277,6 +286,7 @@ func (uc *UserController) updateUserHandler(c *gin.Context) error { // @Tags Users // @Param user body dto.UserCreateDto true "User information" // @Success 200 {object} dto.UserDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/me [put] func (uc *UserController) updateCurrentUserHandler(c *gin.Context) error { return uc.updateUser(c, true) @@ -289,6 +299,7 @@ func (uc *UserController) updateCurrentUserHandler(c *gin.Context) error { // @Produce image/png // @Param id path string true "User ID" // @Success 200 {file} binary "PNG image" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id}/profile-picture.png [get] func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) error { userID := c.Param("id") @@ -316,6 +327,7 @@ func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) error { // @Param id path string true "User ID" // @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id}/profile-picture [put] func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) error { userID := c.Param("id") @@ -345,6 +357,7 @@ func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) error // @Produce json // @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/me/profile-picture [put] func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) error { userID := c.GetString("userID") @@ -373,6 +386,7 @@ func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) // @Param id path string true "User ID" // @Param groups body dto.UserUpdateUserGroupDto true "User group IDs" // @Success 200 {object} dto.UserDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id}/user-groups [put] func (uc *UserController) updateUserGroups(c *gin.Context) error { var input dto.UserUpdateUserGroupDto @@ -434,6 +448,7 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) error { // @Produce json // @Param id path string true "User ID" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/{id}/profile-picture [delete] func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) error { userID := c.Param("id") @@ -452,6 +467,7 @@ func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) error { // @Tags Users // @Produce json // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/users/me/profile-picture [delete] func (uc *UserController) resetCurrentUserProfilePictureHandler(c *gin.Context) error { userID := c.GetString("userID") diff --git a/backend/internal/controller/user_group_controller.go b/backend/internal/controller/user_group_controller.go index 8545b2aa..85f68abe 100644 --- a/backend/internal/controller/user_group_controller.go +++ b/backend/internal/controller/user_group_controller.go @@ -51,6 +51,7 @@ type UserGroupController struct { // @Param sort[column] query string false "Column to sort by" // @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc") // @Success 200 {object} dto.Paginated[dto.UserGroupMinimalDto] +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/user-groups [get] func (ugc *UserGroupController) list(c *gin.Context) error { searchTerm := c.Query("search") @@ -90,6 +91,7 @@ func (ugc *UserGroupController) list(c *gin.Context) error { // @Produce json // @Param id path string true "User Group ID" // @Success 200 {object} dto.UserGroupDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/user-groups/{id} [get] func (ugc *UserGroupController) get(c *gin.Context) error { group, err := ugc.UserGroupService.Get(c.Request.Context(), c.Param("id")) @@ -114,6 +116,7 @@ func (ugc *UserGroupController) get(c *gin.Context) error { // @Produce json // @Param userGroup body dto.UserGroupCreateDto true "User group information" // @Success 201 {object} dto.UserGroupDto "Created user group" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/user-groups [post] func (ugc *UserGroupController) create(c *gin.Context) error { var input dto.UserGroupCreateDto @@ -144,6 +147,7 @@ func (ugc *UserGroupController) create(c *gin.Context) error { // @Param id path string true "User Group ID" // @Param userGroup body dto.UserGroupCreateDto true "User group information" // @Success 200 {object} dto.UserGroupDto "Updated user group" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/user-groups/{id} [put] func (ugc *UserGroupController) update(c *gin.Context) error { dbConfig, err := ugc.appConfigService.GetConfig(c.Request.Context()) @@ -178,6 +182,7 @@ func (ugc *UserGroupController) update(c *gin.Context) error { // @Produce json // @Param id path string true "User Group ID" // @Success 204 "No Content" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/user-groups/{id} [delete] func (ugc *UserGroupController) delete(c *gin.Context) error { dbConfig, err := ugc.appConfigService.GetConfig(c.Request.Context()) @@ -202,6 +207,7 @@ func (ugc *UserGroupController) delete(c *gin.Context) error { // @Param id path string true "User Group ID" // @Param users body dto.UserGroupUpdateUsersDto true "List of user IDs to assign to this group" // @Success 200 {object} dto.UserGroupDto +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/user-groups/{id}/users [put] func (ugc *UserGroupController) updateUsers(c *gin.Context) error { var input dto.UserGroupUpdateUsersDto @@ -232,6 +238,7 @@ func (ugc *UserGroupController) updateUsers(c *gin.Context) error { // @Param id path string true "User Group ID" // @Param groups body dto.UserGroupUpdateAllowedOidcClientsDto true "OIDC client IDs to allow" // @Success 200 {object} dto.UserGroupDto "Updated user group" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/user-groups/{id}/allowed-oidc-clients [put] func (ugc *UserGroupController) updateAllowedOidcClients(c *gin.Context) error { var input dto.UserGroupUpdateAllowedOidcClientsDto diff --git a/backend/internal/controller/version_controller.go b/backend/internal/controller/version_controller.go index b094496c..a4c7b757 100644 --- a/backend/internal/controller/version_controller.go +++ b/backend/internal/controller/version_controller.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pocket-id/pocket-id/backend/internal/common" + _ "github.com/pocket-id/pocket-id/backend/internal/dto" "github.com/pocket-id/pocket-id/backend/internal/httpserver" "github.com/pocket-id/pocket-id/backend/internal/middleware" "github.com/pocket-id/pocket-id/backend/internal/service" @@ -28,6 +29,7 @@ type VersionController struct { // @Tags Version // @Produce json // @Success 200 {object} map[string]string "Latest version information" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/version/latest [get] func (vc *VersionController) getLatestVersionHandler(c *gin.Context) error { tag, err := vc.versionService.GetLatestVersion(c.Request.Context()) @@ -48,6 +50,7 @@ func (vc *VersionController) getLatestVersionHandler(c *gin.Context) error { // @Tags Version // @Produce json // @Success 200 {object} map[string]string "Current version information" +// @Failure default {object} dto.ErrorDto "Error" // @Router /api/version/current [get] func (vc *VersionController) getCurrentVersionHandler(c *gin.Context) error { c.JSON(http.StatusOK, gin.H{ diff --git a/backend/internal/controller/well_known_controller.go b/backend/internal/controller/well_known_controller.go index e6656938..6fb7c383 100644 --- a/backend/internal/controller/well_known_controller.go +++ b/backend/internal/controller/well_known_controller.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/pocket-id/pocket-id/backend/internal/common" + _ "github.com/pocket-id/pocket-id/backend/internal/dto" "github.com/pocket-id/pocket-id/backend/internal/httpserver" "github.com/pocket-id/pocket-id/backend/internal/service" ) @@ -37,6 +38,7 @@ type WellKnownController struct { // @Tags Well Known // @Produce json // @Success 200 {object} object "{ \"keys\": []interface{} }" +// @Failure default {object} dto.ErrorDto "Error" // @Router /.well-known/jwks.json [get] func (wkc *WellKnownController) jwksHandler(c *gin.Context) error { jwks, err := wkc.jwtService.GetPublicJWKSAsJSON() @@ -53,6 +55,7 @@ func (wkc *WellKnownController) jwksHandler(c *gin.Context) error { // @Description Returns the OpenID Connect discovery document with endpoints and capabilities // @Tags Well Known // @Success 200 {object} object "OpenID Connect configuration" +// @Failure default {object} dto.ErrorDto "Error" // @Router /.well-known/openid-configuration [get] func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) error { oidcConfig, err := wkc.computeOIDCConfiguration() diff --git a/backend/internal/dto/error_dto.go b/backend/internal/dto/error_dto.go new file mode 100644 index 00000000..6c05bfe6 --- /dev/null +++ b/backend/internal/dto/error_dto.go @@ -0,0 +1,11 @@ +package dto + +import "github.com/pocket-id/pocket-id/backend/internal/apperror" + +// ErrorDto is the response body returned for every failed request. +type ErrorDto struct { + Error string `json:"error"` + Code apperror.Code `json:"code"` + Details map[string]any `json:"details,omitempty"` + RequestID string `json:"request_id,omitempty"` +} diff --git a/backend/internal/middleware/error_handler.go b/backend/internal/middleware/error_handler.go index bf447a6f..401cb5b7 100644 --- a/backend/internal/middleware/error_handler.go +++ b/backend/internal/middleware/error_handler.go @@ -106,13 +106,6 @@ func (m *ErrorHandlerMiddleware) Add() gin.HandlerFunc { } } -type errorResponseBody struct { - Error string `json:"error"` - Code apperror.Code `json:"code"` - Details map[string]any `json:"details,omitempty"` - RequestID string `json:"request_id,omitempty"` -} - func classifyError(err error) classifiedError { var structuredErr *apperror.Error if errors.As(err, &structuredErr) && structuredErr != nil { @@ -188,7 +181,7 @@ func writeErrorResponse(c *gin.Context, classified classifiedError, requestID st details = nil } - response := errorResponseBody{ + response := dto.ErrorDto{ Error: classified.message, Code: classified.code, Details: details,