refactor: standardize API error handling (#1635)

This commit is contained in:
Elias Schneider
2026-08-02 23:36:06 +02:00
committed by GitHub
parent 281bea54d3
commit 7a4d0dd275
108 changed files with 3163 additions and 1629 deletions
@@ -9,6 +9,7 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/tracing"
@@ -35,12 +36,12 @@ func NewAppConfigController(
emailSender: emailSender,
ldapService: ldapService,
}
group.GET("/application-configuration", acc.listAppConfigHandler)
group.GET("/application-configuration/all", authMiddleware.Add(), acc.listAllAppConfigHandler)
group.PUT("/application-configuration", authMiddleware.Add(), acc.updateAppConfigHandler)
group.GET("/application-configuration", httpserver.Handle(acc.listAppConfigHandler))
group.GET("/application-configuration/all", authMiddleware.Add(), httpserver.Handle(acc.listAllAppConfigHandler))
group.PUT("/application-configuration", authMiddleware.Add(), httpserver.Handle(acc.updateAppConfigHandler))
group.POST("/application-configuration/test-email", authMiddleware.Add(), acc.testEmailHandler)
group.POST("/application-configuration/sync-ldap", authMiddleware.Add(), acc.syncLdapHandler)
group.POST("/application-configuration/test-email", authMiddleware.Add(), httpserver.Handle(acc.testEmailHandler))
group.POST("/application-configuration/sync-ldap", authMiddleware.Add(), httpserver.Handle(acc.syncLdapHandler))
}
type AppConfigController struct {
@@ -57,18 +58,16 @@ type AppConfigController struct {
// @Produce json
// @Success 200 {array} dto.PublicAppConfigVariableDto
// @Router /api/application-configuration [get]
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) error {
dbConfig, err := acc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
configuration := dbConfig.ToAppConfigVariableSlice(false, true)
var configVariablesDto []dto.PublicAppConfigVariableDto
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
_ = c.Error(err)
return
return err
}
// Manually add uiConfigDisabled which isn't in the database but defined with an environment variable
@@ -85,6 +84,7 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
})
c.JSON(http.StatusOK, configVariablesDto)
return nil
}
// listAllAppConfigHandler godoc
@@ -95,21 +95,20 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
// @Produce json
// @Success 200 {array} dto.AppConfigVariableDto
// @Router /api/application-configuration/all [get]
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) error {
dbConfig, err := acc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
configuration := dbConfig.ToAppConfigVariableSlice(true, true)
var configVariablesDto []dto.AppConfigVariableDto
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, configVariablesDto)
return nil
}
// updateAppConfigHandler godoc
@@ -121,26 +120,24 @@ func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
// @Param body body dto.AppConfigUpdateDto true "Application Configuration"
// @Success 200 {array} dto.AppConfigVariableDto
// @Router /api/application-configuration [put]
func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) error {
var input dto.AppConfigUpdateDto
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
savedConfigVariables, err := acc.appConfigService.UpdateAppConfig(c.Request.Context(), input)
if err != nil {
_ = c.Error(err)
return
return err
}
var configVariablesDto []dto.AppConfigVariableDto
if err := dto.MapStructList(savedConfigVariables, &configVariablesDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, configVariablesDto)
return nil
}
// syncLdapHandler godoc
@@ -149,20 +146,19 @@ func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
// @Tags Application Configuration
// @Success 204 "No Content"
// @Router /api/application-configuration/sync-ldap [post]
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) error {
dbConfig, err := acc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
err = acc.ldapService.SyncAll(c.Request.Context(), dbConfig)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// testEmailHandler godoc
@@ -171,20 +167,19 @@ func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
// @Tags Application Configuration
// @Success 204 "No Content"
// @Router /api/application-configuration/test-email [post]
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
func (acc *AppConfigController) testEmailHandler(c *gin.Context) error {
dbConfig, err := acc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
userID := c.GetString("userID")
err = acc.emailSender.SendTestEmail(c.Request.Context(), dbConfig, userID)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
@@ -9,7 +9,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/apperror"
"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"
"github.com/pocket-id/pocket-id/backend/internal/utils"
@@ -24,20 +25,20 @@ func NewAppImagesController(
appImagesService: appImagesService,
}
group.GET("/application-images/logo", controller.getLogoHandler)
group.GET("/application-images/email", controller.getEmailLogoHandler)
group.GET("/application-images/background", controller.getBackgroundImageHandler)
group.GET("/application-images/favicon", controller.getFaviconHandler)
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), controller.getDefaultProfilePicture)
group.GET("/application-images/logo", httpserver.Handle(controller.getLogoHandler))
group.GET("/application-images/email", httpserver.Handle(controller.getEmailLogoHandler))
group.GET("/application-images/background", httpserver.Handle(controller.getBackgroundImageHandler))
group.GET("/application-images/favicon", httpserver.Handle(controller.getFaviconHandler))
group.GET("/application-images/default-profile-picture", authMiddleware.Add(), httpserver.Handle(controller.getDefaultProfilePicture))
group.PUT("/application-images/logo", authMiddleware.Add(), controller.updateLogoHandler)
group.PUT("/application-images/email", authMiddleware.Add(), controller.updateEmailLogoHandler)
group.PUT("/application-images/background", authMiddleware.Add(), controller.updateBackgroundImageHandler)
group.PUT("/application-images/favicon", authMiddleware.Add(), controller.updateFaviconHandler)
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), controller.updateDefaultProfilePicture)
group.PUT("/application-images/logo", authMiddleware.Add(), httpserver.Handle(controller.updateLogoHandler))
group.PUT("/application-images/email", authMiddleware.Add(), httpserver.Handle(controller.updateEmailLogoHandler))
group.PUT("/application-images/background", authMiddleware.Add(), httpserver.Handle(controller.updateBackgroundImageHandler))
group.PUT("/application-images/favicon", authMiddleware.Add(), httpserver.Handle(controller.updateFaviconHandler))
group.PUT("/application-images/default-profile-picture", authMiddleware.Add(), httpserver.Handle(controller.updateDefaultProfilePicture))
group.DELETE("/application-images/background", authMiddleware.Add(), controller.deleteBackgroundImageHandler)
group.DELETE("/application-images/default-profile-picture", authMiddleware.Add(), controller.deleteDefaultProfilePicture)
group.DELETE("/application-images/background", authMiddleware.Add(), httpserver.Handle(controller.deleteBackgroundImageHandler))
group.DELETE("/application-images/default-profile-picture", authMiddleware.Add(), httpserver.Handle(controller.deleteDefaultProfilePicture))
}
type AppImagesController struct {
@@ -54,14 +55,14 @@ type AppImagesController struct {
// @Produce image/svg+xml
// @Success 200 {file} binary "Logo image"
// @Router /api/application-images/logo [get]
func (c *AppImagesController) getLogoHandler(ctx *gin.Context) {
func (c *AppImagesController) getLogoHandler(ctx *gin.Context) error {
lightLogo, _ := strconv.ParseBool(ctx.DefaultQuery("light", "true"))
imageName := "logoLight"
if !lightLogo {
imageName = "logoDark"
}
c.getImage(ctx, imageName)
return c.getImage(ctx, imageName)
}
// getEmailLogoHandler godoc
@@ -72,8 +73,8 @@ func (c *AppImagesController) getLogoHandler(ctx *gin.Context) {
// @Produce image/jpeg
// @Success 200 {file} binary "Email logo image"
// @Router /api/application-images/email [get]
func (c *AppImagesController) getEmailLogoHandler(ctx *gin.Context) {
c.getImage(ctx, "logoEmail")
func (c *AppImagesController) getEmailLogoHandler(ctx *gin.Context) error {
return c.getImage(ctx, "logoEmail")
}
// getBackgroundImageHandler godoc
@@ -84,8 +85,8 @@ func (c *AppImagesController) getEmailLogoHandler(ctx *gin.Context) {
// @Produce image/jpeg
// @Success 200 {file} binary "Background image"
// @Router /api/application-images/background [get]
func (c *AppImagesController) getBackgroundImageHandler(ctx *gin.Context) {
c.getImage(ctx, "background")
func (c *AppImagesController) getBackgroundImageHandler(ctx *gin.Context) error {
return c.getImage(ctx, "background")
}
// getFaviconHandler godoc
@@ -95,8 +96,8 @@ func (c *AppImagesController) getBackgroundImageHandler(ctx *gin.Context) {
// @Produce image/x-icon
// @Success 200 {file} binary "Favicon image"
// @Router /api/application-images/favicon [get]
func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) {
c.getImage(ctx, "favicon")
func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) error {
return c.getImage(ctx, "favicon")
}
// getDefaultProfilePicture godoc
@@ -107,8 +108,8 @@ func (c *AppImagesController) getFaviconHandler(ctx *gin.Context) {
// @Produce image/jpeg
// @Success 200 {file} binary "Default profile picture image"
// @Router /api/application-images/default-profile-picture [get]
func (c *AppImagesController) getDefaultProfilePicture(ctx *gin.Context) {
c.getImage(ctx, "default-profile-picture")
func (c *AppImagesController) getDefaultProfilePicture(ctx *gin.Context) error {
return c.getImage(ctx, "default-profile-picture")
}
// updateLogoHandler godoc
@@ -120,11 +121,10 @@ func (c *AppImagesController) getDefaultProfilePicture(ctx *gin.Context) {
// @Param file formData file true "Logo image file"
// @Success 204 "No Content"
// @Router /api/application-images/logo [put]
func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
file, err := ctx.FormFile("file")
func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) error {
file, err := httpserver.FormFile(ctx, "file")
if err != nil {
_ = ctx.Error(err)
return
return err
}
lightLogo, _ := strconv.ParseBool(ctx.DefaultQuery("light", "true"))
@@ -134,11 +134,11 @@ func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
}
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, imageName); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
// updateEmailLogoHandler godoc
@@ -149,27 +149,25 @@ func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
// @Param file formData file true "Email logo image file"
// @Success 204 "No Content"
// @Router /api/application-images/email [put]
func (c *AppImagesController) updateEmailLogoHandler(ctx *gin.Context) {
file, err := ctx.FormFile("file")
func (c *AppImagesController) updateEmailLogoHandler(ctx *gin.Context) error {
file, err := httpserver.FormFile(ctx, "file")
if err != nil {
_ = ctx.Error(err)
return
return err
}
fileType := utils.GetFileExtension(file.Filename)
mimeType := utils.GetImageMimeType(fileType)
if mimeType != "image/png" && mimeType != "image/jpeg" {
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".png or .jpg/jpeg"})
return
return apperror.UnsupportedFileType("PNG or JPEG")
}
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "logoEmail"); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
// updateBackgroundImageHandler godoc
@@ -180,19 +178,18 @@ func (c *AppImagesController) updateEmailLogoHandler(ctx *gin.Context) {
// @Param file formData file true "Background image file"
// @Success 204 "No Content"
// @Router /api/application-images/background [put]
func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) {
file, err := ctx.FormFile("file")
func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) error {
file, err := httpserver.FormFile(ctx, "file")
if err != nil {
_ = ctx.Error(err)
return
return err
}
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "background"); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
// deleteBackgroundImageHandler godoc
@@ -201,13 +198,13 @@ func (c *AppImagesController) updateBackgroundImageHandler(ctx *gin.Context) {
// @Tags Application Images
// @Success 204 "No Content"
// @Router /api/application-images/background [delete]
func (c *AppImagesController) deleteBackgroundImageHandler(ctx *gin.Context) {
func (c *AppImagesController) deleteBackgroundImageHandler(ctx *gin.Context) error {
if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "background"); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
// updateFaviconHandler godoc
@@ -218,39 +215,37 @@ func (c *AppImagesController) deleteBackgroundImageHandler(ctx *gin.Context) {
// @Param file formData file true "Favicon file (.svg/.png/.ico)"
// @Success 204 "No Content"
// @Router /api/application-images/favicon [put]
func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) {
file, err := ctx.FormFile("file")
func (c *AppImagesController) updateFaviconHandler(ctx *gin.Context) error {
file, err := httpserver.FormFile(ctx, "file")
if err != nil {
_ = ctx.Error(err)
return
return err
}
fileType := utils.GetFileExtension(file.Filename)
mimeType := utils.GetImageMimeType(strings.ToLower(fileType))
if !slices.Contains([]string{"image/svg+xml", "image/png", "image/x-icon"}, mimeType) {
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".svg or .png or .ico"})
return
return apperror.UnsupportedFileType("SVG, PNG, or ICO")
}
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "favicon"); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
func (c *AppImagesController) getImage(ctx *gin.Context, name string) {
func (c *AppImagesController) getImage(ctx *gin.Context, name string) error {
reader, size, mimeType, err := c.appImagesService.GetImage(ctx.Request.Context(), name)
if err != nil {
_ = ctx.Error(err)
return
return err
}
defer reader.Close()
ctx.Header("Content-Type", mimeType)
utils.SetCacheControlHeader(ctx, 15*time.Minute, 24*time.Hour)
ctx.DataFromReader(http.StatusOK, size, mimeType, reader, nil)
return nil
}
// updateDefaultProfilePicture godoc
@@ -261,19 +256,18 @@ func (c *AppImagesController) getImage(ctx *gin.Context, name string) {
// @Param file formData file true "Profile picture image file"
// @Success 204 "No Content"
// @Router /api/application-images/default-profile-picture [put]
func (c *AppImagesController) updateDefaultProfilePicture(ctx *gin.Context) {
file, err := ctx.FormFile("file")
func (c *AppImagesController) updateDefaultProfilePicture(ctx *gin.Context) error {
file, err := httpserver.FormFile(ctx, "file")
if err != nil {
_ = ctx.Error(err)
return
return err
}
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "default-profile-picture"); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
// deleteDefaultProfilePicture godoc
@@ -282,11 +276,11 @@ func (c *AppImagesController) updateDefaultProfilePicture(ctx *gin.Context) {
// @Tags Application Images
// @Success 204 "No Content"
// @Router /api/application-images/default-profile-picture [delete]
func (c *AppImagesController) deleteDefaultProfilePicture(ctx *gin.Context) {
func (c *AppImagesController) deleteDefaultProfilePicture(ctx *gin.Context) error {
if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "default-profile-picture"); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
@@ -4,6 +4,7 @@ import (
"net/http"
"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/utils"
@@ -20,10 +21,10 @@ func NewAuditLogController(group *gin.RouterGroup, auditLogService *service.Audi
auditLogService: auditLogService,
}
group.GET("/audit-logs/all", authMiddleware.Add(), alc.listAllAuditLogsHandler)
group.GET("/audit-logs", authMiddleware.WithAdminNotRequired().Add(), alc.listAuditLogsForUserHandler)
group.GET("/audit-logs/filters/client-names", authMiddleware.Add(), alc.listClientNamesHandler)
group.GET("/audit-logs/filters/users", authMiddleware.Add(), alc.listUserNamesWithIdsHandler)
group.GET("/audit-logs/all", authMiddleware.Add(), httpserver.Handle(alc.listAllAuditLogsHandler))
group.GET("/audit-logs", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(alc.listAuditLogsForUserHandler))
group.GET("/audit-logs/filters/client-names", authMiddleware.Add(), httpserver.Handle(alc.listClientNamesHandler))
group.GET("/audit-logs/filters/users", authMiddleware.Add(), httpserver.Handle(alc.listUserNamesWithIdsHandler))
}
type AuditLogController struct {
@@ -40,7 +41,7 @@ type AuditLogController struct {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
// @Router /api/audit-logs [get]
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) error {
listRequestOptions := utils.ParseListRequestOptions(c)
userID := c.GetString("userID")
@@ -48,16 +49,14 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
// Fetch audit logs for the user
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, listRequestOptions)
if err != nil {
_ = c.Error(err)
return
return err
}
// Map the audit logs to DTOs
var logsDtos []dto.AuditLogDto
err = dto.MapStructList(logs, &logsDtos)
if err != nil {
_ = c.Error(err)
return
return err
}
// Add device information to the logs
@@ -71,6 +70,7 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
Data: logsDtos,
Pagination: pagination,
})
return nil
}
// listAllAuditLogsHandler godoc
@@ -83,20 +83,18 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
// @Router /api/audit-logs/all [get]
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) error {
listRequestOptions := utils.ParseListRequestOptions(c)
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), listRequestOptions)
if err != nil {
_ = c.Error(err)
return
return err
}
var logsDtos []dto.AuditLogDto
err = dto.MapStructList(logs, &logsDtos)
if err != nil {
_ = c.Error(err)
return
return err
}
for i, logsDto := range logsDtos {
@@ -110,6 +108,7 @@ func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
Data: logsDtos,
Pagination: pagination,
})
return nil
}
// listClientNamesHandler godoc
@@ -118,14 +117,14 @@ func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
// @Tags Audit Logs
// @Success 200 {array} string "List of client names"
// @Router /api/audit-logs/filters/client-names [get]
func (alc *AuditLogController) listClientNamesHandler(c *gin.Context) {
func (alc *AuditLogController) listClientNamesHandler(c *gin.Context) error {
names, err := alc.auditLogService.ListClientNames(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, names)
return nil
}
// listUserNamesWithIdsHandler godoc
@@ -134,12 +133,12 @@ func (alc *AuditLogController) listClientNamesHandler(c *gin.Context) {
// @Tags Audit Logs
// @Success 200 {object} map[string]string "Map of user IDs to usernames"
// @Router /api/audit-logs/filters/users [get]
func (alc *AuditLogController) listUserNamesWithIdsHandler(c *gin.Context) {
func (alc *AuditLogController) listUserNamesWithIdsHandler(c *gin.Context) error {
users, err := alc.auditLogService.ListUsernamesWithIds(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, users)
return nil
}
@@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"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"
)
@@ -19,9 +20,9 @@ func NewCustomClaimController(group *gin.RouterGroup, authMiddleware *middleware
customClaimsGroup := group.Group("/custom-claims")
customClaimsGroup.Use(authMiddleware.Add())
{
customClaimsGroup.GET("/suggestions", wkc.getSuggestionsHandler)
customClaimsGroup.PUT("/user/:userId", wkc.UpdateCustomClaimsForUserHandler)
customClaimsGroup.PUT("/user-group/:userGroupId", wkc.UpdateCustomClaimsForUserGroupHandler)
customClaimsGroup.GET("/suggestions", httpserver.Handle(wkc.getSuggestionsHandler))
customClaimsGroup.PUT("/user/:userId", httpserver.Handle(wkc.UpdateCustomClaimsForUserHandler))
customClaimsGroup.PUT("/user-group/:userGroupId", httpserver.Handle(wkc.UpdateCustomClaimsForUserGroupHandler))
}
}
@@ -36,14 +37,14 @@ type CustomClaimController struct {
// @Produce json
// @Success 200 {array} string "List of suggested custom claim names"
// @Router /api/custom-claims/suggestions [get]
func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) {
func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) error {
claims, err := ccc.customClaimService.GetSuggestions(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, claims)
return nil
}
// UpdateCustomClaimsForUserHandler godoc
@@ -56,28 +57,26 @@ func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) {
// @Param claims body []dto.CustomClaimCreateDto true "List of custom claims to set for the user"
// @Success 200 {array} dto.CustomClaimDto "Updated custom claims"
// @Router /api/custom-claims/user/{userId} [put]
func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Context) {
func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Context) error {
var input []dto.CustomClaimCreateDto
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
userId := c.Param("userId")
claims, err := ccc.customClaimService.UpdateCustomClaimsForUser(c.Request.Context(), userId, input)
if err != nil {
_ = c.Error(err)
return
return err
}
var customClaimsDto []dto.CustomClaimDto
if err := dto.MapStructList(claims, &customClaimsDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, customClaimsDto)
return nil
}
// UpdateCustomClaimsForUserGroupHandler godoc
@@ -90,26 +89,24 @@ func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Contex
// @Param claims body []dto.CustomClaimCreateDto true "List of custom claims to set for the user group"
// @Success 200 {array} dto.CustomClaimDto "Updated custom claims"
// @Router /api/custom-claims/user-group/{userGroupId} [put]
func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.Context) {
func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.Context) error {
var input []dto.CustomClaimCreateDto
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
userGroupId := c.Param("userGroupId")
claims, err := ccc.customClaimService.UpdateCustomClaimsForUserGroup(c.Request.Context(), userGroupId, input)
if err != nil {
_ = c.Error(err)
return
return err
}
var customClaimsDto []dto.CustomClaimDto
if err := dto.MapStructList(claims, &customClaimsDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, customClaimsDto)
return nil
}
@@ -7,25 +7,26 @@ import (
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
func NewTestController(group *gin.RouterGroup, testService *service.TestService) {
testController := &TestController{TestService: testService}
group.POST("/test/reset", testController.resetAndSeedHandler)
group.POST("/test/accesstoken", testController.signAccessToken)
group.POST("/test/refreshtoken", testController.signRefreshToken)
group.POST("/test/reset", httpserver.Handle(testController.resetAndSeedHandler))
group.POST("/test/accesstoken", httpserver.Handle(testController.signAccessToken))
group.POST("/test/refreshtoken", httpserver.Handle(testController.signRefreshToken))
group.GET("/externalidp/jwks.json", testController.externalIdPJWKS)
group.POST("/externalidp/sign", testController.externalIdPSignToken)
group.GET("/externalidp/jwks.json", httpserver.Handle(testController.externalIdPJWKS))
group.POST("/externalidp/sign", httpserver.Handle(testController.externalIdPSignToken))
}
type TestController struct {
TestService *service.TestService
}
func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
func (tc *TestController) resetAndSeedHandler(c *gin.Context) error {
var baseURL string
if c.Request.TLS != nil {
baseURL = "https://" + c.Request.Host
@@ -37,111 +38,103 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
skipSeed := c.Query("skip-seed") == "true"
if err := tc.TestService.ResetDatabase(); err != nil {
_ = c.Error(err)
return
return err
}
if err := tc.TestService.ResetApplicationImages(c.Request.Context()); err != nil {
_ = c.Error(err)
return
return err
}
if !skipSeed {
if err := tc.TestService.SeedDatabase(baseURL); err != nil {
_ = c.Error(err)
return
return err
}
}
if err := tc.TestService.ResetAppConfig(c.Request.Context()); err != nil {
_ = c.Error(err)
return
return err
}
if !skipLdap {
if err := tc.TestService.SetLdapTestConfig(c.Request.Context()); err != nil {
_ = c.Error(err)
return
return err
}
if err := tc.TestService.SyncLdap(c.Request.Context()); err != nil {
_ = c.Error(err)
return
return err
}
}
c.Status(http.StatusNoContent)
return nil
}
func (tc *TestController) externalIdPJWKS(c *gin.Context) {
func (tc *TestController) externalIdPJWKS(c *gin.Context) error {
jwks, err := tc.TestService.GetExternalIdPJWKS()
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, jwks)
return nil
}
func (tc *TestController) externalIdPSignToken(c *gin.Context) {
func (tc *TestController) externalIdPSignToken(c *gin.Context) error {
var input struct {
Aud string `json:"aud"`
Iss string `json:"iss"`
Sub string `json:"sub"`
}
err := c.ShouldBindJSON(&input)
err := httpserver.BindJSON(c, &input)
if err != nil {
_ = c.Error(err)
return
return err
}
token, err := tc.TestService.SignExternalIdPToken(input.Iss, input.Sub, input.Aud)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Writer.WriteString(token)
return nil
}
func (tc *TestController) signAccessToken(c *gin.Context) {
func (tc *TestController) signAccessToken(c *gin.Context) error {
var input struct {
UserID string `json:"user"`
ClientID string `json:"client"`
Expired bool `json:"expired"`
}
err := c.ShouldBindJSON(&input)
err := httpserver.BindJSON(c, &input)
if err != nil {
_ = c.Error(err)
return
return err
}
token, err := tc.TestService.SignAccessToken(c.Request.Context(), input.UserID, input.ClientID, input.Expired)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Writer.WriteString(token)
return nil
}
func (tc *TestController) signRefreshToken(c *gin.Context) {
func (tc *TestController) signRefreshToken(c *gin.Context) error {
var input struct {
UserID string `json:"user"`
ClientID string `json:"client"`
RefreshToken string `json:"rt"`
}
err := c.ShouldBindJSON(&input)
err := httpserver.BindJSON(c, &input)
if err != nil {
_ = c.Error(err)
return
return err
}
token, err := tc.TestService.SignRefreshToken(c.Request.Context(), input.UserID, input.ClientID, input.RefreshToken)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Writer.WriteString(token)
return nil
}
+103 -120
View File
@@ -1,8 +1,6 @@
package controller
import (
"errors"
"io"
"net/http"
"strconv"
"strings"
@@ -10,8 +8,9 @@ import (
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/common"
"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"
"github.com/pocket-id/pocket-id/backend/internal/utils"
@@ -26,31 +25,31 @@ func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
oidcService: oidcService,
}
group.GET("/oidc/clients", authMiddleware.Add(), oc.listClientsHandler)
group.POST("/oidc/clients", authMiddleware.Add(), oc.createClientHandler)
group.GET("/oidc/clients/:id", authMiddleware.Add(), oc.getClientHandler)
group.GET("/oidc/clients/:id/meta", oc.getClientMetaDataHandler)
group.PUT("/oidc/clients/:id", authMiddleware.Add(), oc.updateClientHandler)
group.POST("/oidc/clients/:id/refresh", authMiddleware.Add(), oc.refreshClientMetadataHandler)
group.DELETE("/oidc/clients/:id", authMiddleware.Add(), oc.deleteClientHandler)
group.GET("/oidc/clients", authMiddleware.Add(), httpserver.Handle(oc.listClientsHandler))
group.POST("/oidc/clients", authMiddleware.Add(), httpserver.Handle(oc.createClientHandler))
group.GET("/oidc/clients/:id", authMiddleware.Add(), httpserver.Handle(oc.getClientHandler))
group.GET("/oidc/clients/:id/meta", httpserver.Handle(oc.getClientMetaDataHandler))
group.PUT("/oidc/clients/:id", authMiddleware.Add(), httpserver.Handle(oc.updateClientHandler))
group.POST("/oidc/clients/:id/refresh", authMiddleware.Add(), httpserver.Handle(oc.refreshClientMetadataHandler))
group.DELETE("/oidc/clients/:id", authMiddleware.Add(), httpserver.Handle(oc.deleteClientHandler))
group.PUT("/oidc/clients/:id/allowed-user-groups", authMiddleware.Add(), oc.updateAllowedUserGroupsHandler)
group.POST("/oidc/clients/:id/secret", authMiddleware.Add(), oc.createClientSecretHandler)
group.PUT("/oidc/clients/:id/allowed-user-groups", authMiddleware.Add(), httpserver.Handle(oc.updateAllowedUserGroupsHandler))
group.POST("/oidc/clients/:id/secret", authMiddleware.Add(), httpserver.Handle(oc.createClientSecretHandler))
group.GET("/oidc/clients/:id/logo", oc.getClientLogoHandler)
group.DELETE("/oidc/clients/:id/logo", authMiddleware.Add(), oc.deleteClientLogoHandler)
group.POST("/oidc/clients/:id/logo", authMiddleware.Add(), fileSizeLimitMiddleware.Add(2<<20), oc.updateClientLogoHandler)
group.GET("/oidc/clients/:id/logo", httpserver.Handle(oc.getClientLogoHandler))
group.DELETE("/oidc/clients/:id/logo", authMiddleware.Add(), httpserver.Handle(oc.deleteClientLogoHandler))
group.POST("/oidc/clients/:id/logo", authMiddleware.Add(), fileSizeLimitMiddleware.Add(2<<20), httpserver.Handle(oc.updateClientLogoHandler))
group.GET("/oidc/clients/:id/preview/:userId", authMiddleware.Add(), oc.getClientPreviewHandler)
group.GET("/oidc/clients/:id/preview/:userId", authMiddleware.Add(), httpserver.Handle(oc.getClientPreviewHandler))
group.GET("/oidc/users/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
group.GET("/oidc/users/:id/authorized-clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
group.GET("/oidc/users/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(oc.listOwnAuthorizedClientsHandler))
group.GET("/oidc/users/:id/authorized-clients", authMiddleware.Add(), httpserver.Handle(oc.listAuthorizedClientsHandler))
group.DELETE("/oidc/users/me/authorized-clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), oc.revokeOwnClientAuthorizationHandler)
group.DELETE("/oidc/users/me/authorized-clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(oc.revokeOwnClientAuthorizationHandler))
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAccessibleClientsHandler)
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(oc.listOwnAccessibleClientsHandler))
group.GET("/oidc/clients/:id/scim-service-provider", authMiddleware.Add(), oc.getClientScimServiceProviderHandler)
group.GET("/oidc/clients/:id/scim-service-provider", authMiddleware.Add(), httpserver.Handle(oc.getClientScimServiceProviderHandler))
}
@@ -66,22 +65,21 @@ type OidcController struct {
// @Param id path string true "Client ID"
// @Success 200 {object} dto.OidcClientMetaDataDto "Client metadata"
// @Router /api/oidc/clients/{id}/meta [get]
func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) error {
clientId := c.Param("id")
client, err := oc.oidcService.GetClient(c.Request.Context(), clientId)
if err != nil {
_ = c.Error(err)
return
return err
}
clientDto := dto.OidcClientMetaDataDto{}
err = dto.MapStruct(client, &clientDto)
if err == nil {
c.JSON(http.StatusOK, clientDto)
return
if err := dto.MapStruct(client, &clientDto); err != nil {
return err
}
_ = c.Error(err)
clientDto.HasDarkLogo = client.HasDarkLogo()
c.JSON(http.StatusOK, clientDto)
return nil
}
// getClientHandler godoc
@@ -92,22 +90,21 @@ func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
// @Param id path string true "Client ID"
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Client information"
// @Router /api/oidc/clients/{id} [get]
func (oc *OidcController) getClientHandler(c *gin.Context) {
func (oc *OidcController) getClientHandler(c *gin.Context) error {
clientId := c.Param("id")
client, err := oc.oidcService.GetClient(c.Request.Context(), clientId)
if err != nil {
_ = c.Error(err)
return
return err
}
clientDto := dto.OidcClientWithAllowedUserGroupsDto{}
err = dto.MapStruct(client, &clientDto)
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, clientDto)
return nil
}
// listClientsHandler godoc
@@ -121,14 +118,13 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.OidcClientWithAllowedGroupsCountDto]
// @Router /api/oidc/clients [get]
func (oc *OidcController) listClientsHandler(c *gin.Context) {
func (oc *OidcController) listClientsHandler(c *gin.Context) error {
searchTerm := c.Query("search")
listRequestOptions := utils.ParseListRequestOptions(c)
clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, listRequestOptions)
if err != nil {
_ = c.Error(err)
return
return err
}
// Map the user groups to DTOs
@@ -136,14 +132,13 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
for i, client := range clients {
var clientDto dto.OidcClientWithAllowedGroupsCountDto
if err := dto.MapStruct(client, &clientDto); err != nil {
_ = c.Error(err)
return
return err
}
clientDto.HasDarkLogo = client.HasDarkLogo()
clientDto.AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(c, client.ID)
if err != nil {
_ = c.Error(err)
return
return err
}
clientsDto[i] = clientDto
}
@@ -152,6 +147,7 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
Data: clientsDto,
Pagination: pagination,
})
return nil
}
// createClientHandler godoc
@@ -163,26 +159,24 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
// @Param client body dto.OidcClientCreateDto true "Client information"
// @Success 201 {object} dto.OidcClientWithAllowedUserGroupsDto "Created client"
// @Router /api/oidc/clients [post]
func (oc *OidcController) createClientHandler(c *gin.Context) {
func (oc *OidcController) createClientHandler(c *gin.Context) error {
var input dto.OidcClientCreateDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
client, err := oc.oidcService.CreateClient(c.Request.Context(), input, c.GetString("userID"))
if err != nil {
_ = c.Error(err)
return
return err
}
var clientDto dto.OidcClientWithAllowedUserGroupsDto
if err := dto.MapStruct(client, &clientDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusCreated, clientDto)
return nil
}
// deleteClientHandler godoc
@@ -192,14 +186,14 @@ func (oc *OidcController) createClientHandler(c *gin.Context) {
// @Param id path string true "Client ID"
// @Success 204 "No Content"
// @Router /api/oidc/clients/{id} [delete]
func (oc *OidcController) deleteClientHandler(c *gin.Context) {
func (oc *OidcController) deleteClientHandler(c *gin.Context) error {
err := oc.oidcService.DeleteClient(c.Request.Context(), c.Param("id"))
if err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// updateClientHandler godoc
@@ -212,26 +206,24 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) {
// @Param client body dto.OidcClientUpdateDto true "Client information"
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client"
// @Router /api/oidc/clients/{id} [put]
func (oc *OidcController) updateClientHandler(c *gin.Context) {
func (oc *OidcController) updateClientHandler(c *gin.Context) error {
var input dto.OidcClientUpdateDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
client, err := oc.oidcService.UpdateClient(c.Request.Context(), c.Param("id"), input)
if err != nil {
_ = c.Error(err)
return
return err
}
var clientDto dto.OidcClientWithAllowedUserGroupsDto
if err := dto.MapStruct(client, &clientDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, clientDto)
return nil
}
// refreshClientMetadataHandler godoc
@@ -242,20 +234,20 @@ func (oc *OidcController) updateClientHandler(c *gin.Context) {
// @Param id path string true "Client ID"
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Refreshed client"
// @Router /api/oidc/clients/{id}/refresh [post]
func (oc *OidcController) refreshClientMetadataHandler(c *gin.Context) {
func (oc *OidcController) refreshClientMetadataHandler(c *gin.Context) error {
client, err := oc.oidcService.RefreshClientMetadata(c.Request.Context(), c.Param("id"))
if err != nil {
_ = c.Error(err)
return
return err
}
var clientDto dto.OidcClientWithAllowedUserGroupsDto
if err := dto.MapStruct(client, &clientDto); err != nil {
_ = c.Error(err)
return
return err
}
clientDto.HasDarkLogo = client.HasDarkLogo()
c.JSON(http.StatusOK, clientDto)
return nil
}
// createClientSecretHandler godoc
@@ -268,20 +260,19 @@ func (oc *OidcController) refreshClientMetadataHandler(c *gin.Context) {
// @Param payload body dto.OidcClientSecretDto false "Client secret"
// @Success 200 {object} object "{ \"secret\": \"string\" }"
// @Router /api/oidc/clients/{id}/secret [post]
func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
func (oc *OidcController) createClientSecretHandler(c *gin.Context) error {
var input dto.OidcClientSecretDto
if err := c.ShouldBindJSON(&input); err != nil && !errors.Is(err, io.EOF) {
_ = c.Error(err)
return
if err := httpserver.BindOptionalJSON(c, &input); err != nil {
return err
}
secret, err := oc.oidcService.CreateClientSecret(c.Request.Context(), c.Param("id"), input)
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, gin.H{"secret": secret})
return nil
}
// getClientLogoHandler godoc
@@ -295,13 +286,12 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
// @Success 200 {file} binary "Logo image"
// @Router /api/oidc/clients/{id}/logo [get]
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
func (oc *OidcController) getClientLogoHandler(c *gin.Context) error {
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
reader, size, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"), lightLogo)
if err != nil {
_ = c.Error(err)
return
return err
}
defer reader.Close()
@@ -309,6 +299,7 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
c.Header("Content-Type", mimeType)
c.DataFromReader(http.StatusOK, size, mimeType, reader, nil)
return nil
}
// updateClientLogoHandler godoc
@@ -321,22 +312,21 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
// @Success 204 "No Content"
// @Router /api/oidc/clients/{id}/logo [post]
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
file, err := c.FormFile("file")
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) error {
file, err := httpserver.FormFile(c, "file")
if err != nil {
_ = c.Error(err)
return
return err
}
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
err = oc.oidcService.UpdateClientLogo(c.Request.Context(), c.Param("id"), file, lightLogo)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// deleteClientLogoHandler godoc
@@ -347,7 +337,7 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
// @Success 204 "No Content"
// @Router /api/oidc/clients/{id}/logo [delete]
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) error {
var err error
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
@@ -358,11 +348,11 @@ func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
}
if err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// updateAllowedUserGroupsHandler godoc
@@ -375,27 +365,25 @@ func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
// @Param groups body dto.OidcUpdateAllowedUserGroupsDto true "User group IDs"
// @Success 200 {object} dto.OidcClientDto "Updated client"
// @Router /api/oidc/clients/{id}/allowed-user-groups [put]
func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) error {
var input dto.OidcUpdateAllowedUserGroupsDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
oidcClient, err := oc.oidcService.UpdateAllowedUserGroups(c.Request.Context(), c.Param("id"), input)
if err != nil {
_ = c.Error(err)
return
return err
}
var oidcClientDto dto.OidcClientDto
if err := dto.MapStruct(oidcClient, &oidcClientDto); err != nil {
_ = c.Error(err)
return
return err
}
oidcClientDto.HasDarkLogo = oidcClient.HasDarkLogo()
c.JSON(http.StatusOK, oidcClientDto)
return nil
}
// listOwnAuthorizedClientsHandler godoc
@@ -408,9 +396,9 @@ func (oc *OidcController) updateAllowedUserGroupsHandler(c *gin.Context) {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
// @Router /api/oidc/users/me/authorized-clients [get]
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) error {
userID := c.GetString("userID")
oc.listAuthorizedClients(c, userID)
return oc.listAuthorizedClients(c, userID)
}
// listAuthorizedClientsHandler godoc
@@ -424,31 +412,30 @@ func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.AuthorizedOidcClientDto]
// @Router /api/oidc/users/{id}/authorized-clients [get]
func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) error {
userID := c.Param("id")
oc.listAuthorizedClients(c, userID)
return oc.listAuthorizedClients(c, userID)
}
func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) error {
listRequestOptions := utils.ParseListRequestOptions(c)
authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, listRequestOptions)
if err != nil {
_ = c.Error(err)
return
return err
}
// Map the clients to DTOs
var authorizedClientsDto []dto.AuthorizedOidcClientDto
if err := dto.MapStructList(authorizedClients, &authorizedClientsDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, dto.Paginated[dto.AuthorizedOidcClientDto]{
Data: authorizedClientsDto,
Pagination: pagination,
})
return nil
}
// revokeOwnClientAuthorizationHandler godoc
@@ -458,18 +445,18 @@ func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
// @Param clientId path string true "Client ID to revoke authorization for"
// @Success 204 "No Content"
// @Router /api/oidc/users/me/authorized-clients/{clientId} [delete]
func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) error {
clientID := c.Param("clientId")
userID := c.GetString("userID")
err := oc.oidcService.RevokeAuthorizedClient(c.Request.Context(), userID, clientID)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// listOwnAccessibleClientsHandler godoc
@@ -482,21 +469,21 @@ func (oc *OidcController) revokeOwnClientAuthorizationHandler(c *gin.Context) {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.AccessibleOidcClientDto]
// @Router /api/oidc/users/me/clients [get]
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) error {
listRequestOptions := utils.ParseListRequestOptions(c)
userID := c.GetString("userID")
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, listRequestOptions)
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, dto.Paginated[dto.AccessibleOidcClientDto]{
Data: clients,
Pagination: pagination,
})
return nil
}
// getClientPreviewHandler godoc
@@ -510,24 +497,21 @@ func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
// @Success 200 {object} dto.OidcClientPreviewDto "Preview data including ID token, access token, and userinfo payloads"
// @Security BearerAuth
// @Router /api/oidc/clients/{id}/preview/{userId} [get]
func (oc *OidcController) getClientPreviewHandler(c *gin.Context) {
func (oc *OidcController) getClientPreviewHandler(c *gin.Context) error {
clientID := c.Param("id")
userID := c.Param("userId")
scopes := c.Query("scopes")
if clientID == "" {
_ = c.Error(&common.ValidationError{Message: "client ID is required"})
return
return apperror.MissingField("clientId")
}
if userID == "" {
_ = c.Error(&common.ValidationError{Message: "user ID is required"})
return
return apperror.MissingField("userId")
}
if scopes == "" {
_ = c.Error(&common.ValidationError{Message: "scopes are required"})
return
return apperror.MissingField("scopes")
}
preview, err := oc.oidcService.GetClientPreview(
@@ -538,11 +522,11 @@ func (oc *OidcController) getClientPreviewHandler(c *gin.Context) {
c.GetString("authenticationMethod"))
if err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, preview)
return nil
}
// getClientScimServiceProviderHandler godoc
@@ -553,20 +537,19 @@ func (oc *OidcController) getClientPreviewHandler(c *gin.Context) {
// @Param id path string true "Client ID"
// @Success 200 {object} dto.ScimServiceProviderDTO "SCIM service provider configuration"
// @Router /api/oidc/clients/{id}/scim-service-provider [get]
func (oc *OidcController) getClientScimServiceProviderHandler(c *gin.Context) {
func (oc *OidcController) getClientScimServiceProviderHandler(c *gin.Context) error {
clientID := c.Param("id")
provider, err := oc.oidcService.GetClientScimServiceProvider(c.Request.Context(), clientID)
if err != nil {
_ = c.Error(err)
return
return err
}
var providerDto dto.ScimServiceProviderDTO
if err := dto.MapStruct(provider, &providerDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, providerDto)
return nil
}
+23 -26
View File
@@ -5,6 +5,7 @@ import (
"github.com/gin-gonic/gin"
"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"
)
@@ -14,10 +15,10 @@ func NewScimController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
scimService: scimService,
}
group.POST("/scim/service-provider", authMiddleware.Add(), ugc.createServiceProviderHandler)
group.POST("/scim/service-provider/:id/sync", authMiddleware.Add(), ugc.syncServiceProviderHandler)
group.PUT("/scim/service-provider/:id", authMiddleware.Add(), ugc.updateServiceProviderHandler)
group.DELETE("/scim/service-provider/:id", authMiddleware.Add(), ugc.deleteServiceProviderHandler)
group.POST("/scim/service-provider", authMiddleware.Add(), httpserver.Handle(ugc.createServiceProviderHandler))
group.POST("/scim/service-provider/:id/sync", authMiddleware.Add(), httpserver.Handle(ugc.syncServiceProviderHandler))
group.PUT("/scim/service-provider/:id", authMiddleware.Add(), httpserver.Handle(ugc.updateServiceProviderHandler))
group.DELETE("/scim/service-provider/:id", authMiddleware.Add(), httpserver.Handle(ugc.deleteServiceProviderHandler))
}
type ScimController struct {
@@ -31,14 +32,14 @@ type ScimController struct {
// @Param id path string true "Service Provider ID"
// @Success 200 "OK"
// @Router /api/scim/service-provider/{id}/sync [post]
func (c *ScimController) syncServiceProviderHandler(ctx *gin.Context) {
func (c *ScimController) syncServiceProviderHandler(ctx *gin.Context) error {
err := c.scimService.SyncServiceProvider(ctx.Request.Context(), ctx.Param("id"))
if err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusOK)
return nil
}
// createServiceProviderHandler godoc
@@ -50,26 +51,24 @@ func (c *ScimController) syncServiceProviderHandler(ctx *gin.Context) {
// @Param serviceProvider body dto.ScimServiceProviderCreateDTO true "SCIM service provider information"
// @Success 201 {object} dto.ScimServiceProviderDTO "Created SCIM service provider"
// @Router /api/scim/service-provider [post]
func (c *ScimController) createServiceProviderHandler(ctx *gin.Context) {
func (c *ScimController) createServiceProviderHandler(ctx *gin.Context) error {
var input dto.ScimServiceProviderCreateDTO
if err := ctx.ShouldBindJSON(&input); err != nil {
_ = ctx.Error(err)
return
if err := httpserver.BindJSON(ctx, &input); err != nil {
return err
}
provider, err := c.scimService.CreateServiceProvider(ctx.Request.Context(), &input)
if err != nil {
_ = ctx.Error(err)
return
return err
}
var providerDTO dto.ScimServiceProviderDTO
if err := dto.MapStruct(provider, &providerDTO); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.JSON(http.StatusCreated, providerDTO)
return nil
}
// updateServiceProviderHandler godoc
@@ -82,26 +81,24 @@ func (c *ScimController) createServiceProviderHandler(ctx *gin.Context) {
// @Param serviceProvider body dto.ScimServiceProviderCreateDTO true "SCIM service provider information"
// @Success 200 {object} dto.ScimServiceProviderDTO "Updated SCIM service provider"
// @Router /api/scim/service-provider/{id} [put]
func (c *ScimController) updateServiceProviderHandler(ctx *gin.Context) {
func (c *ScimController) updateServiceProviderHandler(ctx *gin.Context) error {
var input dto.ScimServiceProviderCreateDTO
if err := ctx.ShouldBindJSON(&input); err != nil {
_ = ctx.Error(err)
return
if err := httpserver.BindJSON(ctx, &input); err != nil {
return err
}
provider, err := c.scimService.UpdateServiceProvider(ctx.Request.Context(), ctx.Param("id"), &input)
if err != nil {
_ = ctx.Error(err)
return
return err
}
var providerDTO dto.ScimServiceProviderDTO
if err := dto.MapStruct(provider, &providerDTO); err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.JSON(http.StatusOK, providerDTO)
return nil
}
// deleteServiceProviderHandler godoc
@@ -111,12 +108,12 @@ func (c *ScimController) updateServiceProviderHandler(ctx *gin.Context) {
// @Param id path string true "Service Provider ID"
// @Success 204 "No Content"
// @Router /api/scim/service-provider/{id} [delete]
func (c *ScimController) deleteServiceProviderHandler(ctx *gin.Context) {
func (c *ScimController) deleteServiceProviderHandler(ctx *gin.Context) error {
err := c.scimService.DeleteServiceProvider(ctx.Request.Context(), ctx.Param("id"))
if err != nil {
_ = ctx.Error(err)
return
return err
}
ctx.Status(http.StatusNoContent)
return nil
}
+90 -108
View File
@@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin"
"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"
"github.com/pocket-id/pocket-id/backend/internal/utils"
@@ -26,26 +27,26 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
webAuthnService: webAuthnService,
}
group.GET("/users", authMiddleware.Add(), uc.listUsersHandler)
group.GET("/users/me", authMiddleware.WithAdminNotRequired().Add(), uc.getCurrentUserHandler)
group.GET("/users/:id", authMiddleware.Add(), uc.getUserHandler)
group.POST("/users", authMiddleware.Add(), uc.createUserHandler)
group.PUT("/users/:id", authMiddleware.Add(), uc.updateUserHandler)
group.GET("/users/:id/groups", authMiddleware.Add(), uc.getUserGroupsHandler)
group.GET("/users/:id/webauthn-credentials", authMiddleware.Add(), uc.listUserWebauthnCredentialsHandler)
group.PUT("/users/me", authMiddleware.WithAdminNotRequired().Add(), uc.updateCurrentUserHandler)
group.DELETE("/users/:id", authMiddleware.Add(), uc.deleteUserHandler)
group.DELETE("/users/:id/webauthn-credentials/:credentialId", authMiddleware.Add(), uc.deleteUserWebauthnCredentialHandler)
group.GET("/users", authMiddleware.Add(), httpserver.Handle(uc.listUsersHandler))
group.GET("/users/me", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(uc.getCurrentUserHandler))
group.GET("/users/:id", authMiddleware.Add(), httpserver.Handle(uc.getUserHandler))
group.POST("/users", authMiddleware.Add(), httpserver.Handle(uc.createUserHandler))
group.PUT("/users/:id", authMiddleware.Add(), httpserver.Handle(uc.updateUserHandler))
group.GET("/users/:id/groups", authMiddleware.Add(), httpserver.Handle(uc.getUserGroupsHandler))
group.GET("/users/:id/webauthn-credentials", authMiddleware.Add(), httpserver.Handle(uc.listUserWebauthnCredentialsHandler))
group.PUT("/users/me", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(uc.updateCurrentUserHandler))
group.DELETE("/users/:id", authMiddleware.Add(), httpserver.Handle(uc.deleteUserHandler))
group.DELETE("/users/:id/webauthn-credentials/:credentialId", authMiddleware.Add(), httpserver.Handle(uc.deleteUserWebauthnCredentialHandler))
group.PUT("/users/:id/user-groups", authMiddleware.Add(), uc.updateUserGroups)
group.PUT("/users/:id/user-groups", authMiddleware.Add(), httpserver.Handle(uc.updateUserGroups))
group.GET("/users/:id/profile-picture.png", uc.getUserProfilePictureHandler)
group.GET("/users/:id/profile-picture.png", httpserver.Handle(uc.getUserProfilePictureHandler))
group.PUT("/users/:id/profile-picture", authMiddleware.Add(), uc.updateUserProfilePictureHandler)
group.PUT("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.updateCurrentUserProfilePictureHandler)
group.PUT("/users/:id/profile-picture", authMiddleware.Add(), httpserver.Handle(uc.updateUserProfilePictureHandler))
group.PUT("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(uc.updateCurrentUserProfilePictureHandler))
group.DELETE("/users/:id/profile-picture", authMiddleware.Add(), uc.resetUserProfilePictureHandler)
group.DELETE("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.resetCurrentUserProfilePictureHandler)
group.DELETE("/users/:id/profile-picture", authMiddleware.Add(), httpserver.Handle(uc.resetUserProfilePictureHandler))
group.DELETE("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(uc.resetCurrentUserProfilePictureHandler))
}
type UserController struct {
@@ -61,21 +62,20 @@ type UserController struct {
// @Param id path string true "User ID"
// @Success 200 {array} dto.UserGroupDto
// @Router /api/users/{id}/groups [get]
func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
func (uc *UserController) getUserGroupsHandler(c *gin.Context) error {
userID := c.Param("id")
groups, err := uc.userService.GetUserGroups(c.Request.Context(), userID)
if err != nil {
_ = c.Error(err)
return
return err
}
var groupsDto []dto.UserGroupDto
if err := dto.MapStructList(groups, &groupsDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, groupsDto)
return nil
}
// listUserWebauthnCredentialsHandler godoc
@@ -85,27 +85,25 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
// @Param id path string true "User ID"
// @Success 200 {array} dto.WebauthnCredentialDto
// @Router /api/users/{id}/webauthn-credentials [get]
func (uc *UserController) listUserWebauthnCredentialsHandler(c *gin.Context) {
func (uc *UserController) listUserWebauthnCredentialsHandler(c *gin.Context) error {
userID := c.Param("id")
if _, err := uc.userService.GetUser(c.Request.Context(), userID); err != nil {
_ = c.Error(err)
return
return err
}
credentials, err := uc.webAuthnService.ListCredentials(c.Request.Context(), userID)
if err != nil {
_ = c.Error(err)
return
return err
}
var credentialDtos []dto.WebauthnCredentialDto
if err := dto.MapStructList(credentials, &credentialDtos); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, credentialDtos)
return nil
}
// listUsersHandler godoc
@@ -119,26 +117,25 @@ func (uc *UserController) listUserWebauthnCredentialsHandler(c *gin.Context) {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.UserDto]
// @Router /api/users [get]
func (uc *UserController) listUsersHandler(c *gin.Context) {
func (uc *UserController) listUsersHandler(c *gin.Context) error {
searchTerm := c.Query("search")
listRequestOptions := utils.ParseListRequestOptions(c)
users, pagination, err := uc.userService.ListUsers(c.Request.Context(), searchTerm, listRequestOptions)
if err != nil {
_ = c.Error(err)
return
return err
}
var usersDto []dto.UserDto
if err := dto.MapStructList(users, &usersDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, dto.Paginated[dto.UserDto]{
Data: usersDto,
Pagination: pagination,
})
return nil
}
// getUserHandler godoc
@@ -148,20 +145,19 @@ func (uc *UserController) listUsersHandler(c *gin.Context) {
// @Param id path string true "User ID"
// @Success 200 {object} dto.UserDto
// @Router /api/users/{id} [get]
func (uc *UserController) getUserHandler(c *gin.Context) {
func (uc *UserController) getUserHandler(c *gin.Context) error {
user, err := uc.userService.GetUser(c.Request.Context(), c.Param("id"))
if err != nil {
_ = c.Error(err)
return
return err
}
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, userDto)
return nil
}
// getCurrentUserHandler godoc
@@ -170,20 +166,19 @@ func (uc *UserController) getUserHandler(c *gin.Context) {
// @Tags Users
// @Success 200 {object} dto.UserDto
// @Router /api/users/me [get]
func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
func (uc *UserController) getCurrentUserHandler(c *gin.Context) error {
user, err := uc.userService.GetUser(c.Request.Context(), c.GetString("userID"))
if err != nil {
_ = c.Error(err)
return
return err
}
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, userDto)
return nil
}
// deleteUserHandler godoc
@@ -193,19 +188,18 @@ func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
// @Param id path string true "User ID"
// @Success 204 "No Content"
// @Router /api/users/{id} [delete]
func (uc *UserController) deleteUserHandler(c *gin.Context) {
func (uc *UserController) deleteUserHandler(c *gin.Context) error {
dbConfig, err := uc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
return
return fmt.Errorf("error loading app configuration: %w", err)
}
if err := uc.userService.DeleteUser(c.Request.Context(), dbConfig, c.Param("id"), false); err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// deleteUserWebauthnCredentialHandler godoc
@@ -216,7 +210,7 @@ func (uc *UserController) deleteUserHandler(c *gin.Context) {
// @Param credentialId path string true "Credential ID"
// @Success 204 "No Content"
// @Router /api/users/{id}/webauthn-credentials/{credentialId} [delete]
func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) {
func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) error {
err := uc.webAuthnService.DeleteCredential(
c.Request.Context(),
c.Param("id"),
@@ -226,11 +220,11 @@ func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) {
c.GetString("userID"),
)
if err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// createUserHandler godoc
@@ -240,32 +234,29 @@ func (uc *UserController) deleteUserWebauthnCredentialHandler(c *gin.Context) {
// @Param user body dto.UserCreateDto true "User information"
// @Success 201 {object} dto.UserDto
// @Router /api/users [post]
func (uc *UserController) createUserHandler(c *gin.Context) {
func (uc *UserController) createUserHandler(c *gin.Context) error {
dbConfig, err := uc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
return
return fmt.Errorf("error loading app configuration: %w", err)
}
var input dto.UserCreateDto
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
user, err := uc.userService.CreateUser(c.Request.Context(), dbConfig, input)
if err != nil {
_ = c.Error(err)
return
return err
}
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusCreated, userDto)
return nil
}
// updateUserHandler godoc
@@ -276,8 +267,8 @@ func (uc *UserController) createUserHandler(c *gin.Context) {
// @Param user body dto.UserCreateDto true "User information"
// @Success 200 {object} dto.UserDto
// @Router /api/users/{id} [put]
func (uc *UserController) updateUserHandler(c *gin.Context) {
uc.updateUser(c, false)
func (uc *UserController) updateUserHandler(c *gin.Context) error {
return uc.updateUser(c, false)
}
// updateCurrentUserHandler godoc
@@ -287,8 +278,8 @@ func (uc *UserController) updateUserHandler(c *gin.Context) {
// @Param user body dto.UserCreateDto true "User information"
// @Success 200 {object} dto.UserDto
// @Router /api/users/me [put]
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
uc.updateUser(c, true)
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) error {
return uc.updateUser(c, true)
}
// getUserProfilePictureHandler godoc
@@ -299,13 +290,12 @@ func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
// @Param id path string true "User ID"
// @Success 200 {file} binary "PNG image"
// @Router /api/users/{id}/profile-picture.png [get]
func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) error {
userID := c.Param("id")
picture, size, err := uc.userService.GetProfilePicture(c.Request.Context(), userID)
if err != nil {
_ = c.Error(err)
return
return err
}
if picture != nil {
defer picture.Close()
@@ -314,6 +304,7 @@ func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
utils.SetCacheControlHeader(c, 15*time.Minute, 1*time.Hour)
c.DataFromReader(http.StatusOK, size, "image/png", picture, nil)
return nil
}
// updateUserProfilePictureHandler godoc
@@ -326,26 +317,24 @@ func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
// @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)"
// @Success 204 "No Content"
// @Router /api/users/{id}/profile-picture [put]
func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) error {
userID := c.Param("id")
fileHeader, err := c.FormFile("file")
fileHeader, err := httpserver.FormFile(c, "file")
if err != nil {
_ = c.Error(err)
return
return err
}
file, err := fileHeader.Open()
if err != nil {
_ = c.Error(err)
return
return err
}
defer file.Close()
if err := uc.userService.UpdateProfilePicture(c.Request.Context(), userID, file); err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// updateCurrentUserProfilePictureHandler godoc
@@ -357,26 +346,24 @@ func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
// @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)"
// @Success 204 "No Content"
// @Router /api/users/me/profile-picture [put]
func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) {
func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) error {
userID := c.GetString("userID")
fileHeader, err := c.FormFile("file")
fileHeader, err := httpserver.FormFile(c, "file")
if err != nil {
_ = c.Error(err)
return
return err
}
file, err := fileHeader.Open()
if err != nil {
_ = c.Error(err)
return
return err
}
defer file.Close()
if err := uc.userService.UpdateProfilePicture(c.Request.Context(), userID, file); err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// updateUserGroups godoc
@@ -387,40 +374,36 @@ func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context)
// @Param groups body dto.UserUpdateUserGroupDto true "User group IDs"
// @Success 200 {object} dto.UserDto
// @Router /api/users/{id}/user-groups [put]
func (uc *UserController) updateUserGroups(c *gin.Context) {
func (uc *UserController) updateUserGroups(c *gin.Context) error {
var input dto.UserUpdateUserGroupDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
user, err := uc.userService.UpdateUserGroups(c.Request.Context(), c.Param("id"), input.UserGroupIds)
if err != nil {
_ = c.Error(err)
return
return err
}
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, userDto)
return nil
}
// updateUser is an internal helper method, not exposed as an API endpoint
func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) error {
dbConfig, err := uc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
return
return fmt.Errorf("error loading app configuration: %w", err)
}
var input dto.UserCreateDto
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
var userID string
@@ -432,17 +415,16 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
user, err := uc.userService.UpdateUser(c.Request.Context(), dbConfig, userID, input, updateOwnUser, false)
if err != nil {
_ = c.Error(err)
return
return err
}
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, userDto)
return nil
}
// resetUserProfilePictureHandler godoc
@@ -453,15 +435,15 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
// @Param id path string true "User ID"
// @Success 204 "No Content"
// @Router /api/users/{id}/profile-picture [delete]
func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) {
func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) error {
userID := c.Param("id")
if err := uc.userService.ResetProfilePicture(c.Request.Context(), userID); err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// resetCurrentUserProfilePictureHandler godoc
@@ -471,13 +453,13 @@ func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) {
// @Produce json
// @Success 204 "No Content"
// @Router /api/users/me/profile-picture [delete]
func (uc *UserController) resetCurrentUserProfilePictureHandler(c *gin.Context) {
func (uc *UserController) resetCurrentUserProfilePictureHandler(c *gin.Context) error {
userID := c.GetString("userID")
if err := uc.userService.ResetProfilePicture(c.Request.Context(), userID); err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
@@ -7,6 +7,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
"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"
"github.com/pocket-id/pocket-id/backend/internal/utils"
@@ -25,13 +26,13 @@ func NewUserGroupController(group *gin.RouterGroup, authMiddleware *middleware.A
userGroupsGroup := group.Group("/user-groups")
userGroupsGroup.Use(authMiddleware.Add())
{
userGroupsGroup.GET("", ugc.list)
userGroupsGroup.GET("/:id", ugc.get)
userGroupsGroup.POST("", ugc.create)
userGroupsGroup.PUT("/:id", ugc.update)
userGroupsGroup.DELETE("/:id", ugc.delete)
userGroupsGroup.PUT("/:id/users", ugc.updateUsers)
userGroupsGroup.PUT("/:id/allowed-oidc-clients", ugc.updateAllowedOidcClients)
userGroupsGroup.GET("", httpserver.Handle(ugc.list))
userGroupsGroup.GET("/:id", httpserver.Handle(ugc.get))
userGroupsGroup.POST("", httpserver.Handle(ugc.create))
userGroupsGroup.PUT("/:id", httpserver.Handle(ugc.update))
userGroupsGroup.DELETE("/:id", httpserver.Handle(ugc.delete))
userGroupsGroup.PUT("/:id/users", httpserver.Handle(ugc.updateUsers))
userGroupsGroup.PUT("/:id/allowed-oidc-clients", httpserver.Handle(ugc.updateAllowedOidcClients))
}
}
@@ -51,14 +52,13 @@ type UserGroupController struct {
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.UserGroupMinimalDto]
// @Router /api/user-groups [get]
func (ugc *UserGroupController) list(c *gin.Context) {
func (ugc *UserGroupController) list(c *gin.Context) error {
searchTerm := c.Query("search")
listRequestOptions := utils.ParseListRequestOptions(c)
groups, pagination, err := ugc.UserGroupService.List(c, searchTerm, listRequestOptions)
if err != nil {
_ = c.Error(err)
return
return err
}
// Map the user groups to DTOs
@@ -66,13 +66,11 @@ func (ugc *UserGroupController) list(c *gin.Context) {
for i, group := range groups {
var groupDto dto.UserGroupMinimalDto
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
return err
}
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(c.Request.Context(), group.ID)
if err != nil {
_ = c.Error(err)
return
return err
}
groupsDto[i] = groupDto
}
@@ -81,6 +79,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
Data: groupsDto,
Pagination: pagination,
})
return nil
}
// get godoc
@@ -92,20 +91,19 @@ func (ugc *UserGroupController) list(c *gin.Context) {
// @Param id path string true "User Group ID"
// @Success 200 {object} dto.UserGroupDto
// @Router /api/user-groups/{id} [get]
func (ugc *UserGroupController) get(c *gin.Context) {
func (ugc *UserGroupController) get(c *gin.Context) error {
group, err := ugc.UserGroupService.Get(c.Request.Context(), c.Param("id"))
if err != nil {
_ = c.Error(err)
return
return err
}
var groupDto dto.UserGroupDto
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, groupDto)
return nil
}
// create godoc
@@ -117,26 +115,24 @@ func (ugc *UserGroupController) get(c *gin.Context) {
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
// @Success 201 {object} dto.UserGroupDto "Created user group"
// @Router /api/user-groups [post]
func (ugc *UserGroupController) create(c *gin.Context) {
func (ugc *UserGroupController) create(c *gin.Context) error {
var input dto.UserGroupCreateDto
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
group, err := ugc.UserGroupService.Create(c.Request.Context(), input)
if err != nil {
_ = c.Error(err)
return
return err
}
var groupDto dto.UserGroupDto
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusCreated, groupDto)
return nil
}
// update godoc
@@ -149,32 +145,29 @@ func (ugc *UserGroupController) create(c *gin.Context) {
// @Param userGroup body dto.UserGroupCreateDto true "User group information"
// @Success 200 {object} dto.UserGroupDto "Updated user group"
// @Router /api/user-groups/{id} [put]
func (ugc *UserGroupController) update(c *gin.Context) {
func (ugc *UserGroupController) update(c *gin.Context) error {
dbConfig, err := ugc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
return
return fmt.Errorf("error loading app configuration: %w", err)
}
var input dto.UserGroupCreateDto
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
group, err := ugc.UserGroupService.Update(c.Request.Context(), dbConfig, c.Param("id"), input)
if err != nil {
_ = c.Error(err)
return
return err
}
var groupDto dto.UserGroupDto
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, groupDto)
return nil
}
// delete godoc
@@ -186,19 +179,18 @@ func (ugc *UserGroupController) update(c *gin.Context) {
// @Param id path string true "User Group ID"
// @Success 204 "No Content"
// @Router /api/user-groups/{id} [delete]
func (ugc *UserGroupController) delete(c *gin.Context) {
func (ugc *UserGroupController) delete(c *gin.Context) error {
dbConfig, err := ugc.appConfigService.GetConfig(c.Request.Context())
if err != nil {
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
return
return fmt.Errorf("error loading app configuration: %w", err)
}
if err := ugc.UserGroupService.Delete(c.Request.Context(), dbConfig, c.Param("id")); err != nil {
_ = c.Error(err)
return
return err
}
c.Status(http.StatusNoContent)
return nil
}
// updateUsers godoc
@@ -211,26 +203,24 @@ func (ugc *UserGroupController) delete(c *gin.Context) {
// @Param users body dto.UserGroupUpdateUsersDto true "List of user IDs to assign to this group"
// @Success 200 {object} dto.UserGroupDto
// @Router /api/user-groups/{id}/users [put]
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
func (ugc *UserGroupController) updateUsers(c *gin.Context) error {
var input dto.UserGroupUpdateUsersDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
group, err := ugc.UserGroupService.UpdateUsers(c.Request.Context(), c.Param("id"), input.UserIDs)
if err != nil {
_ = c.Error(err)
return
return err
}
var groupDto dto.UserGroupDto
if err := dto.MapStruct(group, &groupDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, groupDto)
return nil
}
// updateAllowedOidcClients godoc
@@ -243,24 +233,22 @@ func (ugc *UserGroupController) updateUsers(c *gin.Context) {
// @Param groups body dto.UserGroupUpdateAllowedOidcClientsDto true "OIDC client IDs to allow"
// @Success 200 {object} dto.UserGroupDto "Updated user group"
// @Router /api/user-groups/{id}/allowed-oidc-clients [put]
func (ugc *UserGroupController) updateAllowedOidcClients(c *gin.Context) {
func (ugc *UserGroupController) updateAllowedOidcClients(c *gin.Context) error {
var input dto.UserGroupUpdateAllowedOidcClientsDto
if err := c.ShouldBindJSON(&input); err != nil {
_ = c.Error(err)
return
if err := httpserver.BindJSON(c, &input); err != nil {
return err
}
userGroup, err := ugc.UserGroupService.UpdateAllowedOidcClient(c.Request.Context(), c.Param("id"), input)
if err != nil {
_ = c.Error(err)
return
return err
}
var userGroupDto dto.UserGroupDto
if err := dto.MapStruct(userGroup, &userGroupDto); err != nil {
_ = c.Error(err)
return
return err
}
c.JSON(http.StatusOK, userGroupDto)
return nil
}
@@ -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/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
"github.com/pocket-id/pocket-id/backend/internal/service"
"github.com/pocket-id/pocket-id/backend/internal/utils"
@@ -14,8 +15,8 @@ import (
// NewVersionController registers version-related routes.
func NewVersionController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, versionService *service.VersionService) {
vc := &VersionController{versionService: versionService}
group.GET("/version/latest", vc.getLatestVersionHandler)
group.GET("/version/current", authMiddleware.WithAdminNotRequired().Add(), vc.getCurrentVersionHandler)
group.GET("/version/latest", httpserver.Handle(vc.getLatestVersionHandler))
group.GET("/version/current", authMiddleware.WithAdminNotRequired().Add(), httpserver.Handle(vc.getCurrentVersionHandler))
}
type VersionController struct {
@@ -28,11 +29,10 @@ type VersionController struct {
// @Produce json
// @Success 200 {object} map[string]string "Latest version information"
// @Router /api/version/latest [get]
func (vc *VersionController) getLatestVersionHandler(c *gin.Context) {
func (vc *VersionController) getLatestVersionHandler(c *gin.Context) error {
tag, err := vc.versionService.GetLatestVersion(c.Request.Context())
if err != nil {
_ = c.Error(err)
return
return err
}
utils.SetCacheControlHeader(c, 5*time.Minute, 15*time.Minute)
@@ -40,6 +40,7 @@ func (vc *VersionController) getLatestVersionHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"latestVersion": tag,
})
return nil
}
// getCurrentVersionHandler godoc
@@ -48,9 +49,9 @@ func (vc *VersionController) getLatestVersionHandler(c *gin.Context) {
// @Produce json
// @Success 200 {object} map[string]string "Current version information"
// @Router /api/version/current [get]
func (vc *VersionController) getCurrentVersionHandler(c *gin.Context) {
func (vc *VersionController) getCurrentVersionHandler(c *gin.Context) error {
c.JSON(http.StatusOK, gin.H{
"currentVersion": common.Version,
})
return nil
}
@@ -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/httpserver"
"github.com/pocket-id/pocket-id/backend/internal/service"
)
@@ -21,8 +22,8 @@ func NewWellKnownController(group *gin.RouterGroup, jwtService *service.JwtServi
getCIMDURLAllowlist: getCIMDURLAllowlist,
}
group.GET("/.well-known/jwks.json", wkc.jwksHandler)
group.GET("/.well-known/openid-configuration", wkc.openIDConfigurationHandler)
group.GET("/.well-known/jwks.json", httpserver.Handle(wkc.jwksHandler))
group.GET("/.well-known/openid-configuration", httpserver.Handle(wkc.openIDConfigurationHandler))
}
type WellKnownController struct {
@@ -37,14 +38,14 @@ type WellKnownController struct {
// @Produce json
// @Success 200 {object} object "{ \"keys\": []interface{} }"
// @Router /.well-known/jwks.json [get]
func (wkc *WellKnownController) jwksHandler(c *gin.Context) {
func (wkc *WellKnownController) jwksHandler(c *gin.Context) error {
jwks, err := wkc.jwtService.GetPublicJWKSAsJSON()
if err != nil {
_ = c.Error(err)
return
return err
}
c.Data(http.StatusOK, "application/json; charset=utf-8", jwks)
return nil
}
// openIDConfigurationHandler godoc
@@ -53,13 +54,13 @@ func (wkc *WellKnownController) jwksHandler(c *gin.Context) {
// @Tags Well Known
// @Success 200 {object} object "OpenID Connect configuration"
// @Router /.well-known/openid-configuration [get]
func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) error {
oidcConfig, err := wkc.computeOIDCConfiguration()
if err != nil {
_ = c.Error(err)
return
return err
}
c.Data(http.StatusOK, "application/json; charset=utf-8", oidcConfig)
return nil
}
func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {