feat: built in api documentation via huma gin adapter

This commit is contained in:
Kyle Mendell
2026-07-10 16:31:45 -05:00
parent 20254eea7c
commit f8b441fb06
68 changed files with 3231 additions and 3106 deletions
+260 -430
View File
@@ -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
}