mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-05 09:19:04 +02:00
feat: built in api documentation via huma gin adapter
This commit is contained in:
@@ -1,40 +1,58 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/tracing"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
// NewAppConfigController creates a new controller for application configuration endpoints
|
||||
// @Summary Create a new application configuration controller
|
||||
// @Description Initialize routes for application configuration
|
||||
// @Tags Application Configuration
|
||||
type appConfigUpdateInput struct {
|
||||
Body dto.AppConfigUpdateDto
|
||||
}
|
||||
|
||||
// NewAppConfigController registers application configuration endpoints
|
||||
func NewAppConfigController(
|
||||
group *gin.RouterGroup,
|
||||
api huma.API,
|
||||
authMiddleware *middleware.AuthMiddleware,
|
||||
appConfigService *service.AppConfigService,
|
||||
emailService *service.EmailService,
|
||||
ldapService *service.LdapService,
|
||||
) {
|
||||
controller := &AppConfigController{appConfigService: appConfigService, emailService: emailService, ldapService: ldapService}
|
||||
|
||||
acc := &AppConfigController{
|
||||
appConfigService: appConfigService,
|
||||
emailService: emailService,
|
||||
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)
|
||||
httpapi.Register(api, appConfigOperation("list-public-application-configuration", http.MethodGet, "/api/application-configuration", "List public application configurations"), controller.listAppConfigHandler)
|
||||
|
||||
group.POST("/application-configuration/test-email", authMiddleware.Add(), acc.testEmailHandler)
|
||||
group.POST("/application-configuration/sync-ldap", authMiddleware.Add(), acc.syncLdapHandler)
|
||||
auth := authMiddleware.Huma(api)
|
||||
allOperation := appConfigOperation("list-all-application-configuration", http.MethodGet, "/api/application-configuration/all", "List all application configurations")
|
||||
auth(&allOperation)
|
||||
httpapi.Register(api, allOperation, controller.listAllAppConfigHandler)
|
||||
|
||||
updateOperation := appConfigOperation("update-application-configuration", http.MethodPut, "/api/application-configuration", "Update application configurations")
|
||||
auth(&updateOperation)
|
||||
httpapi.Register(api, updateOperation, controller.updateAppConfigHandler)
|
||||
|
||||
testEmailOperation := appConfigOperation("test-email-configuration", http.MethodPost, "/api/application-configuration/test-email", "Send test email")
|
||||
testEmailOperation.DefaultStatus = http.StatusNoContent
|
||||
auth(&testEmailOperation)
|
||||
httpapi.Register(api, testEmailOperation, controller.testEmailHandler)
|
||||
|
||||
syncLDAPOperation := appConfigOperation("sync-ldap", http.MethodPost, "/api/application-configuration/sync-ldap", "Synchronize LDAP")
|
||||
syncLDAPOperation.DefaultStatus = http.StatusNoContent
|
||||
auth(&syncLDAPOperation)
|
||||
httpapi.Register(api, syncLDAPOperation, controller.syncLDAPHandler)
|
||||
}
|
||||
|
||||
func appConfigOperation(id, method, path, summary string) huma.Operation {
|
||||
return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"Application Configuration"}}
|
||||
}
|
||||
|
||||
type AppConfigController struct {
|
||||
@@ -43,121 +61,51 @@ type AppConfigController struct {
|
||||
ldapService *service.LdapService
|
||||
}
|
||||
|
||||
// listAppConfigHandler godoc
|
||||
// @Summary List public application configurations
|
||||
// @Description Get all public application configurations
|
||||
// @Tags Application Configuration
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.PublicAppConfigVariableDto
|
||||
// @Router /api/application-configuration [get]
|
||||
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
|
||||
func (acc *AppConfigController) listAppConfigHandler(_ context.Context, _ *httpapi.EmptyInput) (*httpapi.BodyOutput[[]dto.PublicAppConfigVariableDto], error) {
|
||||
configuration := acc.appConfigService.ListAppConfig(false)
|
||||
|
||||
var configVariablesDto []dto.PublicAppConfigVariableDto
|
||||
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
var output []dto.PublicAppConfigVariableDto
|
||||
if err := dto.MapStructList(configuration, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Manually add uiConfigDisabled which isn't in the database but defined with an environment variable
|
||||
configVariablesDto = append(configVariablesDto, dto.PublicAppConfigVariableDto{
|
||||
Key: "uiConfigDisabled",
|
||||
Value: strconv.FormatBool(common.EnvConfig.UiConfigDisabled),
|
||||
Type: "boolean",
|
||||
})
|
||||
|
||||
// Manually add tracingEnabled, derived from the OTel environment, so the frontend only exports traces when the backend can forward them to a collector
|
||||
configVariablesDto = append(configVariablesDto, dto.PublicAppConfigVariableDto{
|
||||
Key: "tracingEnabled",
|
||||
Value: strconv.FormatBool(tracing.FrontendTracingEnabled()),
|
||||
Type: "boolean",
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, configVariablesDto)
|
||||
output = append(output,
|
||||
dto.PublicAppConfigVariableDto{Key: "uiConfigDisabled", Value: strconv.FormatBool(common.EnvConfig.UiConfigDisabled), Type: "boolean"},
|
||||
dto.PublicAppConfigVariableDto{Key: "tracingEnabled", Value: strconv.FormatBool(tracing.FrontendTracingEnabled()), Type: "boolean"},
|
||||
)
|
||||
return &httpapi.BodyOutput[[]dto.PublicAppConfigVariableDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
// listAllAppConfigHandler godoc
|
||||
// @Summary List all application configurations
|
||||
// @Description Get all application configurations including private ones
|
||||
// @Tags Application Configuration
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.AppConfigVariableDto
|
||||
// @Router /api/application-configuration/all [get]
|
||||
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
|
||||
func (acc *AppConfigController) listAllAppConfigHandler(_ context.Context, _ *httpapi.EmptyInput) (*httpapi.BodyOutput[[]dto.AppConfigVariableDto], error) {
|
||||
configuration := acc.appConfigService.ListAppConfig(true)
|
||||
|
||||
var configVariablesDto []dto.AppConfigVariableDto
|
||||
if err := dto.MapStructList(configuration, &configVariablesDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
var output []dto.AppConfigVariableDto
|
||||
if err := dto.MapStructList(configuration, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, configVariablesDto)
|
||||
return &httpapi.BodyOutput[[]dto.AppConfigVariableDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
// updateAppConfigHandler godoc
|
||||
// @Summary Update application configurations
|
||||
// @Description Update application configuration settings
|
||||
// @Tags Application Configuration
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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) {
|
||||
var input dto.AppConfigUpdateDto
|
||||
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
savedConfigVariables, err := acc.appConfigService.UpdateAppConfig(c.Request.Context(), input)
|
||||
func (acc *AppConfigController) updateAppConfigHandler(ctx context.Context, input *appConfigUpdateInput) (*httpapi.BodyOutput[[]dto.AppConfigVariableDto], error) {
|
||||
saved, err := acc.appConfigService.UpdateAppConfig(ctx, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var configVariablesDto []dto.AppConfigVariableDto
|
||||
if err := dto.MapStructList(savedConfigVariables, &configVariablesDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
var output []dto.AppConfigVariableDto
|
||||
if err := dto.MapStructList(saved, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, configVariablesDto)
|
||||
return &httpapi.BodyOutput[[]dto.AppConfigVariableDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
// syncLdapHandler godoc
|
||||
// @Summary Synchronize LDAP
|
||||
// @Description Manually trigger LDAP synchronization
|
||||
// @Tags Application Configuration
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-configuration/sync-ldap [post]
|
||||
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
|
||||
err := acc.ldapService.SyncAll(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
func (acc *AppConfigController) syncLDAPHandler(ctx context.Context, _ *httpapi.EmptyInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := acc.ldapService.SyncAll(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
// testEmailHandler godoc
|
||||
// @Summary Send test email
|
||||
// @Description Send a test email to verify email configuration
|
||||
// @Tags Application Configuration
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-configuration/test-email [post]
|
||||
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
|
||||
err := acc.emailService.SendTestEmail(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
func (acc *AppConfigController) testEmailHandler(ctx context.Context, _ *httpapi.EmptyInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := acc.emailService.SendTestEmail(ctx, httpapi.UserID(ctx)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
@@ -1,292 +1,219 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"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"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
func NewAppImagesController(
|
||||
group *gin.RouterGroup,
|
||||
authMiddleware *middleware.AuthMiddleware,
|
||||
appImagesService *service.AppImagesService,
|
||||
) {
|
||||
controller := &AppImagesController{
|
||||
appImagesService: appImagesService,
|
||||
type imageGetInput struct {
|
||||
Light string `query:"light" default:"true" required:"false"`
|
||||
}
|
||||
|
||||
type imageUploadForm struct {
|
||||
File huma.FormFile `form:"file" required:"true"`
|
||||
}
|
||||
|
||||
type imageUploadInput struct {
|
||||
RawBody huma.MultipartFormFiles[imageUploadForm]
|
||||
}
|
||||
|
||||
type logoUploadInput struct {
|
||||
Light string `query:"light" default:"true" required:"false"`
|
||||
RawBody huma.MultipartFormFiles[imageUploadForm]
|
||||
}
|
||||
|
||||
type imageOutput struct {
|
||||
ContentType string `header:"Content-Type"`
|
||||
ContentLength int64 `header:"Content-Length"`
|
||||
CacheControl string `header:"Cache-Control"`
|
||||
Body func(huma.Context)
|
||||
}
|
||||
|
||||
func NewAppImagesController(api huma.API, authMiddleware *middleware.AuthMiddleware, appImagesService *service.AppImagesService) {
|
||||
controller := &AppImagesController{appImagesService: appImagesService}
|
||||
|
||||
httpapi.Register(api, imageOperation("get-application-logo", http.MethodGet, "/api/application-images/logo", "Get logo image"), controller.getLogoHandler)
|
||||
httpapi.Register(api, imageOperation("get-email-logo", http.MethodGet, "/api/application-images/email", "Get email logo image"), controller.getEmailLogoHandler)
|
||||
httpapi.Register(api, imageOperation("get-background-image", http.MethodGet, "/api/application-images/background", "Get background image"), controller.getBackgroundImageHandler)
|
||||
httpapi.Register(api, imageOperation("get-favicon", http.MethodGet, "/api/application-images/favicon", "Get favicon"), controller.getFaviconHandler)
|
||||
|
||||
auth := authMiddleware.Huma(api)
|
||||
defaultPictureOperation := imageOperation("get-default-profile-picture", http.MethodGet, "/api/application-images/default-profile-picture", "Get default profile picture")
|
||||
auth(&defaultPictureOperation)
|
||||
httpapi.Register(api, defaultPictureOperation, controller.getDefaultProfilePicture)
|
||||
|
||||
logoOperation := imageOperation("update-application-logo", http.MethodPut, "/api/application-images/logo", "Update logo")
|
||||
logoOperation.DefaultStatus = http.StatusNoContent
|
||||
auth(&logoOperation)
|
||||
httpapi.Register(api, logoOperation, controller.updateLogoHandler)
|
||||
|
||||
registerUpload := func(operation huma.Operation, handler func(context.Context, *imageUploadInput) (*httpapi.EmptyOutput, error)) {
|
||||
operation.DefaultStatus = http.StatusNoContent
|
||||
auth(&operation)
|
||||
httpapi.Register(api, operation, handler)
|
||||
}
|
||||
registerUpload(imageOperation("update-email-logo", http.MethodPut, "/api/application-images/email", "Update email logo"), controller.updateEmailLogoHandler)
|
||||
registerUpload(imageOperation("update-background-image", http.MethodPut, "/api/application-images/background", "Update background image"), controller.updateBackgroundImageHandler)
|
||||
registerUpload(imageOperation("update-favicon", http.MethodPut, "/api/application-images/favicon", "Update favicon"), controller.updateFaviconHandler)
|
||||
registerUpload(imageOperation("update-default-profile-picture", http.MethodPut, "/api/application-images/default-profile-picture", "Update default profile picture"), controller.updateDefaultProfilePicture)
|
||||
|
||||
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)
|
||||
registerDelete := func(operation huma.Operation, handler func(context.Context, *httpapi.EmptyInput) (*httpapi.EmptyOutput, error)) {
|
||||
operation.DefaultStatus = http.StatusNoContent
|
||||
auth(&operation)
|
||||
httpapi.Register(api, operation, handler)
|
||||
}
|
||||
registerDelete(imageOperation("delete-background-image", http.MethodDelete, "/api/application-images/background", "Delete background image"), controller.deleteBackgroundImageHandler)
|
||||
registerDelete(imageOperation("delete-default-profile-picture", http.MethodDelete, "/api/application-images/default-profile-picture", "Delete default profile picture"), controller.deleteDefaultProfilePicture)
|
||||
}
|
||||
|
||||
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.DELETE("/application-images/background", authMiddleware.Add(), controller.deleteBackgroundImageHandler)
|
||||
group.DELETE("/application-images/default-profile-picture", authMiddleware.Add(), controller.deleteDefaultProfilePicture)
|
||||
func imageOperation(id, method, path, summary string) huma.Operation {
|
||||
return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"Application Images"}}
|
||||
}
|
||||
|
||||
type AppImagesController struct {
|
||||
appImagesService *service.AppImagesService
|
||||
}
|
||||
|
||||
// getLogoHandler godoc
|
||||
// @Summary Get logo image
|
||||
// @Description Get the logo image for the application
|
||||
// @Tags Application Images
|
||||
// @Param light query boolean false "Light mode logo (true) or dark mode logo (false)"
|
||||
// @Produce image/png
|
||||
// @Produce image/jpeg
|
||||
// @Produce image/svg+xml
|
||||
// @Success 200 {file} binary "Logo image"
|
||||
// @Router /api/application-images/logo [get]
|
||||
func (c *AppImagesController) getLogoHandler(ctx *gin.Context) {
|
||||
lightLogo, _ := strconv.ParseBool(ctx.DefaultQuery("light", "true"))
|
||||
func (c *AppImagesController) getLogoHandler(ctx context.Context, input *imageGetInput) (*imageOutput, error) {
|
||||
lightLogo, _ := strconv.ParseBool(input.Light)
|
||||
imageName := "logoLight"
|
||||
if !lightLogo {
|
||||
imageName = "logoDark"
|
||||
}
|
||||
|
||||
c.getImage(ctx, imageName)
|
||||
return c.getImage(ctx, imageName)
|
||||
}
|
||||
|
||||
// getEmailLogoHandler godoc
|
||||
// @Summary Get email logo image
|
||||
// @Description Get the email logo image for use in emails
|
||||
// @Tags Application Images
|
||||
// @Produce image/png
|
||||
// @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 context.Context, _ *httpapi.EmptyInput) (*imageOutput, error) {
|
||||
return c.getImage(ctx, "logoEmail")
|
||||
}
|
||||
|
||||
// getBackgroundImageHandler godoc
|
||||
// @Summary Get background image
|
||||
// @Description Get the background image for the application
|
||||
// @Tags Application Images
|
||||
// @Produce image/png
|
||||
// @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 context.Context, _ *httpapi.EmptyInput) (*imageOutput, error) {
|
||||
return c.getImage(ctx, "background")
|
||||
}
|
||||
|
||||
// getFaviconHandler godoc
|
||||
// @Summary Get favicon
|
||||
// @Description Get the favicon for the application
|
||||
// @Tags Application Images
|
||||
// @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 context.Context, _ *httpapi.EmptyInput) (*imageOutput, error) {
|
||||
return c.getImage(ctx, "favicon")
|
||||
}
|
||||
|
||||
// getDefaultProfilePicture godoc
|
||||
// @Summary Get default profile picture image
|
||||
// @Description Get the default profile picture image for the application
|
||||
// @Tags Application Images
|
||||
// @Produce image/png
|
||||
// @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 context.Context, _ *httpapi.EmptyInput) (*imageOutput, error) {
|
||||
return c.getImage(ctx, "default-profile-picture")
|
||||
}
|
||||
|
||||
// updateLogoHandler godoc
|
||||
// @Summary Update logo
|
||||
// @Description Update the application logo
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @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"
|
||||
// @Router /api/application-images/logo [put]
|
||||
func (c *AppImagesController) updateLogoHandler(ctx *gin.Context) {
|
||||
file, err := ctx.FormFile("file")
|
||||
func (c *AppImagesController) updateLogoHandler(ctx context.Context, input *logoUploadInput) (*httpapi.EmptyOutput, error) {
|
||||
file, err := uploadFile(input.RawBody.Form)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lightLogo, _ := strconv.ParseBool(ctx.DefaultQuery("light", "true"))
|
||||
lightLogo, _ := strconv.ParseBool(input.Light)
|
||||
imageName := "logoLight"
|
||||
if !lightLogo {
|
||||
imageName = "logoDark"
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, imageName); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
if err := c.appImagesService.UpdateImage(ctx, file, imageName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
// updateEmailLogoHandler godoc
|
||||
// @Summary Update email logo
|
||||
// @Description Update the email logo for use in emails
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @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 context.Context, input *imageUploadInput) (*httpapi.EmptyOutput, error) {
|
||||
file, err := uploadFile(input.RawBody.Form)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileType := utils.GetFileExtension(file.Filename)
|
||||
mimeType := utils.GetImageMimeType(fileType)
|
||||
|
||||
mimeType := utils.GetImageMimeType(utils.GetFileExtension(file.Filename))
|
||||
if mimeType != "image/png" && mimeType != "image/jpeg" {
|
||||
_ = ctx.Error(&common.WrongFileTypeError{ExpectedFileType: ".png or .jpg/jpeg"})
|
||||
return
|
||||
return nil, &common.WrongFileTypeError{ExpectedFileType: ".png or .jpg/jpeg"}
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "logoEmail"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return c.updateImage(ctx, file, "logoEmail")
|
||||
}
|
||||
|
||||
// updateBackgroundImageHandler godoc
|
||||
// @Summary Update background image
|
||||
// @Description Update the application background image
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @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 context.Context, input *imageUploadInput) (*httpapi.EmptyOutput, error) {
|
||||
file, err := uploadFile(input.RawBody.Form)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "background"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return c.updateImage(ctx, file, "background")
|
||||
}
|
||||
|
||||
// deleteBackgroundImageHandler godoc
|
||||
// @Summary Delete background image
|
||||
// @Description Delete the application background image
|
||||
// @Tags Application Images
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-images/background [delete]
|
||||
func (c *AppImagesController) deleteBackgroundImageHandler(ctx *gin.Context) {
|
||||
if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "background"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// updateFaviconHandler godoc
|
||||
// @Summary Update favicon
|
||||
// @Description Update the application favicon
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @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 context.Context, input *imageUploadInput) (*httpapi.EmptyOutput, error) {
|
||||
file, err := uploadFile(input.RawBody.Form)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileType := utils.GetFileExtension(file.Filename)
|
||||
mimeType := utils.GetImageMimeType(strings.ToLower(fileType))
|
||||
mimeType := utils.GetImageMimeType(strings.ToLower(utils.GetFileExtension(file.Filename)))
|
||||
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 nil, &common.WrongFileTypeError{ExpectedFileType: ".svg or .png or .ico"}
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "favicon"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return c.updateImage(ctx, file, "favicon")
|
||||
}
|
||||
|
||||
func (c *AppImagesController) getImage(ctx *gin.Context, name string) {
|
||||
reader, size, mimeType, err := c.appImagesService.GetImage(ctx.Request.Context(), name)
|
||||
func (c *AppImagesController) updateDefaultProfilePicture(ctx context.Context, input *imageUploadInput) (*httpapi.EmptyOutput, error) {
|
||||
file, err := uploadFile(input.RawBody.Form)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, 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 c.updateImage(ctx, file, "default-profile-picture")
|
||||
}
|
||||
|
||||
// updateDefaultProfilePicture godoc
|
||||
// @Summary Update default profile picture image
|
||||
// @Description Update the default profile picture image
|
||||
// @Tags Application Images
|
||||
// @Accept multipart/form-data
|
||||
// @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) updateImage(ctx context.Context, file *multipart.FileHeader, name string) (*httpapi.EmptyOutput, error) {
|
||||
if err := c.appImagesService.UpdateImage(ctx, file, name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
func uploadFile(form *multipart.Form) (*multipart.FileHeader, error) {
|
||||
files := form.File["file"]
|
||||
if len(files) == 0 {
|
||||
return nil, http.ErrMissingFile
|
||||
}
|
||||
return files[0], nil
|
||||
}
|
||||
|
||||
func (c *AppImagesController) deleteBackgroundImageHandler(ctx context.Context, _ *httpapi.EmptyInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := c.appImagesService.DeleteImage(ctx, "background"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
func (c *AppImagesController) deleteDefaultProfilePicture(ctx context.Context, _ *httpapi.EmptyInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := c.appImagesService.DeleteImage(ctx, "default-profile-picture"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
func (c *AppImagesController) getImage(ctx context.Context, name string) (*imageOutput, error) {
|
||||
reader, size, mimeType, err := c.appImagesService.GetImage(ctx, name)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.appImagesService.UpdateImage(ctx.Request.Context(), file, "default-profile-picture"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
cacheControl := ""
|
||||
if !httpapi.QueryPresent(ctx, "skipCache") {
|
||||
cacheControl = utils.CacheControlValue(15*time.Minute, 24*time.Hour)
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// deleteDefaultProfilePicture godoc
|
||||
// @Summary Delete default profile picture image
|
||||
// @Description Delete the default profile picture image
|
||||
// @Tags Application Images
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/application-images/default-profile-picture [delete]
|
||||
func (c *AppImagesController) deleteDefaultProfilePicture(ctx *gin.Context) {
|
||||
if err := c.appImagesService.DeleteImage(ctx.Request.Context(), "default-profile-picture"); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return &imageOutput{
|
||||
ContentType: mimeType,
|
||||
ContentLength: size,
|
||||
CacheControl: cacheControl,
|
||||
Body: func(streamCtx huma.Context) {
|
||||
defer reader.Close()
|
||||
_, _ = io.Copy(streamCtx.BodyWriter(), reader)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,145 +1,96 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
// NewAuditLogController creates a new controller for audit log management
|
||||
// @Summary Audit log controller
|
||||
// @Description Initializes API endpoints for accessing audit logs
|
||||
// @Tags Audit Logs
|
||||
func NewAuditLogController(group *gin.RouterGroup, auditLogService *service.AuditLogService, authMiddleware *middleware.AuthMiddleware) {
|
||||
alc := AuditLogController{
|
||||
auditLogService: auditLogService,
|
||||
}
|
||||
// NewAuditLogController registers audit log routes
|
||||
func NewAuditLogController(api huma.API, auditLogService *service.AuditLogService, authMiddleware *middleware.AuthMiddleware) {
|
||||
controller := &AuditLogController{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)
|
||||
allOperation := huma.Operation{OperationID: "list-all-audit-logs", Method: http.MethodGet, Path: "/api/audit-logs/all", Summary: "List all audit logs", Tags: []string{"Audit Logs"}}
|
||||
authMiddleware.Huma(api)(&allOperation)
|
||||
httpapi.Register(api, allOperation, controller.listAllAuditLogsHandler)
|
||||
|
||||
userOperation := huma.Operation{OperationID: "list-current-user-audit-logs", Method: http.MethodGet, Path: "/api/audit-logs", Summary: "List audit logs for the current user", Tags: []string{"Audit Logs"}}
|
||||
authMiddleware.WithAdminNotRequired().Huma(api)(&userOperation)
|
||||
httpapi.Register(api, userOperation, controller.listAuditLogsForUserHandler)
|
||||
|
||||
clientsOperation := huma.Operation{OperationID: "list-audit-log-client-names", Method: http.MethodGet, Path: "/api/audit-logs/filters/client-names", Summary: "List client names", Tags: []string{"Audit Logs"}}
|
||||
authMiddleware.Huma(api)(&clientsOperation)
|
||||
httpapi.Register(api, clientsOperation, controller.listClientNamesHandler)
|
||||
|
||||
usersOperation := huma.Operation{OperationID: "list-audit-log-users", Method: http.MethodGet, Path: "/api/audit-logs/filters/users", Summary: "List users with IDs", Tags: []string{"Audit Logs"}}
|
||||
authMiddleware.Huma(api)(&usersOperation)
|
||||
httpapi.Register(api, usersOperation, controller.listUserNamesWithIDsHandler)
|
||||
}
|
||||
|
||||
type AuditLogController struct {
|
||||
auditLogService *service.AuditLogService
|
||||
}
|
||||
|
||||
// listAuditLogsForUserHandler godoc
|
||||
// @Summary List audit logs
|
||||
// @Description Get a paginated list of audit logs for the current user
|
||||
// @Tags Audit Logs
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @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]
|
||||
// @Router /api/audit-logs [get]
|
||||
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
userID := c.GetString("userID")
|
||||
|
||||
// Fetch audit logs for the user
|
||||
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(c.Request.Context(), userID, listRequestOptions)
|
||||
func (alc *AuditLogController) listAuditLogsForUserHandler(ctx context.Context, input *httpapi.ListInput) (*httpapi.BodyOutput[dto.Paginated[dto.AuditLogDto]], error) {
|
||||
logs, pagination, err := alc.auditLogService.ListAuditLogsForUser(ctx, httpapi.UserID(ctx), input.ListRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map the audit logs to DTOs
|
||||
var logsDtos []dto.AuditLogDto
|
||||
err = dto.MapStructList(logs, &logsDtos)
|
||||
logsDTOs, err := alc.mapAuditLogs(logs, false)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Add device information to the logs
|
||||
for i, logsDto := range logsDtos {
|
||||
logsDto.Device = alc.auditLogService.DeviceStringFromUserAgent(logs[i].UserAgent)
|
||||
logsDto.ActorUsername = logsDto.Data["actorUsername"]
|
||||
logsDtos[i] = logsDto
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[dto.AuditLogDto]{
|
||||
Data: logsDtos,
|
||||
Pagination: pagination,
|
||||
})
|
||||
return &httpapi.BodyOutput[dto.Paginated[dto.AuditLogDto]]{Body: dto.Paginated[dto.AuditLogDto]{Data: logsDTOs, Pagination: pagination}}, nil
|
||||
}
|
||||
|
||||
// listAllAuditLogsHandler godoc
|
||||
// @Summary List all audit logs
|
||||
// @Description Get a paginated list of all audit logs (admin only)
|
||||
// @Tags Audit Logs
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @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]
|
||||
// @Router /api/audit-logs/all [get]
|
||||
func (alc *AuditLogController) listAllAuditLogsHandler(c *gin.Context) {
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(c.Request.Context(), listRequestOptions)
|
||||
func (alc *AuditLogController) listAllAuditLogsHandler(ctx context.Context, input *httpapi.ListInput) (*httpapi.BodyOutput[dto.Paginated[dto.AuditLogDto]], error) {
|
||||
logs, pagination, err := alc.auditLogService.ListAllAuditLogs(ctx, input.ListRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var logsDtos []dto.AuditLogDto
|
||||
err = dto.MapStructList(logs, &logsDtos)
|
||||
logsDTOs, err := alc.mapAuditLogs(logs, true)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, logsDto := range logsDtos {
|
||||
logsDto.Device = alc.auditLogService.DeviceStringFromUserAgent(logs[i].UserAgent)
|
||||
logsDto.Username = logs[i].User.Username
|
||||
logsDto.ActorUsername = logsDto.Data["actorUsername"]
|
||||
logsDtos[i] = logsDto
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[dto.AuditLogDto]{
|
||||
Data: logsDtos,
|
||||
Pagination: pagination,
|
||||
})
|
||||
return &httpapi.BodyOutput[dto.Paginated[dto.AuditLogDto]]{Body: dto.Paginated[dto.AuditLogDto]{Data: logsDTOs, Pagination: pagination}}, nil
|
||||
}
|
||||
|
||||
// listClientNamesHandler godoc
|
||||
// @Summary List client names
|
||||
// @Description Get a list of all client names for audit log filtering
|
||||
// @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) {
|
||||
names, err := alc.auditLogService.ListClientNames(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
func (alc *AuditLogController) mapAuditLogs(logs []model.AuditLog, includeUsername bool) ([]dto.AuditLogDto, error) {
|
||||
var logsDTOs []dto.AuditLogDto
|
||||
if err := dto.MapStructList(logs, &logsDTOs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, names)
|
||||
for i := range logsDTOs {
|
||||
logsDTOs[i].Device = alc.auditLogService.DeviceStringFromUserAgent(logs[i].UserAgent)
|
||||
logsDTOs[i].ActorUsername = logsDTOs[i].Data["actorUsername"]
|
||||
if includeUsername {
|
||||
logsDTOs[i].Username = logs[i].User.Username
|
||||
}
|
||||
}
|
||||
return logsDTOs, nil
|
||||
}
|
||||
|
||||
// listUserNamesWithIdsHandler godoc
|
||||
// @Summary List users with IDs
|
||||
// @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"
|
||||
// @Router /api/audit-logs/filters/users [get]
|
||||
func (alc *AuditLogController) listUserNamesWithIdsHandler(c *gin.Context) {
|
||||
users, err := alc.auditLogService.ListUsernamesWithIds(c.Request.Context())
|
||||
func (alc *AuditLogController) listClientNamesHandler(ctx context.Context, _ *httpapi.EmptyInput) (*httpapi.BodyOutput[[]string], error) {
|
||||
names, err := alc.auditLogService.ListClientNames(ctx)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.BodyOutput[[]string]{Body: names}, nil
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, users)
|
||||
func (alc *AuditLogController) listUserNamesWithIDsHandler(ctx context.Context, _ *httpapi.EmptyInput) (*httpapi.BodyOutput[map[string]string], error) {
|
||||
users, err := alc.auditLogService.ListUsernamesWithIds(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.BodyOutput[map[string]string]{Body: users}, nil
|
||||
}
|
||||
|
||||
@@ -1,115 +1,79 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
// NewCustomClaimController creates a new controller for custom claim management
|
||||
// @Summary Custom claim management controller
|
||||
// @Description Initializes all custom claim-related API endpoints
|
||||
// @Tags Custom Claims
|
||||
func NewCustomClaimController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, customClaimService *service.CustomClaimService) {
|
||||
wkc := &CustomClaimController{customClaimService: customClaimService}
|
||||
type customClaimUserInput struct {
|
||||
UserID string `path:"userId"`
|
||||
Body []dto.CustomClaimCreateDto `required:"true"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
type customClaimUserGroupInput struct {
|
||||
UserGroupID string `path:"userGroupId"`
|
||||
Body []dto.CustomClaimCreateDto `required:"true"`
|
||||
}
|
||||
|
||||
// NewCustomClaimController registers custom claim management routes
|
||||
func NewCustomClaimController(api huma.API, authMiddleware *middleware.AuthMiddleware, customClaimService *service.CustomClaimService) {
|
||||
controller := &CustomClaimController{customClaimService: customClaimService}
|
||||
auth := authMiddleware.Huma(api)
|
||||
|
||||
suggestionsOperation := huma.Operation{OperationID: "list-custom-claim-suggestions", Method: http.MethodGet, Path: "/api/custom-claims/suggestions", Summary: "Get custom claim suggestions", Tags: []string{"Custom Claims"}}
|
||||
auth(&suggestionsOperation)
|
||||
httpapi.Register(api, suggestionsOperation, controller.getSuggestionsHandler)
|
||||
|
||||
userOperation := huma.Operation{OperationID: "update-user-custom-claims", Method: http.MethodPut, Path: "/api/custom-claims/user/{userId}", Summary: "Update custom claims for a user", Tags: []string{"Custom Claims"}}
|
||||
auth(&userOperation)
|
||||
httpapi.Register(api, userOperation, controller.updateCustomClaimsForUserHandler)
|
||||
|
||||
userGroupOperation := huma.Operation{OperationID: "update-user-group-custom-claims", Method: http.MethodPut, Path: "/api/custom-claims/user-group/{userGroupId}", Summary: "Update custom claims for a user group", Tags: []string{"Custom Claims"}}
|
||||
auth(&userGroupOperation)
|
||||
httpapi.Register(api, userGroupOperation, controller.updateCustomClaimsForUserGroupHandler)
|
||||
}
|
||||
|
||||
type CustomClaimController struct {
|
||||
customClaimService *service.CustomClaimService
|
||||
}
|
||||
|
||||
// getSuggestionsHandler godoc
|
||||
// @Summary Get custom claim suggestions
|
||||
// @Description Get a list of suggested custom claim names
|
||||
// @Tags Custom Claims
|
||||
// @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) {
|
||||
claims, err := ccc.customClaimService.GetSuggestions(c.Request.Context())
|
||||
func (ccc *CustomClaimController) getSuggestionsHandler(ctx context.Context, _ *httpapi.EmptyInput) (*httpapi.BodyOutput[[]string], error) {
|
||||
claims, err := ccc.customClaimService.GetSuggestions(ctx)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, claims)
|
||||
return &httpapi.BodyOutput[[]string]{Body: claims}, nil
|
||||
}
|
||||
|
||||
// UpdateCustomClaimsForUserHandler godoc
|
||||
// @Summary Update custom claims for a user
|
||||
// @Description Update or create custom claims for a specific user
|
||||
// @Tags Custom Claims
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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"
|
||||
// @Router /api/custom-claims/user/{userId} [put]
|
||||
func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Context) {
|
||||
var input []dto.CustomClaimCreateDto
|
||||
|
||||
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.Param("userId")
|
||||
claims, err := ccc.customClaimService.UpdateCustomClaimsForUser(c.Request.Context(), userId, input)
|
||||
func (ccc *CustomClaimController) updateCustomClaimsForUserHandler(ctx context.Context, input *customClaimUserInput) (*httpapi.BodyOutput[[]dto.CustomClaimDto], error) {
|
||||
claims, err := ccc.customClaimService.UpdateCustomClaimsForUser(ctx, input.UserID, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var customClaimsDto []dto.CustomClaimDto
|
||||
if err := dto.MapStructList(claims, &customClaimsDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
var output []dto.CustomClaimDto
|
||||
if err := dto.MapStructList(claims, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, customClaimsDto)
|
||||
return &httpapi.BodyOutput[[]dto.CustomClaimDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
// UpdateCustomClaimsForUserGroupHandler godoc
|
||||
// @Summary Update custom claims for a user group
|
||||
// @Description Update or create custom claims for a specific user group
|
||||
// @Tags Custom Claims
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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"
|
||||
// @Router /api/custom-claims/user-group/{userGroupId} [put]
|
||||
func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.Context) {
|
||||
var input []dto.CustomClaimCreateDto
|
||||
|
||||
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
userGroupId := c.Param("userGroupId")
|
||||
claims, err := ccc.customClaimService.UpdateCustomClaimsForUserGroup(c.Request.Context(), userGroupId, input)
|
||||
func (ccc *CustomClaimController) updateCustomClaimsForUserGroupHandler(ctx context.Context, input *customClaimUserGroupInput) (*httpapi.BodyOutput[[]dto.CustomClaimDto], error) {
|
||||
claims, err := ccc.customClaimService.UpdateCustomClaimsForUserGroup(ctx, input.UserGroupID, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var customClaimsDto []dto.CustomClaimDto
|
||||
if err := dto.MapStructList(claims, &customClaimsDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
var output []dto.CustomClaimDto
|
||||
if err := dto.MapStructList(claims, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, customClaimsDto)
|
||||
return &httpapi.BodyOutput[[]dto.CustomClaimDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
@@ -3,150 +3,138 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
func NewTestController(group *gin.RouterGroup, testService *service.TestService) {
|
||||
testController := &TestController{TestService: testService}
|
||||
type testResetInput struct {
|
||||
SkipLDAP string `query:"skip-ldap" required:"false"`
|
||||
SkipSeed string `query:"skip-seed" required:"false"`
|
||||
}
|
||||
|
||||
group.POST("/test/reset", testController.resetAndSeedHandler)
|
||||
group.POST("/test/accesstoken", testController.signAccessToken)
|
||||
group.POST("/test/refreshtoken", testController.signRefreshToken)
|
||||
type testExternalIDPInput struct {
|
||||
Body struct {
|
||||
Audience string `json:"aud" required:"true"`
|
||||
Issuer string `json:"iss" required:"true"`
|
||||
Subject string `json:"sub" required:"true"`
|
||||
}
|
||||
}
|
||||
|
||||
group.GET("/externalidp/jwks.json", testController.externalIdPJWKS)
|
||||
group.POST("/externalidp/sign", testController.externalIdPSignToken)
|
||||
type testAccessTokenInput struct {
|
||||
Body struct {
|
||||
UserID string `json:"user" required:"true"`
|
||||
ClientID string `json:"client" required:"true"`
|
||||
Expired bool `json:"expired" required:"false"`
|
||||
}
|
||||
}
|
||||
|
||||
type testRefreshTokenInput struct {
|
||||
Body struct {
|
||||
UserID string `json:"user" required:"true"`
|
||||
ClientID string `json:"client" required:"true"`
|
||||
RefreshToken string `json:"rt" required:"true"`
|
||||
}
|
||||
}
|
||||
|
||||
type testBytesOutput struct {
|
||||
ContentType string `header:"Content-Type"`
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func NewTestController(api huma.API, testService *service.TestService) {
|
||||
controller := &TestController{TestService: testService}
|
||||
|
||||
resetOperation := testOperation("test-reset", http.MethodPost, "/api/test/reset")
|
||||
resetOperation.DefaultStatus = http.StatusNoContent
|
||||
httpapi.Register(api, resetOperation, controller.resetAndSeedHandler)
|
||||
httpapi.Register(api, testOperation("test-sign-access-token", http.MethodPost, "/api/test/accesstoken"), controller.signAccessToken)
|
||||
httpapi.Register(api, testOperation("test-sign-refresh-token", http.MethodPost, "/api/test/refreshtoken"), controller.signRefreshToken)
|
||||
httpapi.Register(api, testOperation("test-external-idp-jwks", http.MethodGet, "/api/externalidp/jwks.json"), controller.externalIDPJWKS)
|
||||
httpapi.Register(api, testOperation("test-external-idp-sign", http.MethodPost, "/api/externalidp/sign"), controller.externalIDPSignToken)
|
||||
}
|
||||
|
||||
func testOperation(id, method, path string) huma.Operation {
|
||||
return huma.Operation{OperationID: id, Method: method, Path: path, Tags: []string{"E2E Test"}, Hidden: true}
|
||||
}
|
||||
|
||||
type TestController struct {
|
||||
TestService *service.TestService
|
||||
}
|
||||
|
||||
func (tc *TestController) resetAndSeedHandler(c *gin.Context) {
|
||||
var baseURL string
|
||||
if c.Request.TLS != nil {
|
||||
baseURL = "https://" + c.Request.Host
|
||||
} else {
|
||||
baseURL = "http://" + c.Request.Host
|
||||
func (tc *TestController) resetAndSeedHandler(ctx context.Context, input *testResetInput) (*httpapi.EmptyOutput, error) {
|
||||
request := httpapi.Request(ctx)
|
||||
scheme := "http"
|
||||
if request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
skipLdap := c.Query("skip-ldap") == "true"
|
||||
skipSeed := c.Query("skip-seed") == "true"
|
||||
baseURL := scheme + "://" + request.Host
|
||||
|
||||
if err := tc.TestService.ResetDatabase(); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tc.TestService.ResetLock(c.Request.Context()); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
if err := tc.TestService.ResetLock(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tc.TestService.ResetApplicationImages(c.Request.Context()); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
if err := tc.TestService.ResetApplicationImages(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipSeed {
|
||||
if input.SkipSeed != "true" {
|
||||
if err := tc.TestService.SeedDatabase(baseURL); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tc.TestService.ResetAppConfig(c.Request.Context()); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
if err := tc.TestService.ResetAppConfig(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !skipLdap {
|
||||
if err := tc.TestService.SetLdapTestConfig(c.Request.Context()); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
if input.SkipLDAP != "true" {
|
||||
if err := tc.TestService.SetLdapTestConfig(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tc.TestService.SyncLdap(c.Request.Context()); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
if err := tc.TestService.SyncLdap(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
func (tc *TestController) externalIdPJWKS(c *gin.Context) {
|
||||
func (tc *TestController) externalIDPJWKS(_ context.Context, _ *httpapi.EmptyInput) (*testBytesOutput, error) {
|
||||
jwks, err := tc.TestService.GetExternalIdPJWKS()
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, jwks)
|
||||
body, err := json.Marshal(jwks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &testBytesOutput{ContentType: "application/json; charset=utf-8", Body: body}, nil
|
||||
}
|
||||
|
||||
func (tc *TestController) externalIdPSignToken(c *gin.Context) {
|
||||
var input struct {
|
||||
Aud string `json:"aud"`
|
||||
Iss string `json:"iss"`
|
||||
Sub string `json:"sub"`
|
||||
}
|
||||
err := c.ShouldBindJSON(&input)
|
||||
func (tc *TestController) externalIDPSignToken(_ context.Context, input *testExternalIDPInput) (*testBytesOutput, error) {
|
||||
token, err := tc.TestService.SignExternalIdPToken(input.Body.Issuer, input.Body.Subject, input.Body.Audience)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := tc.TestService.SignExternalIdPToken(input.Iss, input.Sub, input.Aud)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.WriteString(token)
|
||||
return &testBytesOutput{ContentType: "text/plain; charset=utf-8", Body: []byte(token)}, nil
|
||||
}
|
||||
|
||||
func (tc *TestController) signAccessToken(c *gin.Context) {
|
||||
var input struct {
|
||||
UserID string `json:"user"`
|
||||
ClientID string `json:"client"`
|
||||
Expired bool `json:"expired"`
|
||||
}
|
||||
err := c.ShouldBindJSON(&input)
|
||||
func (tc *TestController) signAccessToken(ctx context.Context, input *testAccessTokenInput) (*testBytesOutput, error) {
|
||||
token, err := tc.TestService.SignAccessToken(ctx, input.Body.UserID, input.Body.ClientID, input.Body.Expired)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := tc.TestService.SignAccessToken(c.Request.Context(), input.UserID, input.ClientID, input.Expired)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.WriteString(token)
|
||||
return &testBytesOutput{ContentType: "text/plain; charset=utf-8", Body: []byte(token)}, nil
|
||||
}
|
||||
|
||||
func (tc *TestController) signRefreshToken(c *gin.Context) {
|
||||
var input struct {
|
||||
UserID string `json:"user"`
|
||||
ClientID string `json:"client"`
|
||||
RefreshToken string `json:"rt"`
|
||||
}
|
||||
err := c.ShouldBindJSON(&input)
|
||||
func (tc *TestController) signRefreshToken(ctx context.Context, input *testRefreshTokenInput) (*testBytesOutput, error) {
|
||||
token, err := tc.TestService.SignRefreshToken(ctx, input.Body.UserID, input.Body.ClientID, input.Body.RefreshToken)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := tc.TestService.SignRefreshToken(c.Request.Context(), input.UserID, input.ClientID, input.RefreshToken)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Writer.WriteString(token)
|
||||
return &testBytesOutput{ContentType: "text/plain; charset=utf-8", Body: []byte(token)}, nil
|
||||
}
|
||||
|
||||
@@ -1,538 +1,368 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
// NewOidcController creates a new controller for OIDC related endpoints
|
||||
// @Summary OIDC controller
|
||||
// @Description Initializes all OIDC-related API endpoints for authentication and client management
|
||||
// @Tags OIDC
|
||||
func NewOidcController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, oidcService *service.OidcService) {
|
||||
oc := &OidcController{
|
||||
oidcService: oidcService,
|
||||
}
|
||||
type oidcClientIDInput struct {
|
||||
ID string `path:"id"`
|
||||
}
|
||||
|
||||
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.DELETE("/oidc/clients/:id", authMiddleware.Add(), oc.deleteClientHandler)
|
||||
type oidcClientListInput struct {
|
||||
utils.ListRequestOptions
|
||||
Search string `query:"search" required:"false"`
|
||||
}
|
||||
|
||||
group.PUT("/oidc/clients/:id/allowed-user-groups", authMiddleware.Add(), oc.updateAllowedUserGroupsHandler)
|
||||
group.POST("/oidc/clients/:id/secret", authMiddleware.Add(), oc.createClientSecretHandler)
|
||||
type oidcClientCreateInput struct {
|
||||
Body dto.OidcClientCreateDto
|
||||
}
|
||||
|
||||
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)
|
||||
type oidcClientUpdateInput struct {
|
||||
ID string `path:"id"`
|
||||
Body dto.OidcClientUpdateDto
|
||||
}
|
||||
|
||||
group.GET("/oidc/clients/:id/preview/:userId", authMiddleware.Add(), oc.getClientPreviewHandler)
|
||||
type oidcAllowedGroupsInput struct {
|
||||
ID string `path:"id"`
|
||||
Body dto.OidcUpdateAllowedUserGroupsDto
|
||||
}
|
||||
|
||||
group.GET("/oidc/users/me/authorized-clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAuthorizedClientsHandler)
|
||||
group.GET("/oidc/users/:id/authorized-clients", authMiddleware.Add(), oc.listAuthorizedClientsHandler)
|
||||
type oidcLogoInput struct {
|
||||
ID string `path:"id"`
|
||||
Light string `query:"light" default:"true" required:"false"`
|
||||
}
|
||||
|
||||
group.DELETE("/oidc/users/me/authorized-clients/:clientId", authMiddleware.WithAdminNotRequired().Add(), oc.revokeOwnClientAuthorizationHandler)
|
||||
type oidcLogoUploadInput struct {
|
||||
ID string `path:"id"`
|
||||
Light string `query:"light" default:"true" required:"false"`
|
||||
RawBody huma.MultipartFormFiles[imageUploadForm]
|
||||
}
|
||||
|
||||
group.GET("/oidc/users/me/clients", authMiddleware.WithAdminNotRequired().Add(), oc.listOwnAccessibleClientsHandler)
|
||||
type oidcUserAuthorizedClientsInput struct {
|
||||
utils.ListRequestOptions
|
||||
ID string `path:"id"`
|
||||
}
|
||||
|
||||
group.GET("/oidc/clients/:id/scim-service-provider", authMiddleware.Add(), oc.getClientScimServiceProviderHandler)
|
||||
type oidcOwnAuthorizedClientsInput struct {
|
||||
utils.ListRequestOptions
|
||||
}
|
||||
|
||||
type oidcClientAuthorizationInput struct {
|
||||
ClientID string `path:"clientId"`
|
||||
}
|
||||
|
||||
type oidcPreviewInput struct {
|
||||
ID string `path:"id"`
|
||||
UserID string `path:"userId"`
|
||||
Scopes string `query:"scopes" required:"true"`
|
||||
}
|
||||
|
||||
type oidcLogoOutput struct {
|
||||
ContentType string `header:"Content-Type"`
|
||||
ContentLength int64 `header:"Content-Length"`
|
||||
CacheControl string `header:"Cache-Control"`
|
||||
Body func(huma.Context)
|
||||
}
|
||||
|
||||
// NewOidcController registers typed OIDC client management endpoints
|
||||
func NewOidcController(api huma.API, authMiddleware *middleware.AuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, oidcService *service.OidcService) {
|
||||
controller := &OidcController{oidcService: oidcService}
|
||||
adminAuth := authMiddleware.Huma(api)
|
||||
userAuth := authMiddleware.WithAdminNotRequired().Huma(api)
|
||||
|
||||
listClients := oidcOperation("list-oidc-clients", http.MethodGet, "/api/oidc/clients", "List OIDC clients")
|
||||
adminAuth(&listClients)
|
||||
httpapi.Register(api, listClients, controller.listClientsHandler)
|
||||
|
||||
createClient := oidcOperation("create-oidc-client", http.MethodPost, "/api/oidc/clients", "Create OIDC client")
|
||||
createClient.DefaultStatus = http.StatusCreated
|
||||
adminAuth(&createClient)
|
||||
httpapi.Register(api, createClient, controller.createClientHandler)
|
||||
|
||||
getClient := oidcOperation("get-oidc-client", http.MethodGet, "/api/oidc/clients/{id}", "Get OIDC client")
|
||||
adminAuth(&getClient)
|
||||
httpapi.Register(api, getClient, controller.getClientHandler)
|
||||
|
||||
httpapi.Register(api, oidcOperation("get-oidc-client-metadata", http.MethodGet, "/api/oidc/clients/{id}/meta", "Get OIDC client metadata"), controller.getClientMetaDataHandler)
|
||||
|
||||
updateClient := oidcOperation("update-oidc-client", http.MethodPut, "/api/oidc/clients/{id}", "Update OIDC client")
|
||||
adminAuth(&updateClient)
|
||||
httpapi.Register(api, updateClient, controller.updateClientHandler)
|
||||
|
||||
deleteClient := oidcOperation("delete-oidc-client", http.MethodDelete, "/api/oidc/clients/{id}", "Delete OIDC client")
|
||||
deleteClient.DefaultStatus = http.StatusNoContent
|
||||
adminAuth(&deleteClient)
|
||||
httpapi.Register(api, deleteClient, controller.deleteClientHandler)
|
||||
|
||||
allowedGroups := oidcOperation("update-oidc-client-allowed-user-groups", http.MethodPut, "/api/oidc/clients/{id}/allowed-user-groups", "Update allowed user groups")
|
||||
adminAuth(&allowedGroups)
|
||||
httpapi.Register(api, allowedGroups, controller.updateAllowedUserGroupsHandler)
|
||||
|
||||
createSecret := oidcOperation("create-oidc-client-secret", http.MethodPost, "/api/oidc/clients/{id}/secret", "Create client secret")
|
||||
adminAuth(&createSecret)
|
||||
httpapi.Register(api, createSecret, controller.createClientSecretHandler)
|
||||
|
||||
httpapi.Register(api, oidcOperation("get-oidc-client-logo", http.MethodGet, "/api/oidc/clients/{id}/logo", "Get client logo"), controller.getClientLogoHandler)
|
||||
|
||||
deleteLogo := oidcOperation("delete-oidc-client-logo", http.MethodDelete, "/api/oidc/clients/{id}/logo", "Delete client logo")
|
||||
deleteLogo.DefaultStatus = http.StatusNoContent
|
||||
adminAuth(&deleteLogo)
|
||||
httpapi.Register(api, deleteLogo, controller.deleteClientLogoHandler)
|
||||
|
||||
updateLogo := oidcOperation("update-oidc-client-logo", http.MethodPost, "/api/oidc/clients/{id}/logo", "Update client logo")
|
||||
updateLogo.DefaultStatus = http.StatusNoContent
|
||||
adminAuth(&updateLogo)
|
||||
updateLogo.Middlewares = append(updateLogo.Middlewares, fileSizeLimitMiddleware.Huma(api, 2<<20))
|
||||
httpapi.Register(api, updateLogo, controller.updateClientLogoHandler)
|
||||
|
||||
preview := oidcOperation("preview-oidc-client-data", http.MethodGet, "/api/oidc/clients/{id}/preview/{userId}", "Preview OIDC client data for user")
|
||||
adminAuth(&preview)
|
||||
httpapi.Register(api, preview, controller.getClientPreviewHandler)
|
||||
|
||||
ownAuthorized := oidcOperation("list-own-authorized-oidc-clients", http.MethodGet, "/api/oidc/users/me/authorized-clients", "List authorized clients for current user")
|
||||
userAuth(&ownAuthorized)
|
||||
httpapi.Register(api, ownAuthorized, controller.listOwnAuthorizedClientsHandler)
|
||||
|
||||
userAuthorized := oidcOperation("list-user-authorized-oidc-clients", http.MethodGet, "/api/oidc/users/{id}/authorized-clients", "List authorized clients for a user")
|
||||
adminAuth(&userAuthorized)
|
||||
httpapi.Register(api, userAuthorized, controller.listAuthorizedClientsHandler)
|
||||
|
||||
revokeAuthorization := oidcOperation("revoke-own-oidc-client-authorization", http.MethodDelete, "/api/oidc/users/me/authorized-clients/{clientId}", "Revoke authorization for an OIDC client")
|
||||
revokeAuthorization.DefaultStatus = http.StatusNoContent
|
||||
userAuth(&revokeAuthorization)
|
||||
httpapi.Register(api, revokeAuthorization, controller.revokeOwnClientAuthorizationHandler)
|
||||
|
||||
accessibleClients := oidcOperation("list-own-accessible-oidc-clients", http.MethodGet, "/api/oidc/users/me/clients", "List accessible OIDC clients for current user")
|
||||
userAuth(&accessibleClients)
|
||||
httpapi.Register(api, accessibleClients, controller.listOwnAccessibleClientsHandler)
|
||||
|
||||
clientSCIM := oidcOperation("get-oidc-client-scim-service-provider", http.MethodGet, "/api/oidc/clients/{id}/scim-service-provider", "Get SCIM service provider")
|
||||
adminAuth(&clientSCIM)
|
||||
httpapi.Register(api, clientSCIM, controller.getClientScimServiceProviderHandler)
|
||||
}
|
||||
|
||||
func oidcOperation(id, method, path, summary string) huma.Operation {
|
||||
return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"OIDC"}}
|
||||
}
|
||||
|
||||
type OidcController struct {
|
||||
oidcService *service.OidcService
|
||||
}
|
||||
|
||||
// getClientMetaDataHandler godoc
|
||||
// @Summary Get client metadata
|
||||
// @Description Get OIDC client metadata for discovery and configuration
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @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) {
|
||||
clientId := c.Param("id")
|
||||
client, err := oc.oidcService.GetClient(c.Request.Context(), clientId)
|
||||
func (oc *OidcController) getClientMetaDataHandler(ctx context.Context, input *oidcClientIDInput) (*httpapi.BodyOutput[dto.OidcClientMetaDataDto], error) {
|
||||
client, err := oc.oidcService.GetClient(ctx, input.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientDto := dto.OidcClientMetaDataDto{}
|
||||
err = dto.MapStruct(client, &clientDto)
|
||||
if err == nil {
|
||||
clientDto.HasDarkLogo = client.HasDarkLogo()
|
||||
c.JSON(http.StatusOK, clientDto)
|
||||
return
|
||||
var output dto.OidcClientMetaDataDto
|
||||
if err := dto.MapStruct(client, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = c.Error(err)
|
||||
output.HasDarkLogo = client.HasDarkLogo()
|
||||
return &httpapi.BodyOutput[dto.OidcClientMetaDataDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
// getClientHandler godoc
|
||||
// @Summary Get OIDC client
|
||||
// @Description Get detailed information about an OIDC client
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @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) {
|
||||
clientId := c.Param("id")
|
||||
client, err := oc.oidcService.GetClient(c.Request.Context(), clientId)
|
||||
func (oc *OidcController) getClientHandler(ctx context.Context, input *oidcClientIDInput) (*httpapi.BodyOutput[dto.OidcClientWithAllowedUserGroupsDto], error) {
|
||||
client, err := oc.oidcService.GetClient(ctx, input.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientDto := dto.OidcClientWithAllowedUserGroupsDto{}
|
||||
err = dto.MapStruct(client, &clientDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, clientDto)
|
||||
return mapOIDCClient(client)
|
||||
}
|
||||
|
||||
// listClientsHandler godoc
|
||||
// @Summary List OIDC clients
|
||||
// @Description Get a paginated list of OIDC clients with optional search and sorting
|
||||
// @Tags OIDC
|
||||
// @Param search query string false "Search term to filter clients by name"
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @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]
|
||||
// @Router /api/oidc/clients [get]
|
||||
func (oc *OidcController) listClientsHandler(c *gin.Context) {
|
||||
searchTerm := c.Query("search")
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
clients, pagination, err := oc.oidcService.ListClients(c.Request.Context(), searchTerm, listRequestOptions)
|
||||
func (oc *OidcController) listClientsHandler(ctx context.Context, input *oidcClientListInput) (*httpapi.BodyOutput[dto.Paginated[dto.OidcClientWithAllowedGroupsCountDto]], error) {
|
||||
clients, pagination, err := oc.oidcService.ListClients(ctx, input.Search, input.ListRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map the user groups to DTOs
|
||||
var clientsDto = make([]dto.OidcClientWithAllowedGroupsCountDto, len(clients))
|
||||
for i, client := range clients {
|
||||
var clientDto dto.OidcClientWithAllowedGroupsCountDto
|
||||
if err := dto.MapStruct(client, &clientDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
output := make([]dto.OidcClientWithAllowedGroupsCountDto, len(clients))
|
||||
for i := range clients {
|
||||
if err := dto.MapStruct(clients[i], &output[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientDto.HasDarkLogo = client.HasDarkLogo()
|
||||
clientDto.AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(c, client.ID)
|
||||
output[i].HasDarkLogo = clients[i].HasDarkLogo()
|
||||
output[i].AllowedUserGroupsCount, err = oc.oidcService.GetAllowedGroupsCountOfClient(ctx, clients[i].ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
clientsDto[i] = clientDto
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[dto.OidcClientWithAllowedGroupsCountDto]{
|
||||
Data: clientsDto,
|
||||
Pagination: pagination,
|
||||
})
|
||||
return &httpapi.BodyOutput[dto.Paginated[dto.OidcClientWithAllowedGroupsCountDto]]{Body: dto.Paginated[dto.OidcClientWithAllowedGroupsCountDto]{Data: output, Pagination: pagination}}, nil
|
||||
}
|
||||
|
||||
// createClientHandler godoc
|
||||
// @Summary Create OIDC client
|
||||
// @Description Create a new OIDC client
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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) {
|
||||
var input dto.OidcClientCreateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := oc.oidcService.CreateClient(c.Request.Context(), input, c.GetString("userID"))
|
||||
func (oc *OidcController) createClientHandler(ctx context.Context, input *oidcClientCreateInput) (*httpapi.BodyOutput[dto.OidcClientWithAllowedUserGroupsDto], error) {
|
||||
client, err := oc.oidcService.CreateClient(ctx, input.Body, httpapi.UserID(ctx))
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var clientDto dto.OidcClientWithAllowedUserGroupsDto
|
||||
if err := dto.MapStruct(client, &clientDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, clientDto)
|
||||
return mapOIDCClient(client)
|
||||
}
|
||||
|
||||
// deleteClientHandler godoc
|
||||
// @Summary Delete OIDC client
|
||||
// @Description Delete an OIDC client by ID
|
||||
// @Tags OIDC
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/oidc/clients/{id} [delete]
|
||||
func (oc *OidcController) deleteClientHandler(c *gin.Context) {
|
||||
err := oc.oidcService.DeleteClient(c.Request.Context(), c.Param("id"))
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
func (oc *OidcController) deleteClientHandler(ctx context.Context, input *oidcClientIDInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := oc.oidcService.DeleteClient(ctx, input.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
// updateClientHandler godoc
|
||||
// @Summary Update OIDC client
|
||||
// @Description Update an existing OIDC client
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Client ID"
|
||||
// @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) {
|
||||
var input dto.OidcClientUpdateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := oc.oidcService.UpdateClient(c.Request.Context(), c.Param("id"), input)
|
||||
func (oc *OidcController) updateClientHandler(ctx context.Context, input *oidcClientUpdateInput) (*httpapi.BodyOutput[dto.OidcClientWithAllowedUserGroupsDto], error) {
|
||||
client, err := oc.oidcService.UpdateClient(ctx, input.ID, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var clientDto dto.OidcClientWithAllowedUserGroupsDto
|
||||
if err := dto.MapStruct(client, &clientDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, clientDto)
|
||||
return mapOIDCClient(client)
|
||||
}
|
||||
|
||||
// createClientSecretHandler godoc
|
||||
// @Summary Create client secret
|
||||
// @Description Generate a new secret for an OIDC client
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @Param id path string true "Client ID"
|
||||
// @Success 200 {object} object "{ \"secret\": \"string\" }"
|
||||
// @Router /api/oidc/clients/{id}/secret [post]
|
||||
func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
|
||||
secret, err := oc.oidcService.CreateClientSecret(c.Request.Context(), c.Param("id"))
|
||||
func (oc *OidcController) createClientSecretHandler(ctx context.Context, input *oidcClientIDInput) (*httpapi.BodyOutput[map[string]string], error) {
|
||||
secret, err := oc.oidcService.CreateClientSecret(ctx, input.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"secret": secret})
|
||||
return &httpapi.BodyOutput[map[string]string]{Body: map[string]string{"secret": secret}}, nil
|
||||
}
|
||||
|
||||
// getClientLogoHandler godoc
|
||||
// @Summary Get client logo
|
||||
// @Description Get the logo image for an OIDC client
|
||||
// @Tags OIDC
|
||||
// @Produce image/png
|
||||
// @Produce image/jpeg
|
||||
// @Produce image/svg+xml
|
||||
// @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"
|
||||
// @Router /api/oidc/clients/{id}/logo [get]
|
||||
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
|
||||
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||
|
||||
reader, size, mimeType, err := oc.oidcService.GetClientLogo(c.Request.Context(), c.Param("id"), lightLogo)
|
||||
func (oc *OidcController) getClientLogoHandler(ctx context.Context, input *oidcLogoInput) (*oidcLogoOutput, error) {
|
||||
light, _ := strconv.ParseBool(input.Light)
|
||||
reader, size, mimeType, err := oc.oidcService.GetClientLogo(ctx, input.ID, light)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
utils.SetCacheControlHeader(c, 15*time.Minute, 12*time.Hour)
|
||||
|
||||
c.Header("Content-Type", mimeType)
|
||||
c.DataFromReader(http.StatusOK, size, mimeType, reader, nil)
|
||||
cacheControl := ""
|
||||
if !httpapi.QueryPresent(ctx, "skipCache") {
|
||||
cacheControl = utils.CacheControlValue(15*time.Minute, 12*time.Hour)
|
||||
}
|
||||
return &oidcLogoOutput{
|
||||
ContentType: mimeType,
|
||||
ContentLength: size,
|
||||
CacheControl: cacheControl,
|
||||
Body: func(streamCtx huma.Context) {
|
||||
defer reader.Close()
|
||||
_, _ = io.Copy(streamCtx.BodyWriter(), reader)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// updateClientLogoHandler godoc
|
||||
// @Summary Update client logo
|
||||
// @Description Upload or update the logo for an OIDC client
|
||||
// @Tags OIDC
|
||||
// @Accept multipart/form-data
|
||||
// @Param id path string true "Client ID"
|
||||
// @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"
|
||||
// @Router /api/oidc/clients/{id}/logo [post]
|
||||
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
|
||||
file, err := c.FormFile("file")
|
||||
func (oc *OidcController) updateClientLogoHandler(ctx context.Context, input *oidcLogoUploadInput) (*httpapi.EmptyOutput, error) {
|
||||
file, err := uploadFile(input.RawBody.Form)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, 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
|
||||
light, _ := strconv.ParseBool(input.Light)
|
||||
if err := oc.oidcService.UpdateClientLogo(ctx, input.ID, file, light); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
// deleteClientLogoHandler godoc
|
||||
// @Summary Delete client logo
|
||||
// @Description Delete the logo for an OIDC client
|
||||
// @Tags OIDC
|
||||
// @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"
|
||||
// @Router /api/oidc/clients/{id}/logo [delete]
|
||||
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
|
||||
func (oc *OidcController) deleteClientLogoHandler(ctx context.Context, input *oidcLogoInput) (*httpapi.EmptyOutput, error) {
|
||||
light, _ := strconv.ParseBool(input.Light)
|
||||
var err error
|
||||
|
||||
lightLogo, _ := strconv.ParseBool(c.DefaultQuery("light", "true"))
|
||||
if lightLogo {
|
||||
err = oc.oidcService.DeleteClientLogo(c.Request.Context(), c.Param("id"))
|
||||
if light {
|
||||
err = oc.oidcService.DeleteClientLogo(ctx, input.ID)
|
||||
} else {
|
||||
err = oc.oidcService.DeleteClientDarkLogo(c.Request.Context(), c.Param("id"))
|
||||
err = oc.oidcService.DeleteClientDarkLogo(ctx, input.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
// updateAllowedUserGroupsHandler godoc
|
||||
// @Summary Update allowed user groups
|
||||
// @Description Update the user groups allowed to access an OIDC client
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Client ID"
|
||||
// @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) {
|
||||
var input dto.OidcUpdateAllowedUserGroupsDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
oidcClient, err := oc.oidcService.UpdateAllowedUserGroups(c.Request.Context(), c.Param("id"), input)
|
||||
func (oc *OidcController) updateAllowedUserGroupsHandler(ctx context.Context, input *oidcAllowedGroupsInput) (*httpapi.BodyOutput[dto.OidcClientDto], error) {
|
||||
client, err := oc.oidcService.UpdateAllowedUserGroups(ctx, input.ID, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var oidcClientDto dto.OidcClientDto
|
||||
if err := dto.MapStruct(oidcClient, &oidcClientDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
var output dto.OidcClientDto
|
||||
if err := dto.MapStruct(client, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oidcClientDto.HasDarkLogo = oidcClient.HasDarkLogo()
|
||||
|
||||
c.JSON(http.StatusOK, oidcClientDto)
|
||||
output.HasDarkLogo = client.HasDarkLogo()
|
||||
return &httpapi.BodyOutput[dto.OidcClientDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
// listOwnAuthorizedClientsHandler godoc
|
||||
// @Summary List authorized clients for current user
|
||||
// @Description Get a paginated list of OIDC clients that the current user has authorized
|
||||
// @Tags OIDC
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @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.AuthorizedOidcClientDto]
|
||||
// @Router /api/oidc/users/me/authorized-clients [get]
|
||||
func (oc *OidcController) listOwnAuthorizedClientsHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
oc.listAuthorizedClients(c, userID)
|
||||
func (oc *OidcController) listOwnAuthorizedClientsHandler(ctx context.Context, input *oidcOwnAuthorizedClientsInput) (*httpapi.BodyOutput[dto.Paginated[dto.AuthorizedOidcClientDto]], error) {
|
||||
return oc.listAuthorizedClients(ctx, httpapi.UserID(ctx), input.ListRequestOptions)
|
||||
}
|
||||
|
||||
// listAuthorizedClientsHandler godoc
|
||||
// @Summary List authorized clients for a user
|
||||
// @Description Get a paginated list of OIDC clients that a specific user has authorized
|
||||
// @Tags OIDC
|
||||
// @Param id path string true "User ID"
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @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.AuthorizedOidcClientDto]
|
||||
// @Router /api/oidc/users/{id}/authorized-clients [get]
|
||||
func (oc *OidcController) listAuthorizedClientsHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
oc.listAuthorizedClients(c, userID)
|
||||
func (oc *OidcController) listAuthorizedClientsHandler(ctx context.Context, input *oidcUserAuthorizedClientsInput) (*httpapi.BodyOutput[dto.Paginated[dto.AuthorizedOidcClientDto]], error) {
|
||||
return oc.listAuthorizedClients(ctx, input.ID, input.ListRequestOptions)
|
||||
}
|
||||
|
||||
func (oc *OidcController) listAuthorizedClients(c *gin.Context, userID string) {
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
authorizedClients, pagination, err := oc.oidcService.ListAuthorizedClients(c.Request.Context(), userID, listRequestOptions)
|
||||
func (oc *OidcController) listAuthorizedClients(ctx context.Context, userID string, options utils.ListRequestOptions) (*httpapi.BodyOutput[dto.Paginated[dto.AuthorizedOidcClientDto]], error) {
|
||||
clients, pagination, err := oc.oidcService.ListAuthorizedClients(ctx, userID, options)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map the clients to DTOs
|
||||
var authorizedClientsDto []dto.AuthorizedOidcClientDto
|
||||
if err := dto.MapStructList(authorizedClients, &authorizedClientsDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
var output []dto.AuthorizedOidcClientDto
|
||||
if err := dto.MapStructList(clients, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[dto.AuthorizedOidcClientDto]{
|
||||
Data: authorizedClientsDto,
|
||||
Pagination: pagination,
|
||||
})
|
||||
return &httpapi.BodyOutput[dto.Paginated[dto.AuthorizedOidcClientDto]]{Body: dto.Paginated[dto.AuthorizedOidcClientDto]{Data: output, Pagination: pagination}}, nil
|
||||
}
|
||||
|
||||
// revokeOwnClientAuthorizationHandler godoc
|
||||
// @Summary Revoke authorization for an OIDC client
|
||||
// @Description Revoke the authorization for a specific OIDC client for the current user
|
||||
// @Tags OIDC
|
||||
// @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) {
|
||||
clientID := c.Param("clientId")
|
||||
func (oc *OidcController) revokeOwnClientAuthorizationHandler(ctx context.Context, input *oidcClientAuthorizationInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := oc.oidcService.RevokeAuthorizedClient(ctx, httpapi.UserID(ctx), input.ClientID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
userID := c.GetString("userID")
|
||||
|
||||
err := oc.oidcService.RevokeAuthorizedClient(c.Request.Context(), userID, clientID)
|
||||
func (oc *OidcController) listOwnAccessibleClientsHandler(ctx context.Context, input *oidcOwnAuthorizedClientsInput) (*httpapi.BodyOutput[dto.Paginated[dto.AccessibleOidcClientDto]], error) {
|
||||
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(ctx, httpapi.UserID(ctx), input.ListRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.BodyOutput[dto.Paginated[dto.AccessibleOidcClientDto]]{Body: dto.Paginated[dto.AccessibleOidcClientDto]{Data: clients, Pagination: pagination}}, nil
|
||||
}
|
||||
|
||||
// listOwnAccessibleClientsHandler godoc
|
||||
// @Summary List accessible OIDC clients for current user
|
||||
// @Description Get a list of OIDC clients that the current user can access
|
||||
// @Tags OIDC
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @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.AccessibleOidcClientDto]
|
||||
// @Router /api/oidc/users/me/clients [get]
|
||||
func (oc *OidcController) listOwnAccessibleClientsHandler(c *gin.Context) {
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
userID := c.GetString("userID")
|
||||
|
||||
clients, pagination, err := oc.oidcService.ListAccessibleOidcClients(c.Request.Context(), userID, listRequestOptions)
|
||||
func (oc *OidcController) getClientPreviewHandler(ctx context.Context, input *oidcPreviewInput) (*httpapi.BodyOutput[dto.OidcClientPreviewDto], error) {
|
||||
if input.ID == "" {
|
||||
return nil, &common.ValidationError{Message: "client ID is required"}
|
||||
}
|
||||
if input.UserID == "" {
|
||||
return nil, &common.ValidationError{Message: "user ID is required"}
|
||||
}
|
||||
if input.Scopes == "" {
|
||||
return nil, &common.ValidationError{Message: "scopes are required"}
|
||||
}
|
||||
preview, err := oc.oidcService.GetClientPreview(ctx, input.ID, input.UserID, strings.Split(input.Scopes, " "), httpapi.AuthenticationMethod(ctx))
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[dto.AccessibleOidcClientDto]{
|
||||
Data: clients,
|
||||
Pagination: pagination,
|
||||
})
|
||||
return &httpapi.BodyOutput[dto.OidcClientPreviewDto]{Body: *preview}, nil
|
||||
}
|
||||
|
||||
// getClientPreviewHandler godoc
|
||||
// @Summary Preview OIDC client data for user
|
||||
// @Description Get a preview of the OIDC data (ID token, access token, userinfo) that would be sent to the client for a specific user
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @Param id path string true "Client ID"
|
||||
// @Param userId path string true "User ID to preview data for"
|
||||
// @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
|
||||
// @Router /api/oidc/clients/{id}/preview/{userId} [get]
|
||||
func (oc *OidcController) getClientPreviewHandler(c *gin.Context) {
|
||||
clientID := c.Param("id")
|
||||
userID := c.Param("userId")
|
||||
scopes := c.Query("scopes")
|
||||
|
||||
if clientID == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "client ID is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if userID == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "user ID is required"})
|
||||
return
|
||||
}
|
||||
|
||||
if scopes == "" {
|
||||
_ = c.Error(&common.ValidationError{Message: "scopes are required"})
|
||||
return
|
||||
}
|
||||
|
||||
preview, err := oc.oidcService.GetClientPreview(
|
||||
c.Request.Context(),
|
||||
clientID,
|
||||
userID,
|
||||
strings.Split(scopes, " "),
|
||||
c.GetString("authenticationMethod"))
|
||||
|
||||
func (oc *OidcController) getClientScimServiceProviderHandler(ctx context.Context, input *oidcClientIDInput) (*httpapi.BodyOutput[dto.ScimServiceProviderDTO], error) {
|
||||
provider, err := oc.oidcService.GetClientScimServiceProvider(ctx, input.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, preview)
|
||||
var output dto.ScimServiceProviderDTO
|
||||
if err := dto.MapStruct(provider, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.BodyOutput[dto.ScimServiceProviderDTO]{Body: output}, nil
|
||||
}
|
||||
|
||||
// getClientScimServiceProviderHandler godoc
|
||||
// @Summary Get SCIM service provider
|
||||
// @Description Get the SCIM service provider configuration for an OIDC client
|
||||
// @Tags OIDC
|
||||
// @Produce json
|
||||
// @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) {
|
||||
clientID := c.Param("id")
|
||||
|
||||
provider, err := oc.oidcService.GetClientScimServiceProvider(c.Request.Context(), clientID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
func mapOIDCClient(client model.OidcClient) (*httpapi.BodyOutput[dto.OidcClientWithAllowedUserGroupsDto], error) {
|
||||
var output dto.OidcClientWithAllowedUserGroupsDto
|
||||
if err := dto.MapStruct(client, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var providerDto dto.ScimServiceProviderDTO
|
||||
if err := dto.MapStruct(provider, &providerDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, providerDto)
|
||||
return &httpapi.BodyOutput[dto.OidcClientWithAllowedUserGroupsDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
@@ -1,122 +1,96 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
func NewScimController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, scimService *service.ScimService) {
|
||||
ugc := ScimController{
|
||||
scimService: scimService,
|
||||
}
|
||||
type scimIDInput struct {
|
||||
ID string `path:"id"`
|
||||
}
|
||||
|
||||
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)
|
||||
type scimCreateInput struct {
|
||||
Body dto.ScimServiceProviderCreateDTO
|
||||
}
|
||||
|
||||
type scimUpdateInput struct {
|
||||
ID string `path:"id"`
|
||||
Body dto.ScimServiceProviderCreateDTO
|
||||
}
|
||||
|
||||
func NewScimController(api huma.API, authMiddleware *middleware.AuthMiddleware, scimService *service.ScimService) {
|
||||
controller := &ScimController{scimService: scimService}
|
||||
auth := authMiddleware.Huma(api)
|
||||
|
||||
createOperation := scimOperation("create-scim-service-provider", http.MethodPost, "/api/scim/service-provider", "Create SCIM service provider")
|
||||
createOperation.DefaultStatus = http.StatusCreated
|
||||
auth(&createOperation)
|
||||
httpapi.Register(api, createOperation, controller.createServiceProviderHandler)
|
||||
|
||||
syncOperation := scimOperation("sync-scim-service-provider", http.MethodPost, "/api/scim/service-provider/{id}/sync", "Sync SCIM service provider")
|
||||
syncOperation.DefaultStatus = http.StatusOK
|
||||
auth(&syncOperation)
|
||||
httpapi.Register(api, syncOperation, controller.syncServiceProviderHandler)
|
||||
|
||||
updateOperation := scimOperation("update-scim-service-provider", http.MethodPut, "/api/scim/service-provider/{id}", "Update SCIM service provider")
|
||||
auth(&updateOperation)
|
||||
httpapi.Register(api, updateOperation, controller.updateServiceProviderHandler)
|
||||
|
||||
deleteOperation := scimOperation("delete-scim-service-provider", http.MethodDelete, "/api/scim/service-provider/{id}", "Delete SCIM service provider")
|
||||
deleteOperation.DefaultStatus = http.StatusNoContent
|
||||
auth(&deleteOperation)
|
||||
httpapi.Register(api, deleteOperation, controller.deleteServiceProviderHandler)
|
||||
}
|
||||
|
||||
func scimOperation(id, method, path, summary string) huma.Operation {
|
||||
return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"SCIM"}}
|
||||
}
|
||||
|
||||
type ScimController struct {
|
||||
scimService *service.ScimService
|
||||
}
|
||||
|
||||
// syncServiceProviderHandler godoc
|
||||
// @Summary Sync SCIM service provider
|
||||
// @Description Trigger synchronization for a SCIM service provider
|
||||
// @Tags SCIM
|
||||
// @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) {
|
||||
err := c.scimService.SyncServiceProvider(ctx.Request.Context(), ctx.Param("id"))
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
func (c *ScimController) syncServiceProviderHandler(ctx context.Context, input *scimIDInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := c.scimService.SyncServiceProvider(ctx, input.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusOK)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
// createServiceProviderHandler godoc
|
||||
// @Summary Create SCIM service provider
|
||||
// @Description Create a new SCIM service provider
|
||||
// @Tags SCIM
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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) {
|
||||
var input dto.ScimServiceProviderCreateDTO
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := c.scimService.CreateServiceProvider(ctx.Request.Context(), &input)
|
||||
func (c *ScimController) createServiceProviderHandler(ctx context.Context, input *scimCreateInput) (*httpapi.BodyOutput[dto.ScimServiceProviderDTO], error) {
|
||||
provider, err := c.scimService.CreateServiceProvider(ctx, &input.Body)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var providerDTO dto.ScimServiceProviderDTO
|
||||
if err := dto.MapStruct(provider, &providerDTO); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, providerDTO)
|
||||
return mapSCIMProvider(provider)
|
||||
}
|
||||
|
||||
// updateServiceProviderHandler godoc
|
||||
// @Summary Update SCIM service provider
|
||||
// @Description Update an existing SCIM service provider
|
||||
// @Tags SCIM
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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"
|
||||
// @Router /api/scim/service-provider/{id} [put]
|
||||
func (c *ScimController) updateServiceProviderHandler(ctx *gin.Context) {
|
||||
var input dto.ScimServiceProviderCreateDTO
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := c.scimService.UpdateServiceProvider(ctx.Request.Context(), ctx.Param("id"), &input)
|
||||
func (c *ScimController) updateServiceProviderHandler(ctx context.Context, input *scimUpdateInput) (*httpapi.BodyOutput[dto.ScimServiceProviderDTO], error) {
|
||||
provider, err := c.scimService.UpdateServiceProvider(ctx, input.ID, &input.Body)
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var providerDTO dto.ScimServiceProviderDTO
|
||||
if err := dto.MapStruct(provider, &providerDTO); err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, providerDTO)
|
||||
return mapSCIMProvider(provider)
|
||||
}
|
||||
|
||||
// deleteServiceProviderHandler godoc
|
||||
// @Summary Delete SCIM service provider
|
||||
// @Description Delete a SCIM service provider by ID
|
||||
// @Tags SCIM
|
||||
// @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) {
|
||||
err := c.scimService.DeleteServiceProvider(ctx.Request.Context(), ctx.Param("id"))
|
||||
if err != nil {
|
||||
_ = ctx.Error(err)
|
||||
return
|
||||
func (c *ScimController) deleteServiceProviderHandler(ctx context.Context, input *scimIDInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := c.scimService.DeleteServiceProvider(ctx, input.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
func mapSCIMProvider(provider any) (*httpapi.BodyOutput[dto.ScimServiceProviderDTO], error) {
|
||||
var output dto.ScimServiceProviderDTO
|
||||
if err := dto.MapStruct(provider, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpapi.BodyOutput[dto.ScimServiceProviderDTO]{Body: output}, nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,250 +1,161 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/middleware"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
// NewUserGroupController creates a new controller for user group management
|
||||
// @Summary User group management controller
|
||||
// @Description Initializes all user group-related API endpoints
|
||||
// @Tags User Groups
|
||||
func NewUserGroupController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, userGroupService *service.UserGroupService) {
|
||||
ugc := UserGroupController{
|
||||
UserGroupService: userGroupService,
|
||||
}
|
||||
type userGroupListInput struct {
|
||||
utils.ListRequestOptions
|
||||
Search string `query:"search" required:"false"`
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
type userGroupIDInput struct {
|
||||
ID string `path:"id"`
|
||||
}
|
||||
|
||||
type userGroupCreateInput struct {
|
||||
Body dto.UserGroupCreateDto
|
||||
}
|
||||
|
||||
type userGroupUpdateInput struct {
|
||||
ID string `path:"id"`
|
||||
Body dto.UserGroupCreateDto
|
||||
}
|
||||
|
||||
type userGroupUsersInput struct {
|
||||
ID string `path:"id"`
|
||||
Body dto.UserGroupUpdateUsersDto
|
||||
}
|
||||
|
||||
type userGroupClientsInput struct {
|
||||
ID string `path:"id"`
|
||||
Body dto.UserGroupUpdateAllowedOidcClientsDto
|
||||
}
|
||||
|
||||
// NewUserGroupController registers user group management routes
|
||||
func NewUserGroupController(api huma.API, authMiddleware *middleware.AuthMiddleware, userGroupService *service.UserGroupService) {
|
||||
controller := &UserGroupController{UserGroupService: userGroupService}
|
||||
auth := authMiddleware.Huma(api)
|
||||
|
||||
listOperation := userGroupOperation("list-user-groups", http.MethodGet, "/api/user-groups", "List user groups")
|
||||
auth(&listOperation)
|
||||
httpapi.Register(api, listOperation, controller.list)
|
||||
|
||||
getOperation := userGroupOperation("get-user-group", http.MethodGet, "/api/user-groups/{id}", "Get user group by ID")
|
||||
auth(&getOperation)
|
||||
httpapi.Register(api, getOperation, controller.get)
|
||||
|
||||
createOperation := userGroupOperation("create-user-group", http.MethodPost, "/api/user-groups", "Create user group")
|
||||
createOperation.DefaultStatus = http.StatusCreated
|
||||
auth(&createOperation)
|
||||
httpapi.Register(api, createOperation, controller.create)
|
||||
|
||||
updateOperation := userGroupOperation("update-user-group", http.MethodPut, "/api/user-groups/{id}", "Update user group")
|
||||
auth(&updateOperation)
|
||||
httpapi.Register(api, updateOperation, controller.update)
|
||||
|
||||
deleteOperation := userGroupOperation("delete-user-group", http.MethodDelete, "/api/user-groups/{id}", "Delete user group")
|
||||
deleteOperation.DefaultStatus = http.StatusNoContent
|
||||
auth(&deleteOperation)
|
||||
httpapi.Register(api, deleteOperation, controller.delete)
|
||||
|
||||
usersOperation := userGroupOperation("update-user-group-users", http.MethodPut, "/api/user-groups/{id}/users", "Update users in a group")
|
||||
auth(&usersOperation)
|
||||
httpapi.Register(api, usersOperation, controller.updateUsers)
|
||||
|
||||
clientsOperation := userGroupOperation("update-user-group-allowed-oidc-clients", http.MethodPut, "/api/user-groups/{id}/allowed-oidc-clients", "Update allowed OIDC clients")
|
||||
auth(&clientsOperation)
|
||||
httpapi.Register(api, clientsOperation, controller.updateAllowedOIDCClients)
|
||||
}
|
||||
|
||||
func userGroupOperation(id, method, path, summary string) huma.Operation {
|
||||
return huma.Operation{OperationID: id, Method: method, Path: path, Summary: summary, Tags: []string{"User Groups"}}
|
||||
}
|
||||
|
||||
type UserGroupController struct {
|
||||
UserGroupService *service.UserGroupService
|
||||
}
|
||||
|
||||
// list godoc
|
||||
// @Summary List user groups
|
||||
// @Description Get a paginated list of user groups with optional search and sorting
|
||||
// @Tags User Groups
|
||||
// @Param search query string false "Search term to filter user groups by name"
|
||||
// @Param pagination[page] query int false "Page number for pagination" default(1)
|
||||
// @Param pagination[limit] query int false "Number of items per page" default(20)
|
||||
// @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]
|
||||
// @Router /api/user-groups [get]
|
||||
func (ugc *UserGroupController) list(c *gin.Context) {
|
||||
searchTerm := c.Query("search")
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
groups, pagination, err := ugc.UserGroupService.List(c, searchTerm, listRequestOptions)
|
||||
func (ugc *UserGroupController) list(ctx context.Context, input *userGroupListInput) (*httpapi.BodyOutput[dto.Paginated[dto.UserGroupMinimalDto]], error) {
|
||||
groups, pagination, err := ugc.UserGroupService.List(ctx, input.Search, input.ListRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Map the user groups to DTOs
|
||||
var groupsDto = make([]dto.UserGroupMinimalDto, len(groups))
|
||||
groupsDTO := make([]dto.UserGroupMinimalDto, len(groups))
|
||||
for i, group := range groups {
|
||||
var groupDto dto.UserGroupMinimalDto
|
||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
if err := dto.MapStruct(group, &groupsDTO[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupDto.UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(c.Request.Context(), group.ID)
|
||||
groupsDTO[i].UserCount, err = ugc.UserGroupService.GetUserCountOfGroup(ctx, group.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
groupsDto[i] = groupDto
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[dto.UserGroupMinimalDto]{
|
||||
Data: groupsDto,
|
||||
Pagination: pagination,
|
||||
})
|
||||
return &httpapi.BodyOutput[dto.Paginated[dto.UserGroupMinimalDto]]{Body: dto.Paginated[dto.UserGroupMinimalDto]{Data: groupsDTO, Pagination: pagination}}, nil
|
||||
}
|
||||
|
||||
// get godoc
|
||||
// @Summary Get user group by ID
|
||||
// @Description Retrieve detailed information about a specific user group including its users
|
||||
// @Tags User Groups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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) {
|
||||
group, err := ugc.UserGroupService.Get(c.Request.Context(), c.Param("id"))
|
||||
func (ugc *UserGroupController) get(ctx context.Context, input *userGroupIDInput) (*httpapi.BodyOutput[dto.UserGroupDto], error) {
|
||||
group, err := ugc.UserGroupService.Get(ctx, input.ID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groupDto dto.UserGroupDto
|
||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, groupDto)
|
||||
return mapUserGroup(group)
|
||||
}
|
||||
|
||||
// create godoc
|
||||
// @Summary Create user group
|
||||
// @Description Create a new user group
|
||||
// @Tags User Groups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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) {
|
||||
var input dto.UserGroupCreateDto
|
||||
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := ugc.UserGroupService.Create(c.Request.Context(), input)
|
||||
func (ugc *UserGroupController) create(ctx context.Context, input *userGroupCreateInput) (*httpapi.BodyOutput[dto.UserGroupDto], error) {
|
||||
group, err := ugc.UserGroupService.Create(ctx, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groupDto dto.UserGroupDto
|
||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, groupDto)
|
||||
return mapUserGroup(group)
|
||||
}
|
||||
|
||||
// update godoc
|
||||
// @Summary Update user group
|
||||
// @Description Update an existing user group by ID
|
||||
// @Tags User Groups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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"
|
||||
// @Router /api/user-groups/{id} [put]
|
||||
func (ugc *UserGroupController) update(c *gin.Context) {
|
||||
var input dto.UserGroupCreateDto
|
||||
if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := ugc.UserGroupService.Update(c.Request.Context(), c.Param("id"), input)
|
||||
func (ugc *UserGroupController) update(ctx context.Context, input *userGroupUpdateInput) (*httpapi.BodyOutput[dto.UserGroupDto], error) {
|
||||
group, err := ugc.UserGroupService.Update(ctx, input.ID, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groupDto dto.UserGroupDto
|
||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, groupDto)
|
||||
return mapUserGroup(group)
|
||||
}
|
||||
|
||||
// delete godoc
|
||||
// @Summary Delete user group
|
||||
// @Description Delete a specific user group by ID
|
||||
// @Tags User Groups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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) {
|
||||
if err := ugc.UserGroupService.Delete(c.Request.Context(), c.Param("id")); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
func (ugc *UserGroupController) delete(ctx context.Context, input *userGroupIDInput) (*httpapi.EmptyOutput, error) {
|
||||
if err := ugc.UserGroupService.Delete(ctx, input.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return &httpapi.EmptyOutput{}, nil
|
||||
}
|
||||
|
||||
// updateUsers godoc
|
||||
// @Summary Update users in a group
|
||||
// @Description Update the list of users belonging to a specific user group
|
||||
// @Tags User Groups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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
|
||||
// @Router /api/user-groups/{id}/users [put]
|
||||
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
|
||||
var input dto.UserGroupUpdateUsersDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
group, err := ugc.UserGroupService.UpdateUsers(c.Request.Context(), c.Param("id"), input.UserIDs)
|
||||
func (ugc *UserGroupController) updateUsers(ctx context.Context, input *userGroupUsersInput) (*httpapi.BodyOutput[dto.UserGroupDto], error) {
|
||||
group, err := ugc.UserGroupService.UpdateUsers(ctx, input.ID, input.Body.UserIDs)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groupDto dto.UserGroupDto
|
||||
if err := dto.MapStruct(group, &groupDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, groupDto)
|
||||
return mapUserGroup(group)
|
||||
}
|
||||
|
||||
// updateAllowedOidcClients godoc
|
||||
// @Summary Update allowed OIDC clients
|
||||
// @Description Update the OIDC clients allowed for a specific user group
|
||||
// @Tags OIDC
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @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"
|
||||
// @Router /api/user-groups/{id}/allowed-oidc-clients [put]
|
||||
func (ugc *UserGroupController) updateAllowedOidcClients(c *gin.Context) {
|
||||
var input dto.UserGroupUpdateAllowedOidcClientsDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
userGroup, err := ugc.UserGroupService.UpdateAllowedOidcClient(c.Request.Context(), c.Param("id"), input)
|
||||
func (ugc *UserGroupController) updateAllowedOIDCClients(ctx context.Context, input *userGroupClientsInput) (*httpapi.BodyOutput[dto.UserGroupDto], error) {
|
||||
group, err := ugc.UserGroupService.UpdateAllowedOidcClient(ctx, input.ID, input.Body)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
return mapUserGroup(group)
|
||||
}
|
||||
|
||||
var userGroupDto dto.UserGroupDto
|
||||
if err := dto.MapStruct(userGroup, &userGroupDto); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
func mapUserGroup(group any) (*httpapi.BodyOutput[dto.UserGroupDto], error) {
|
||||
var output dto.UserGroupDto
|
||||
if err := dto.MapStruct(group, &output); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, userGroupDto)
|
||||
return &httpapi.BodyOutput[dto.UserGroupDto]{Body: output}, nil
|
||||
}
|
||||
|
||||
@@ -1,56 +1,64 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"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"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
// NewVersionController registers version-related routes.
|
||||
func NewVersionController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, versionService *service.VersionService) {
|
||||
type versionOutput struct {
|
||||
CacheControl string `header:"Cache-Control"`
|
||||
Body map[string]string
|
||||
}
|
||||
|
||||
// NewVersionController registers version-related routes
|
||||
func NewVersionController(api huma.API, 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)
|
||||
|
||||
httpapi.Register(api, huma.Operation{
|
||||
OperationID: "get-latest-version",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/version/latest",
|
||||
Summary: "Get latest available version of Pocket ID",
|
||||
Tags: []string{"Version"},
|
||||
}, vc.getLatestVersionHandler)
|
||||
|
||||
currentOperation := huma.Operation{
|
||||
OperationID: "get-current-version",
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/version/current",
|
||||
Summary: "Get current deployed version of Pocket ID",
|
||||
Tags: []string{"Version"},
|
||||
}
|
||||
authMiddleware.WithAdminNotRequired().Huma(api)(¤tOperation)
|
||||
httpapi.Register(api, currentOperation, vc.getCurrentVersionHandler)
|
||||
}
|
||||
|
||||
type VersionController struct {
|
||||
versionService *service.VersionService
|
||||
}
|
||||
|
||||
// getLatestVersionHandler godoc
|
||||
// @Summary Get latest available version of Pocket ID
|
||||
// @Tags Version
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string "Latest version information"
|
||||
// @Router /api/version/latest [get]
|
||||
func (vc *VersionController) getLatestVersionHandler(c *gin.Context) {
|
||||
tag, err := vc.versionService.GetLatestVersion(c.Request.Context())
|
||||
func (vc *VersionController) getLatestVersionHandler(ctx context.Context, _ *httpapi.EmptyInput) (*versionOutput, error) {
|
||||
tag, err := vc.versionService.GetLatestVersion(ctx)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
utils.SetCacheControlHeader(c, 5*time.Minute, 15*time.Minute)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"latestVersion": tag,
|
||||
})
|
||||
cacheControl := ""
|
||||
if !httpapi.QueryPresent(ctx, "skipCache") {
|
||||
cacheControl = utils.CacheControlValue(5*time.Minute, 15*time.Minute)
|
||||
}
|
||||
return &versionOutput{CacheControl: cacheControl, Body: map[string]string{"latestVersion": tag}}, nil
|
||||
}
|
||||
|
||||
// getCurrentVersionHandler godoc
|
||||
// @Summary Get current deployed version of Pocket ID
|
||||
// @Tags Version
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string "Current version information"
|
||||
// @Router /api/version/current [get]
|
||||
func (vc *VersionController) getCurrentVersionHandler(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"currentVersion": common.Version,
|
||||
})
|
||||
func (vc *VersionController) getCurrentVersionHandler(_ context.Context, _ *httpapi.EmptyInput) (*httpapi.BodyOutput[map[string]string], error) {
|
||||
return &httpapi.BodyOutput[map[string]string]{Body: map[string]string{"currentVersion": common.Version}}, nil
|
||||
}
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/service"
|
||||
httpapi "github.com/pocket-id/pocket-id/backend/internal/utils/huma"
|
||||
)
|
||||
|
||||
// NewWellKnownController creates a new controller for OIDC discovery endpoints
|
||||
// @Summary OIDC Discovery controller
|
||||
// @Description Initializes OIDC discovery and JWKS endpoints
|
||||
// @Tags Well Known
|
||||
func NewWellKnownController(group *gin.RouterGroup, jwtService *service.JwtService) {
|
||||
wkc := &WellKnownController{jwtService: jwtService}
|
||||
type wellKnownOutput struct {
|
||||
ContentType string `header:"Content-Type"`
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// NewWellKnownController registers OIDC discovery endpoints
|
||||
func NewWellKnownController(api huma.API, jwtService *service.JwtService) {
|
||||
controller := &WellKnownController{jwtService: jwtService}
|
||||
|
||||
// Pre-compute the OIDC configuration document, which is static
|
||||
var err error
|
||||
wkc.oidcConfig, err = wkc.computeOIDCConfiguration()
|
||||
controller.oidcConfig, err = controller.computeOIDCConfiguration()
|
||||
if err != nil {
|
||||
slog.Error("Failed to pre-compute OpenID Connect configuration document", slog.Any("error", err))
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
group.GET("/.well-known/jwks.json", wkc.jwksHandler)
|
||||
group.GET("/.well-known/openid-configuration", wkc.openIDConfigurationHandler)
|
||||
httpapi.Register(api, huma.Operation{OperationID: "get-jwks", Method: http.MethodGet, Path: "/.well-known/jwks.json", Summary: "Get JSON Web Key Set", Tags: []string{"Well Known"}}, controller.jwksHandler)
|
||||
httpapi.Register(api, huma.Operation{OperationID: "get-openid-configuration", Method: http.MethodGet, Path: "/.well-known/openid-configuration", Summary: "Get OpenID Connect discovery configuration", Tags: []string{"Well Known"}}, controller.openIDConfigurationHandler)
|
||||
}
|
||||
|
||||
type WellKnownController struct {
|
||||
@@ -38,51 +41,35 @@ type WellKnownController struct {
|
||||
oidcConfig []byte
|
||||
}
|
||||
|
||||
// jwksHandler godoc
|
||||
// @Summary Get JSON Web Key Set (JWKS)
|
||||
// @Description Returns the JSON Web Key Set used for token verification
|
||||
// @Tags Well Known
|
||||
// @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(_ context.Context, _ *httpapi.EmptyInput) (*wellKnownOutput, error) {
|
||||
jwks, err := wkc.jwtService.GetPublicJWKSAsJSON()
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "application/json; charset=utf-8", jwks)
|
||||
return &wellKnownOutput{ContentType: "application/json; charset=utf-8", Body: jwks}, nil
|
||||
}
|
||||
|
||||
// openIDConfigurationHandler godoc
|
||||
// @Summary Get OpenID Connect discovery configuration
|
||||
// @Description Returns the OpenID Connect discovery document with endpoints and capabilities
|
||||
// @Tags Well Known
|
||||
// @Success 200 {object} object "OpenID Connect configuration"
|
||||
// @Router /.well-known/openid-configuration [get]
|
||||
func (wkc *WellKnownController) openIDConfigurationHandler(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "application/json; charset=utf-8", wkc.oidcConfig)
|
||||
func (wkc *WellKnownController) openIDConfigurationHandler(_ context.Context, _ *httpapi.EmptyInput) (*wellKnownOutput, error) {
|
||||
return &wellKnownOutput{ContentType: "application/json; charset=utf-8", Body: wkc.oidcConfig}, nil
|
||||
}
|
||||
|
||||
func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {
|
||||
appUrl := common.EnvConfig.AppURL
|
||||
|
||||
internalAppUrl := common.EnvConfig.InternalAppURL
|
||||
appURL := common.EnvConfig.AppURL
|
||||
internalAppURL := common.EnvConfig.InternalAppURL
|
||||
|
||||
alg, err := wkc.jwtService.GetKeyAlg()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key algorithm: %w", err)
|
||||
}
|
||||
config := map[string]any{
|
||||
"issuer": appUrl,
|
||||
"authorization_endpoint": appUrl + "/authorize",
|
||||
"token_endpoint": internalAppUrl + "/api/oidc/token",
|
||||
"userinfo_endpoint": internalAppUrl + "/api/oidc/userinfo",
|
||||
"end_session_endpoint": appUrl + "/api/oidc/end-session",
|
||||
"introspection_endpoint": internalAppUrl + "/api/oidc/introspect",
|
||||
"device_authorization_endpoint": appUrl + "/api/oidc/device/authorize",
|
||||
"jwks_uri": internalAppUrl + "/.well-known/jwks.json",
|
||||
"issuer": appURL,
|
||||
"authorization_endpoint": appURL + "/authorize",
|
||||
"token_endpoint": internalAppURL + "/api/oidc/token",
|
||||
"userinfo_endpoint": internalAppURL + "/api/oidc/userinfo",
|
||||
"end_session_endpoint": appURL + "/api/oidc/end-session",
|
||||
"introspection_endpoint": internalAppURL + "/api/oidc/introspect",
|
||||
"device_authorization_endpoint": appURL + "/api/oidc/device/authorize",
|
||||
"jwks_uri": internalAppURL + "/.well-known/jwks.json",
|
||||
"grant_types_supported": []string{service.GrantTypeAuthorizationCode, service.GrantTypeRefreshToken, service.GrantTypeDeviceCode, service.GrantTypeClientCredentials},
|
||||
"scopes_supported": []string{"openid", "profile", "email", "groups", "offline_access"},
|
||||
"claims_supported": []string{"sub", "given_name", "family_name", "name", "display_name", "email", "email_verified", "preferred_username", "picture", "groups", "auth_time", "amr"},
|
||||
@@ -96,7 +83,7 @@ func (wkc *WellKnownController) computeOIDCConfiguration() ([]byte, error) {
|
||||
"request_object_signing_alg_values_supported": []string{"none"},
|
||||
"prompt_values_supported": []string{"none", "login", "consent", "select_account"},
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_basic", "client_secret_post", "none"},
|
||||
"pushed_authorization_request_endpoint": internalAppUrl + "/api/oidc/par",
|
||||
"pushed_authorization_request_endpoint": internalAppURL + "/api/oidc/par",
|
||||
"require_pushed_authorization_requests": false,
|
||||
}
|
||||
return json.Marshal(config)
|
||||
|
||||
Reference in New Issue
Block a user