feat: api key authentication (#291)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Kyle Mendell
2025-03-11 14:16:42 -05:00
committed by GitHub
parent 74ba8390f4
commit 62915d863a
58 changed files with 2172 additions and 190 deletions

View File

@@ -0,0 +1,125 @@
package controller
import (
"net/http"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/gin-gonic/gin"
"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"
)
// swag init -g cmd/main.go -o ./docs/swagger --parseDependency
// ApiKeyController manages API keys for authenticated users
type ApiKeyController struct {
apiKeyService *service.ApiKeyService
}
// NewApiKeyController creates a new controller for API key management
// @Summary API key management controller
// @Description Initializes API endpoints for managing API keys
// @Tags API Keys
func NewApiKeyController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, apiKeyService *service.ApiKeyService) {
uc := &ApiKeyController{apiKeyService: apiKeyService}
apiKeyGroup := group.Group("/api-keys")
apiKeyGroup.Use(authMiddleware.WithAdminNotRequired().Add())
{
apiKeyGroup.GET("", uc.listApiKeysHandler)
apiKeyGroup.POST("", uc.createApiKeyHandler)
apiKeyGroup.DELETE("/:id", uc.revokeApiKeyHandler)
}
}
// listApiKeysHandler godoc
// @Summary List API keys
// @Description Get a paginated list of API keys belonging to the current user
// @Tags API Keys
// @Param page query int false "Page number, starting from 1" default(1)
// @Param limit query int false "Number of items per page" default(10)
// @Param sort_column query string false "Column to sort by" default("created_at")
// @Param sort_direction query string false "Sort direction (asc or desc)" default("desc")
// @Success 200 {object} dto.Paginated[dto.ApiKeyDto]
// @Router /api-keys [get]
func (c *ApiKeyController) listApiKeysHandler(ctx *gin.Context) {
userID := ctx.GetString("userID")
var sortedPaginationRequest utils.SortedPaginationRequest
if err := ctx.ShouldBindQuery(&sortedPaginationRequest); err != nil {
ctx.Error(err)
return
}
apiKeys, pagination, err := c.apiKeyService.ListApiKeys(userID, sortedPaginationRequest)
if err != nil {
ctx.Error(err)
return
}
var apiKeysDto []dto.ApiKeyDto
if err := dto.MapStructList(apiKeys, &apiKeysDto); err != nil {
ctx.Error(err)
return
}
ctx.JSON(http.StatusOK, dto.Paginated[dto.ApiKeyDto]{
Data: apiKeysDto,
Pagination: pagination,
})
}
// createApiKeyHandler godoc
// @Summary Create API key
// @Description Create a new API key for the current user
// @Tags API Keys
// @Param api_key body dto.ApiKeyCreateDto true "API key information"
// @Success 201 {object} dto.ApiKeyResponseDto "Created API key with token"
// @Router /api-keys [post]
func (c *ApiKeyController) createApiKeyHandler(ctx *gin.Context) {
userID := ctx.GetString("userID")
var input dto.ApiKeyCreateDto
if err := ctx.ShouldBindJSON(&input); err != nil {
ctx.Error(err)
return
}
apiKey, token, err := c.apiKeyService.CreateApiKey(userID, input)
if err != nil {
ctx.Error(err)
return
}
var apiKeyDto dto.ApiKeyDto
if err := dto.MapStruct(apiKey, &apiKeyDto); err != nil {
ctx.Error(err)
return
}
ctx.JSON(http.StatusCreated, dto.ApiKeyResponseDto{
ApiKey: apiKeyDto,
Token: token,
})
}
// revokeApiKeyHandler godoc
// @Summary Revoke API key
// @Description Revoke (delete) an existing API key by ID
// @Tags API Keys
// @Param id path string true "API Key ID"
// @Success 204 "No Content"
// @Router /api-keys/{id} [delete]
func (c *ApiKeyController) revokeApiKeyHandler(ctx *gin.Context) {
userID := ctx.GetString("userID")
apiKeyID := ctx.Param("id")
if err := c.apiKeyService.RevokeApiKey(userID, apiKeyID); err != nil {
ctx.Error(err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@@ -12,9 +12,13 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/utils"
)
// 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
func NewAppConfigController(
group *gin.RouterGroup,
jwtAuthMiddleware *middleware.JwtAuthMiddleware,
authMiddleware *middleware.AuthMiddleware,
appConfigService *service.AppConfigService,
emailService *service.EmailService,
ldapService *service.LdapService,
@@ -26,18 +30,18 @@ func NewAppConfigController(
ldapService: ldapService,
}
group.GET("/application-configuration", acc.listAppConfigHandler)
group.GET("/application-configuration/all", jwtAuthMiddleware.Add(true), acc.listAllAppConfigHandler)
group.PUT("/application-configuration", jwtAuthMiddleware.Add(true), acc.updateAppConfigHandler)
group.GET("/application-configuration/all", authMiddleware.Add(), acc.listAllAppConfigHandler)
group.PUT("/application-configuration", authMiddleware.Add(), acc.updateAppConfigHandler)
group.GET("/application-configuration/logo", acc.getLogoHandler)
group.GET("/application-configuration/background-image", acc.getBackgroundImageHandler)
group.GET("/application-configuration/favicon", acc.getFaviconHandler)
group.PUT("/application-configuration/logo", jwtAuthMiddleware.Add(true), acc.updateLogoHandler)
group.PUT("/application-configuration/favicon", jwtAuthMiddleware.Add(true), acc.updateFaviconHandler)
group.PUT("/application-configuration/background-image", jwtAuthMiddleware.Add(true), acc.updateBackgroundImageHandler)
group.PUT("/application-configuration/logo", authMiddleware.Add(), acc.updateLogoHandler)
group.PUT("/application-configuration/favicon", authMiddleware.Add(), acc.updateFaviconHandler)
group.PUT("/application-configuration/background-image", authMiddleware.Add(), acc.updateBackgroundImageHandler)
group.POST("/application-configuration/test-email", jwtAuthMiddleware.Add(true), acc.testEmailHandler)
group.POST("/application-configuration/sync-ldap", jwtAuthMiddleware.Add(true), acc.syncLdapHandler)
group.POST("/application-configuration/test-email", authMiddleware.Add(), acc.testEmailHandler)
group.POST("/application-configuration/sync-ldap", authMiddleware.Add(), acc.syncLdapHandler)
}
type AppConfigController struct {
@@ -46,6 +50,15 @@ 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
// @Failure 500 {object} object "{"error": "error message"}"
// @Router /application-configuration [get]
func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
configuration, err := acc.appConfigService.ListAppConfig(false)
if err != nil {
@@ -62,6 +75,15 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
c.JSON(200, configVariablesDto)
}
// 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
// @Security BearerAuth
// @Router /application-configuration/all [get]
func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
configuration, err := acc.appConfigService.ListAppConfig(true)
if err != nil {
@@ -78,6 +100,16 @@ func (acc *AppConfigController) listAllAppConfigHandler(c *gin.Context) {
c.JSON(200, configVariablesDto)
}
// 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
// @Security BearerAuth
// @Router /application-configuration [put]
func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
var input dto.AppConfigUpdateDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -100,6 +132,16 @@ func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
c.JSON(http.StatusOK, configVariablesDto)
}
// getLogoHandler godoc
// @Summary Get logo image
// @Description Get the logo image for the application
// @Tags Application Configuration
// @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 /application-configuration/logo [get]
func (acc *AppConfigController) getLogoHandler(c *gin.Context) {
lightLogo := c.DefaultQuery("light", "true") == "true"
@@ -117,15 +159,42 @@ func (acc *AppConfigController) getLogoHandler(c *gin.Context) {
acc.getImage(c, imageName, imageType)
}
// getFaviconHandler godoc
// @Summary Get favicon
// @Description Get the favicon for the application
// @Tags Application Configuration
// @Produce image/x-icon
// @Success 200 {file} binary "Favicon image"
// @Failure 404 {object} object "{"error": "File not found"}"
// @Router /application-configuration/favicon [get]
func (acc *AppConfigController) getFaviconHandler(c *gin.Context) {
acc.getImage(c, "favicon", "ico")
}
// getBackgroundImageHandler godoc
// @Summary Get background image
// @Description Get the background image for the application
// @Tags Application Configuration
// @Produce image/png
// @Produce image/jpeg
// @Success 200 {file} binary "Background image"
// @Failure 404 {object} object "{"error": "File not found"}"
// @Router /application-configuration/background-image [get]
func (acc *AppConfigController) getBackgroundImageHandler(c *gin.Context) {
imageType := acc.appConfigService.DbConfig.BackgroundImageType.Value
acc.getImage(c, "background", imageType)
}
// updateLogoHandler godoc
// @Summary Update logo
// @Description Update the application logo
// @Tags Application Configuration
// @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"
// @Security BearerAuth
// @Router /application-configuration/logo [put]
func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
lightLogo := c.DefaultQuery("light", "true") == "true"
@@ -143,6 +212,15 @@ func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
acc.updateImage(c, imageName, imageType)
}
// updateFaviconHandler godoc
// @Summary Update favicon
// @Description Update the application favicon
// @Tags Application Configuration
// @Accept multipart/form-data
// @Param file formData file true "Favicon file (.ico)"
// @Success 204 "No Content"
// @Security BearerAuth
// @Router /application-configuration/favicon [put]
func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
@@ -158,11 +236,21 @@ func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
acc.updateImage(c, "favicon", "ico")
}
// updateBackgroundImageHandler godoc
// @Summary Update background image
// @Description Update the application background image
// @Tags Application Configuration
// @Accept multipart/form-data
// @Param file formData file true "Background image file"
// @Success 204 "No Content"
// @Security BearerAuth
// @Router /application-configuration/background-image [put]
func (acc *AppConfigController) updateBackgroundImageHandler(c *gin.Context) {
imageType := acc.appConfigService.DbConfig.BackgroundImageType.Value
acc.updateImage(c, "background", imageType)
}
// getImage is a helper function to serve image files
func (acc *AppConfigController) getImage(c *gin.Context, name string, imageType string) {
imagePath := fmt.Sprintf("%s/application-images/%s.%s", common.EnvConfig.UploadPath, name, imageType)
mimeType := utils.GetImageMimeType(imageType)
@@ -171,6 +259,7 @@ func (acc *AppConfigController) getImage(c *gin.Context, name string, imageType
c.File(imagePath)
}
// updateImage is a helper function to update image files
func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, oldImageType string) {
file, err := c.FormFile("file")
if err != nil {
@@ -187,6 +276,13 @@ func (acc *AppConfigController) updateImage(c *gin.Context, imageName string, ol
c.Status(http.StatusNoContent)
}
// syncLdapHandler godoc
// @Summary Synchronize LDAP
// @Description Manually trigger LDAP synchronization
// @Tags Application Configuration
// @Success 204 "No Content"
// @Security BearerAuth
// @Router /application-configuration/sync-ldap [post]
func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
err := acc.ldapService.SyncAll()
if err != nil {
@@ -196,6 +292,14 @@ func (acc *AppConfigController) syncLdapHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// testEmailHandler godoc
// @Summary Send test email
// @Description Send a test email to verify email configuration
// @Tags Application Configuration
// @Success 204 "No Content"
// @Security BearerAuth
// @Router /application-configuration/test-email [post]
func (acc *AppConfigController) testEmailHandler(c *gin.Context) {
userID := c.GetString("userID")

View File

@@ -11,18 +11,32 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/service"
)
func NewAuditLogController(group *gin.RouterGroup, auditLogService *service.AuditLogService, jwtAuthMiddleware *middleware.JwtAuthMiddleware) {
// 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,
}
group.GET("/audit-logs", jwtAuthMiddleware.Add(false), alc.listAuditLogsForUserHandler)
group.GET("/audit-logs", authMiddleware.WithAdminNotRequired().Add(), alc.listAuditLogsForUserHandler)
}
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 page query int false "Page number, starting from 1" default(1)
// @Param limit query int false "Number of items per page" default(10)
// @Param sort_column query string false "Column to sort by" default("created_at")
// @Param sort_direction query string false "Sort direction (asc or desc)" default("desc")
// @Success 200 {object} dto.Paginated[dto.AuditLogDto]
// @Router /audit-logs [get]
func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
var sortedPaginationRequest utils.SortedPaginationRequest
if err := c.ShouldBindQuery(&sortedPaginationRequest); err != nil {
@@ -53,8 +67,8 @@ func (alc *AuditLogController) listAuditLogsForUserHandler(c *gin.Context) {
logsDtos[i] = logsDto
}
c.JSON(http.StatusOK, gin.H{
"data": logsDtos,
"pagination": pagination,
c.JSON(http.StatusOK, dto.Paginated[dto.AuditLogDto]{
Data: logsDtos,
Pagination: pagination,
})
}

View File

@@ -9,17 +9,37 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/service"
)
func NewCustomClaimController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, customClaimService *service.CustomClaimService) {
// 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}
group.GET("/custom-claims/suggestions", jwtAuthMiddleware.Add(true), wkc.getSuggestionsHandler)
group.PUT("/custom-claims/user/:userId", jwtAuthMiddleware.Add(true), wkc.UpdateCustomClaimsForUserHandler)
group.PUT("/custom-claims/user-group/:userGroupId", jwtAuthMiddleware.Add(true), wkc.UpdateCustomClaimsForUserGroupHandler)
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 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"
// @Failure 401 {object} object "Unauthorized"
// @Failure 403 {object} object "Forbidden"
// @Failure 500 {object} object "Internal server error"
// @Security BearerAuth
// @Router /custom-claims/suggestions [get]
func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) {
claims, err := ccc.customClaimService.GetSuggestions()
if err != nil {
@@ -30,6 +50,16 @@ func (ccc *CustomClaimController) getSuggestionsHandler(c *gin.Context) {
c.JSON(http.StatusOK, claims)
}
// 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 /custom-claims/user/{userId} [put]
func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Context) {
var input []dto.CustomClaimCreateDto
@@ -54,6 +84,17 @@ func (ccc *CustomClaimController) UpdateCustomClaimsForUserHandler(c *gin.Contex
c.JSON(http.StatusOK, customClaimsDto)
}
// 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"
// @Security BearerAuth
// @Router /custom-claims/user-group/{userGroupId} [put]
func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.Context) {
var input []dto.CustomClaimCreateDto
@@ -62,8 +103,8 @@ func (ccc *CustomClaimController) UpdateCustomClaimsForUserGroupHandler(c *gin.C
return
}
userId := c.Param("userGroupId")
claims, err := ccc.customClaimService.UpdateCustomClaimsForUserGroup(userId, input)
userGroupId := c.Param("userGroupId")
claims, err := ccc.customClaimService.UpdateCustomClaimsForUserGroup(userGroupId, input)
if err != nil {
c.Error(err)
return

View File

@@ -1,13 +1,14 @@
package controller
import (
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
"log"
"net/http"
"net/url"
"strings"
"github.com/pocket-id/pocket-id/backend/internal/common"
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
"github.com/gin-gonic/gin"
"github.com/pocket-id/pocket-id/backend/internal/dto"
"github.com/pocket-id/pocket-id/backend/internal/middleware"
@@ -15,30 +16,35 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/utils"
)
func NewOidcController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, fileSizeLimitMiddleware *middleware.FileSizeLimitMiddleware, oidcService *service.OidcService, jwtService *service.JwtService) {
// 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, jwtService *service.JwtService) {
oc := &OidcController{oidcService: oidcService, jwtService: jwtService}
group.POST("/oidc/authorize", jwtAuthMiddleware.Add(false), oc.authorizeHandler)
group.POST("/oidc/authorization-required", jwtAuthMiddleware.Add(false), oc.authorizationConfirmationRequiredHandler)
group.POST("/oidc/authorize", authMiddleware.WithAdminNotRequired().Add(), oc.authorizeHandler)
group.POST("/oidc/authorization-required", authMiddleware.WithAdminNotRequired().Add(), oc.authorizationConfirmationRequiredHandler)
group.POST("/oidc/token", oc.createTokensHandler)
group.GET("/oidc/userinfo", oc.userInfoHandler)
group.POST("/oidc/userinfo", oc.userInfoHandler)
group.POST("/oidc/end-session", oc.EndSessionHandler)
group.GET("/oidc/end-session", oc.EndSessionHandler)
group.POST("/oidc/end-session", authMiddleware.WithSuccessOptional().Add(), oc.EndSessionHandler)
group.GET("/oidc/end-session", authMiddleware.WithSuccessOptional().Add(), oc.EndSessionHandler)
group.GET("/oidc/clients", jwtAuthMiddleware.Add(true), oc.listClientsHandler)
group.POST("/oidc/clients", jwtAuthMiddleware.Add(true), oc.createClientHandler)
group.GET("/oidc/clients/:id", oc.getClientHandler)
group.PUT("/oidc/clients/:id", jwtAuthMiddleware.Add(true), oc.updateClientHandler)
group.DELETE("/oidc/clients/:id", jwtAuthMiddleware.Add(true), oc.deleteClientHandler)
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)
group.PUT("/oidc/clients/:id/allowed-user-groups", jwtAuthMiddleware.Add(true), oc.updateAllowedUserGroupsHandler)
group.POST("/oidc/clients/:id/secret", jwtAuthMiddleware.Add(true), oc.createClientSecretHandler)
group.PUT("/oidc/clients/:id/allowed-user-groups", authMiddleware.Add(), oc.updateAllowedUserGroupsHandler)
group.POST("/oidc/clients/:id/secret", authMiddleware.Add(), oc.createClientSecretHandler)
group.GET("/oidc/clients/:id/logo", oc.getClientLogoHandler)
group.DELETE("/oidc/clients/:id/logo", oc.deleteClientLogoHandler)
group.POST("/oidc/clients/:id/logo", jwtAuthMiddleware.Add(true), fileSizeLimitMiddleware.Add(2<<20), oc.updateClientLogoHandler)
group.POST("/oidc/clients/:id/logo", authMiddleware.Add(), fileSizeLimitMiddleware.Add(2<<20), oc.updateClientLogoHandler)
}
type OidcController struct {
@@ -46,6 +52,16 @@ type OidcController struct {
jwtService *service.JwtService
}
// authorizeHandler godoc
// @Summary Authorize OIDC client
// @Description Start the OIDC authorization process for a client
// @Tags OIDC
// @Accept json
// @Produce json
// @Param request body dto.AuthorizeOidcClientRequestDto true "Authorization request parameters"
// @Success 200 {object} dto.AuthorizeOidcClientResponseDto "Authorization code and callback URL"
// @Security BearerAuth
// @Router /oidc/authorize [post]
func (oc *OidcController) authorizeHandler(c *gin.Context) {
var input dto.AuthorizeOidcClientRequestDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -67,6 +83,16 @@ func (oc *OidcController) authorizeHandler(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// authorizationConfirmationRequiredHandler godoc
// @Summary Check if authorization confirmation is required
// @Description Check if the user needs to confirm authorization for the client
// @Tags OIDC
// @Accept json
// @Produce json
// @Param request body dto.AuthorizationRequiredDto true "Authorization check parameters"
// @Success 200 {object} object "{ \"authorizationRequired\": true/false }"
// @Security BearerAuth
// @Router /oidc/authorization-required [post]
func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Context) {
var input dto.AuthorizationRequiredDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -83,6 +109,19 @@ func (oc *OidcController) authorizationConfirmationRequiredHandler(c *gin.Contex
c.JSON(http.StatusOK, gin.H{"authorizationRequired": !hasAuthorizedClient})
}
// createTokensHandler godoc
// @Summary Create OIDC tokens
// @Description Exchange authorization code for ID and access tokens
// @Tags OIDC
// @Accept application/x-www-form-urlencoded
// @Produce json
// @Param client_id formData string false "Client ID (if not using Basic Auth)"
// @Param client_secret formData string false "Client secret (if not using Basic Auth)"
// @Param code formData string true "Authorization code"
// @Param grant_type formData string true "Grant type (must be 'authorization_code')"
// @Param code_verifier formData string false "PKCE code verifier"
// @Success 200 {object} object "{ \"id_token\": \"string\", \"access_token\": \"string\", \"token_type\": \"Bearer\" }"
// @Router /oidc/token [post]
func (oc *OidcController) createTokensHandler(c *gin.Context) {
// Disable cors for this endpoint
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
@@ -111,6 +150,15 @@ func (oc *OidcController) createTokensHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"id_token": idToken, "access_token": accessToken, "token_type": "Bearer"})
}
// userInfoHandler godoc
// @Summary Get user information
// @Description Get user information based on the access token
// @Tags OIDC
// @Accept json
// @Produce json
// @Success 200 {object} object "User claims based on requested scopes"
// @Security OAuth2AccessToken
// @Router /oidc/userinfo [get]
func (oc *OidcController) userInfoHandler(c *gin.Context) {
authHeaderSplit := strings.Split(c.GetHeader("Authorization"), " ")
if len(authHeaderSplit) != 2 {
@@ -136,6 +184,30 @@ func (oc *OidcController) userInfoHandler(c *gin.Context) {
c.JSON(http.StatusOK, claims)
}
// userInfoHandler godoc (POST method)
// @Summary Get user information (POST method)
// @Description Get user information based on the access token using POST
// @Tags OIDC
// @Accept json
// @Produce json
// @Success 200 {object} object "User claims based on requested scopes"
// @Security OAuth2AccessToken
// @Router /oidc/userinfo [post]
func (oc *OidcController) userInfoHandlerPost(c *gin.Context) {
// Implementation is the same as GET
}
// EndSessionHandler godoc
// @Summary End OIDC session
// @Description End user session and handle OIDC logout
// @Tags OIDC
// @Accept application/x-www-form-urlencoded
// @Produce html
// @Param id_token_hint query string false "ID token"
// @Param post_logout_redirect_uri query string false "URL to redirect to after logout"
// @Param state query string false "State parameter to include in the redirect"
// @Success 302 "Redirect to post-logout URL or application logout page"
// @Router /oidc/end-session [get]
func (oc *OidcController) EndSessionHandler(c *gin.Context) {
var input dto.OidcLogoutDto
@@ -174,6 +246,56 @@ func (oc *OidcController) EndSessionHandler(c *gin.Context) {
c.Redirect(http.StatusFound, logoutCallbackURL.String())
}
// EndSessionHandler godoc (POST method)
// @Summary End OIDC session (POST method)
// @Description End user session and handle OIDC logout using POST
// @Tags OIDC
// @Accept application/x-www-form-urlencoded
// @Produce html
// @Param id_token_hint formData string false "ID token"
// @Param post_logout_redirect_uri formData string false "URL to redirect to after logout"
// @Param state formData string false "State parameter to include in the redirect"
// @Success 302 "Redirect to post-logout URL or application logout page"
// @Router /oidc/end-session [post]
func (oc *OidcController) EndSessionHandlerPost(c *gin.Context) {
// Implementation is the same as GET
}
// 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 /oidc/clients/{id}/meta [get]
func (oc *OidcController) getClientMetaDataHandler(c *gin.Context) {
clientId := c.Param("id")
client, err := oc.oidcService.GetClient(clientId)
if err != nil {
c.Error(err)
return
}
clientDto := dto.OidcClientMetaDataDto{}
err = dto.MapStruct(client, &clientDto)
if err == nil {
c.JSON(http.StatusOK, clientDto)
return
}
c.Error(err)
}
// 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"
// @Security BearerAuth
// @Router /oidc/clients/{id} [get]
func (oc *OidcController) getClientHandler(c *gin.Context) {
clientId := c.Param("id")
client, err := oc.oidcService.GetClient(clientId)
@@ -182,26 +304,28 @@ func (oc *OidcController) getClientHandler(c *gin.Context) {
return
}
// Return a different DTO based on the user's role
if c.GetBool("userIsAdmin") {
clientDto := dto.OidcClientWithAllowedUserGroupsDto{}
err = dto.MapStruct(client, &clientDto)
if err == nil {
c.JSON(http.StatusOK, clientDto)
return
}
} else {
clientDto := dto.PublicOidcClientDto{}
err = dto.MapStruct(client, &clientDto)
if err == nil {
c.JSON(http.StatusOK, clientDto)
return
}
clientDto := dto.OidcClientWithAllowedUserGroupsDto{}
err = dto.MapStruct(client, &clientDto)
if err == nil {
c.JSON(http.StatusOK, clientDto)
return
}
c.Error(err)
}
// 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 page query int false "Page number, starting from 1" default(1)
// @Param limit query int false "Number of items per page" default(10)
// @Param sort_column query string false "Column to sort by" default("name")
// @Param sort_direction query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.OidcClientDto]
// @Security BearerAuth
// @Router /oidc/clients [get]
func (oc *OidcController) listClientsHandler(c *gin.Context) {
searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest
@@ -222,12 +346,22 @@ func (oc *OidcController) listClientsHandler(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"data": clientsDto,
"pagination": pagination,
c.JSON(http.StatusOK, dto.Paginated[dto.OidcClientDto]{
Data: clientsDto,
Pagination: pagination,
})
}
// 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"
// @Security BearerAuth
// @Router /oidc/clients [post]
func (oc *OidcController) createClientHandler(c *gin.Context) {
var input dto.OidcClientCreateDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -250,6 +384,14 @@ func (oc *OidcController) createClientHandler(c *gin.Context) {
c.JSON(http.StatusCreated, clientDto)
}
// 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"
// @Security BearerAuth
// @Router /oidc/clients/{id} [delete]
func (oc *OidcController) deleteClientHandler(c *gin.Context) {
err := oc.oidcService.DeleteClient(c.Param("id"))
if err != nil {
@@ -260,6 +402,17 @@ func (oc *OidcController) deleteClientHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// 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.OidcClientCreateDto true "Client information"
// @Success 200 {object} dto.OidcClientWithAllowedUserGroupsDto "Updated client"
// @Security BearerAuth
// @Router /oidc/clients/{id} [put]
func (oc *OidcController) updateClientHandler(c *gin.Context) {
var input dto.OidcClientCreateDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -282,6 +435,15 @@ func (oc *OidcController) updateClientHandler(c *gin.Context) {
c.JSON(http.StatusOK, clientDto)
}
// 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\" }"
// @Security BearerAuth
// @Router /oidc/clients/{id}/secret [post]
func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
secret, err := oc.oidcService.CreateClientSecret(c.Param("id"))
if err != nil {
@@ -292,6 +454,16 @@ func (oc *OidcController) createClientSecretHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"secret": secret})
}
// 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"
// @Success 200 {file} binary "Logo image"
// @Router /oidc/clients/{id}/logo [get]
func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
imagePath, mimeType, err := oc.oidcService.GetClientLogo(c.Param("id"))
if err != nil {
@@ -303,6 +475,16 @@ func (oc *OidcController) getClientLogoHandler(c *gin.Context) {
c.File(imagePath)
}
// 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, max 2MB)"
// @Success 204 "No Content"
// @Security BearerAuth
// @Router /oidc/clients/{id}/logo [post]
func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
@@ -319,6 +501,14 @@ func (oc *OidcController) updateClientLogoHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// deleteClientLogoHandler godoc
// @Summary Delete client logo
// @Description Delete the logo for an OIDC client
// @Tags OIDC
// @Param id path string true "Client ID"
// @Success 204 "No Content"
// @Security BearerAuth
// @Router /oidc/clients/{id}/logo [delete]
func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
err := oc.oidcService.DeleteClientLogo(c.Param("id"))
if err != nil {
@@ -329,6 +519,17 @@ func (oc *OidcController) deleteClientLogoHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// 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"
// @Security BearerAuth
// @Router /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 {

View File

@@ -16,30 +16,34 @@ import (
"golang.org/x/time/rate"
)
func NewUserController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, appConfigService *service.AppConfigService) {
// NewUserController creates a new controller for user management endpoints
// @Summary User management controller
// @Description Initializes all user-related API endpoints
// @Tags Users
func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, userService *service.UserService, appConfigService *service.AppConfigService) {
uc := UserController{
userService: userService,
appConfigService: appConfigService,
}
group.GET("/users", jwtAuthMiddleware.Add(true), uc.listUsersHandler)
group.GET("/users/me", jwtAuthMiddleware.Add(false), uc.getCurrentUserHandler)
group.GET("/users/:id", jwtAuthMiddleware.Add(true), uc.getUserHandler)
group.POST("/users", jwtAuthMiddleware.Add(true), uc.createUserHandler)
group.PUT("/users/:id", jwtAuthMiddleware.Add(true), uc.updateUserHandler)
group.GET("/users/:id/groups", jwtAuthMiddleware.Add(true), uc.getUserGroupsHandler)
group.PUT("/users/me", jwtAuthMiddleware.Add(false), uc.updateCurrentUserHandler)
group.DELETE("/users/:id", jwtAuthMiddleware.Add(true), uc.deleteUserHandler)
group.GET("/users", authMiddleware.Add(), uc.listUsersHandler)
group.GET("/users/me", authMiddleware.WithAdminNotRequired().Add(), uc.getCurrentUserHandler)
group.GET("/users/:id", authMiddleware.Add(), uc.getUserHandler)
group.POST("/users", authMiddleware.Add(), uc.createUserHandler)
group.PUT("/users/:id", authMiddleware.Add(), uc.updateUserHandler)
group.GET("/users/:id/groups", authMiddleware.Add(), uc.getUserGroupsHandler)
group.PUT("/users/me", authMiddleware.WithAdminNotRequired().Add(), uc.updateCurrentUserHandler)
group.DELETE("/users/:id", authMiddleware.Add(), uc.deleteUserHandler)
group.PUT("/users/:id/user-groups", jwtAuthMiddleware.Add(true), uc.updateUserGroups)
group.PUT("/users/:id/user-groups", authMiddleware.Add(), uc.updateUserGroups)
group.GET("/users/:id/profile-picture.png", uc.getUserProfilePictureHandler)
group.GET("/users/me/profile-picture.png", jwtAuthMiddleware.Add(false), uc.getCurrentUserProfilePictureHandler)
group.PUT("/users/:id/profile-picture", jwtAuthMiddleware.Add(true), uc.updateUserProfilePictureHandler)
group.PUT("/users/me/profile-picture", jwtAuthMiddleware.Add(false), uc.updateCurrentUserProfilePictureHandler)
group.GET("/users/me/profile-picture.png", authMiddleware.WithAdminNotRequired().Add(), uc.getCurrentUserProfilePictureHandler)
group.PUT("/users/:id/profile-picture", authMiddleware.Add(), uc.updateUserProfilePictureHandler)
group.PUT("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.updateCurrentUserProfilePictureHandler)
group.POST("/users/me/one-time-access-token", jwtAuthMiddleware.Add(false), uc.createOwnOneTimeAccessTokenHandler)
group.POST("/users/:id/one-time-access-token", jwtAuthMiddleware.Add(true), uc.createAdminOneTimeAccessTokenHandler)
group.POST("/users/me/one-time-access-token", authMiddleware.WithAdminNotRequired().Add(), uc.createOwnOneTimeAccessTokenHandler)
group.POST("/users/:id/one-time-access-token", authMiddleware.Add(), uc.createAdminOneTimeAccessTokenHandler)
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.requestOneTimeAccessEmailHandler)
@@ -50,6 +54,13 @@ type UserController struct {
appConfigService *service.AppConfigService
}
// getUserGroupsHandler godoc
// @Summary Get user groups
// @Description Retrieve all groups a specific user belongs to
// @Tags Users,User Groups
// @Param id path string true "User ID"
// @Success 200 {array} dto.UserGroupDtoWithUsers
// @Router /users/{id}/groups [get]
func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
userID := c.Param("id")
groups, err := uc.userService.GetUserGroups(userID)
@@ -67,6 +78,17 @@ func (uc *UserController) getUserGroupsHandler(c *gin.Context) {
c.JSON(http.StatusOK, groupsDto)
}
// listUsersHandler godoc
// @Summary List users
// @Description Get a paginated list of users with optional search and sorting
// @Tags Users
// @Param search query string false "Search term to filter users"
// @Param page query int false "Page number, starting from 1" default(1)
// @Param limit query int false "Number of items per page" default(10)
// @Param sort_column query string false "Column to sort by" default("created_at")
// @Param sort_direction query string false "Sort direction (asc or desc)" default("desc")
// @Success 200 {object} dto.Paginated[dto.UserDto]
// @Router /users [get]
func (uc *UserController) listUsersHandler(c *gin.Context) {
searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest
@@ -87,12 +109,19 @@ func (uc *UserController) listUsersHandler(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{
"data": usersDto,
"pagination": pagination,
c.JSON(http.StatusOK, dto.Paginated[dto.UserDto]{
Data: usersDto,
Pagination: pagination,
})
}
// getUserHandler godoc
// @Summary Get user by ID
// @Description Retrieve detailed information about a specific user
// @Tags Users
// @Param id path string true "User ID"
// @Success 200 {object} dto.UserDto
// @Router /users/{id} [get]
func (uc *UserController) getUserHandler(c *gin.Context) {
user, err := uc.userService.GetUser(c.Param("id"))
if err != nil {
@@ -109,6 +138,12 @@ func (uc *UserController) getUserHandler(c *gin.Context) {
c.JSON(http.StatusOK, userDto)
}
// getCurrentUserHandler godoc
// @Summary Get current user
// @Description Retrieve information about the currently authenticated user
// @Tags Users
// @Success 200 {object} dto.UserDto
// @Router /users/me [get]
func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
user, err := uc.userService.GetUser(c.GetString("userID"))
if err != nil {
@@ -125,6 +160,13 @@ func (uc *UserController) getCurrentUserHandler(c *gin.Context) {
c.JSON(http.StatusOK, userDto)
}
// deleteUserHandler godoc
// @Summary Delete user
// @Description Delete a specific user by ID
// @Tags Users
// @Param id path string true "User ID"
// @Success 204 "No Content"
// @Router /users/{id} [delete]
func (uc *UserController) deleteUserHandler(c *gin.Context) {
if err := uc.userService.DeleteUser(c.Param("id")); err != nil {
c.Error(err)
@@ -134,6 +176,13 @@ func (uc *UserController) deleteUserHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// createUserHandler godoc
// @Summary Create user
// @Description Create a new user
// @Tags Users
// @Param user body dto.UserCreateDto true "User information"
// @Success 201 {object} dto.UserDto
// @Router /users [post]
func (uc *UserController) createUserHandler(c *gin.Context) {
var input dto.UserCreateDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -156,10 +205,25 @@ func (uc *UserController) createUserHandler(c *gin.Context) {
c.JSON(http.StatusCreated, userDto)
}
// updateUserHandler godoc
// @Summary Update user
// @Description Update an existing user by ID
// @Tags Users
// @Param id path string true "User ID"
// @Param user body dto.UserCreateDto true "User information"
// @Success 200 {object} dto.UserDto
// @Router /users/{id} [put]
func (uc *UserController) updateUserHandler(c *gin.Context) {
uc.updateUser(c, false)
}
// updateCurrentUserHandler godoc
// @Summary Update current user
// @Description Update the currently authenticated user's information
// @Tags Users
// @Param user body dto.UserCreateDto true "User information"
// @Success 200 {object} dto.UserDto
// @Router /users/me [put]
func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
if uc.appConfigService.DbConfig.AllowOwnAccountEdit.Value != "true" {
c.Error(&common.AccountEditNotAllowedError{})
@@ -168,6 +232,14 @@ func (uc *UserController) updateCurrentUserHandler(c *gin.Context) {
uc.updateUser(c, true)
}
// getUserProfilePictureHandler godoc
// @Summary Get user profile picture
// @Description Retrieve a specific user's profile picture
// @Tags Users
// @Produce image/png
// @Param id path string true "User ID"
// @Success 200 {file} binary "PNG image"
// @Router /users/{id}/profile-picture.png [get]
func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
userID := c.Param("id")
@@ -180,6 +252,13 @@ func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
c.DataFromReader(http.StatusOK, size, "image/png", picture, nil)
}
// getCurrentUserProfilePictureHandler godoc
// @Summary Get current user's profile picture
// @Description Retrieve the currently authenticated user's profile picture
// @Tags Users
// @Produce image/png
// @Success 200 {file} binary "PNG image"
// @Router /users/me/profile-picture.png [get]
func (uc *UserController) getCurrentUserProfilePictureHandler(c *gin.Context) {
userID := c.GetString("userID")
@@ -192,6 +271,16 @@ func (uc *UserController) getCurrentUserProfilePictureHandler(c *gin.Context) {
c.DataFromReader(http.StatusOK, size, "image/png", picture, nil)
}
// updateUserProfilePictureHandler godoc
// @Summary Update user profile picture
// @Description Update a specific user's profile picture
// @Tags Users
// @Accept multipart/form-data
// @Produce json
// @Param id path string true "User ID"
// @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)"
// @Success 204 "No Content"
// @Router /users/{id}/profile-picture [put]
func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
userID := c.Param("id")
fileHeader, err := c.FormFile("file")
@@ -214,6 +303,15 @@ func (uc *UserController) updateUserProfilePictureHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// updateCurrentUserProfilePictureHandler godoc
// @Summary Update current user's profile picture
// @Description Update the currently authenticated user's profile picture
// @Tags Users
// @Accept multipart/form-data
// @Produce json
// @Param file formData file true "Profile picture image file (PNG, JPG, or JPEG)"
// @Success 204 "No Content"
// @Router /users/me/profile-picture [put]
func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) {
userID := c.GetString("userID")
fileHeader, err := c.FormFile("file")
@@ -255,6 +353,14 @@ func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bo
c.JSON(http.StatusCreated, gin.H{"token": token})
}
// createOwnOneTimeAccessTokenHandler godoc
// @Summary Create one-time access token for current user
// @Description Generate a one-time access token for the currently authenticated user
// @Tags Users
// @Param id path string true "User ID"
// @Param body body dto.OneTimeAccessTokenCreateDto true "Token options"
// @Success 201 {object} object "{ \"token\": \"string\" }"
// @Router /users/{id}/one-time-access-token [post]
func (uc *UserController) createOwnOneTimeAccessTokenHandler(c *gin.Context) {
uc.createOneTimeAccessTokenHandler(c, true)
}
@@ -279,6 +385,13 @@ func (uc *UserController) requestOneTimeAccessEmailHandler(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// exchangeOneTimeAccessTokenHandler godoc
// @Summary Exchange one-time access token
// @Description Exchange a one-time access token for a session token
// @Tags Users
// @Param token path string true "One-time access token"
// @Success 200 {object} dto.UserDto
// @Router /one-time-access-token/{token} [post]
func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
user, token, err := uc.userService.ExchangeOneTimeAccessToken(c.Param("token"), c.ClientIP(), c.Request.UserAgent())
if err != nil {
@@ -299,6 +412,12 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
c.JSON(http.StatusOK, userDto)
}
// getSetupAccessTokenHandler godoc
// @Summary Setup initial admin
// @Description Generate setup access token for initial admin user configuration
// @Tags Users
// @Success 200 {object} dto.UserDto
// @Router /one-time-access-token/setup [post]
func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
user, token, err := uc.userService.SetupInitialAdmin()
if err != nil {
@@ -319,6 +438,37 @@ func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
c.JSON(http.StatusOK, userDto)
}
// updateUserGroups godoc
// @Summary Update user groups
// @Description Update the groups a specific user belongs to
// @Tags Users
// @Param id path string true "User ID"
// @Param groups body dto.UserUpdateUserGroupDto true "User group IDs"
// @Success 200 {object} dto.UserDto
// @Router /users/{id}/user-groups [put]
func (uc *UserController) updateUserGroups(c *gin.Context) {
var input dto.UserUpdateUserGroupDto
if err := c.ShouldBindJSON(&input); err != nil {
c.Error(err)
return
}
user, err := uc.userService.UpdateUserGroups(c.Param("id"), input.UserGroupIds)
if err != nil {
c.Error(err)
return
}
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, userDto)
}
// updateUser is an internal helper method, not exposed as an API endpoint
func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
var input dto.UserCreateDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -347,25 +497,3 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
c.JSON(http.StatusOK, userDto)
}
func (uc *UserController) updateUserGroups(c *gin.Context) {
var input dto.UserUpdateUserGroupDto
if err := c.ShouldBindJSON(&input); err != nil {
c.Error(err)
return
}
user, err := uc.userService.UpdateUserGroups(c.Param("id"), input.UserGroupIds)
if err != nil {
c.Error(err)
return
}
var userDto dto.UserDto
if err := dto.MapStruct(user, &userDto); err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, userDto)
}

View File

@@ -10,23 +10,42 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/utils"
)
func NewUserGroupController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, userGroupService *service.UserGroupService) {
// 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,
}
group.GET("/user-groups", jwtAuthMiddleware.Add(true), ugc.list)
group.GET("/user-groups/:id", jwtAuthMiddleware.Add(true), ugc.get)
group.POST("/user-groups", jwtAuthMiddleware.Add(true), ugc.create)
group.PUT("/user-groups/:id", jwtAuthMiddleware.Add(true), ugc.update)
group.DELETE("/user-groups/:id", jwtAuthMiddleware.Add(true), ugc.delete)
group.PUT("/user-groups/:id/users", jwtAuthMiddleware.Add(true), ugc.updateUsers)
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)
}
}
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 page query int false "Page number, starting from 1" default(1)
// @Param limit query int false "Number of items per page" default(10)
// @Param sort_column query string false "Column to sort by" default("name")
// @Param sort_direction query string false "Sort direction (asc or desc)" default("asc")
// @Success 200 {object} dto.Paginated[dto.UserGroupDtoWithUserCount]
// @Router /user-groups [get]
func (ugc *UserGroupController) list(c *gin.Context) {
searchTerm := c.Query("search")
var sortedPaginationRequest utils.SortedPaginationRequest
@@ -41,7 +60,7 @@ func (ugc *UserGroupController) list(c *gin.Context) {
return
}
// Map the user groups to DTOs. The user count can't be mapped directly, so we have to do it manually.
// Map the user groups to DTOs
var groupsDto = make([]dto.UserGroupDtoWithUserCount, len(groups))
for i, group := range groups {
var groupDto dto.UserGroupDtoWithUserCount
@@ -57,12 +76,22 @@ func (ugc *UserGroupController) list(c *gin.Context) {
groupsDto[i] = groupDto
}
c.JSON(http.StatusOK, gin.H{
"data": groupsDto,
"pagination": pagination,
c.JSON(http.StatusOK, dto.Paginated[dto.UserGroupDtoWithUserCount]{
Data: groupsDto,
Pagination: pagination,
})
}
// 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.UserGroupDtoWithUsers
// @Security BearerAuth
// @Router /user-groups/{id} [get]
func (ugc *UserGroupController) get(c *gin.Context) {
group, err := ugc.UserGroupService.Get(c.Param("id"))
if err != nil {
@@ -79,6 +108,16 @@ func (ugc *UserGroupController) get(c *gin.Context) {
c.JSON(http.StatusOK, groupDto)
}
// 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.UserGroupDtoWithUsers "Created user group"
// @Security BearerAuth
// @Router /user-groups [post]
func (ugc *UserGroupController) create(c *gin.Context) {
var input dto.UserGroupCreateDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -101,6 +140,17 @@ func (ugc *UserGroupController) create(c *gin.Context) {
c.JSON(http.StatusCreated, groupDto)
}
// 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.UserGroupDtoWithUsers "Updated user group"
// @Security BearerAuth
// @Router /user-groups/{id} [put]
func (ugc *UserGroupController) update(c *gin.Context) {
var input dto.UserGroupCreateDto
if err := c.ShouldBindJSON(&input); err != nil {
@@ -123,6 +173,16 @@ func (ugc *UserGroupController) update(c *gin.Context) {
c.JSON(http.StatusOK, groupDto)
}
// 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"
// @Security BearerAuth
// @Router /user-groups/{id} [delete]
func (ugc *UserGroupController) delete(c *gin.Context) {
if err := ugc.UserGroupService.Delete(c.Param("id")); err != nil {
c.Error(err)
@@ -132,6 +192,17 @@ func (ugc *UserGroupController) delete(c *gin.Context) {
c.Status(http.StatusNoContent)
}
// 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.UserGroupDtoWithUsers
// @Security BearerAuth
// @Router /user-groups/{id}/users [put]
func (ugc *UserGroupController) updateUsers(c *gin.Context) {
var input dto.UserGroupUpdateUsersDto
if err := c.ShouldBindJSON(&input); err != nil {

View File

@@ -16,19 +16,19 @@ import (
"golang.org/x/time/rate"
)
func NewWebauthnController(group *gin.RouterGroup, jwtAuthMiddleware *middleware.JwtAuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, webauthnService *service.WebAuthnService, appConfigService *service.AppConfigService) {
func NewWebauthnController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, webauthnService *service.WebAuthnService, appConfigService *service.AppConfigService) {
wc := &WebauthnController{webAuthnService: webauthnService, appConfigService: appConfigService}
group.GET("/webauthn/register/start", jwtAuthMiddleware.Add(false), wc.beginRegistrationHandler)
group.POST("/webauthn/register/finish", jwtAuthMiddleware.Add(false), wc.verifyRegistrationHandler)
group.GET("/webauthn/register/start", authMiddleware.WithAdminNotRequired().Add(), wc.beginRegistrationHandler)
group.POST("/webauthn/register/finish", authMiddleware.WithAdminNotRequired().Add(), wc.verifyRegistrationHandler)
group.GET("/webauthn/login/start", wc.beginLoginHandler)
group.POST("/webauthn/login/finish", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), wc.verifyLoginHandler)
group.POST("/webauthn/logout", jwtAuthMiddleware.Add(false), wc.logoutHandler)
group.POST("/webauthn/logout", authMiddleware.WithAdminNotRequired().Add(), wc.logoutHandler)
group.GET("/webauthn/credentials", jwtAuthMiddleware.Add(false), wc.listCredentialsHandler)
group.PATCH("/webauthn/credentials/:id", jwtAuthMiddleware.Add(false), wc.updateCredentialHandler)
group.DELETE("/webauthn/credentials/:id", jwtAuthMiddleware.Add(false), wc.deleteCredentialHandler)
group.GET("/webauthn/credentials", authMiddleware.WithAdminNotRequired().Add(), wc.listCredentialsHandler)
group.PATCH("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.updateCredentialHandler)
group.DELETE("/webauthn/credentials/:id", authMiddleware.WithAdminNotRequired().Add(), wc.deleteCredentialHandler)
}
type WebauthnController struct {

View File

@@ -8,6 +8,10 @@ import (
"github.com/pocket-id/pocket-id/backend/internal/service"
)
// 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}
group.GET("/.well-known/jwks.json", wkc.jwksHandler)
@@ -18,6 +22,13 @@ type WellKnownController struct {
jwtService *service.JwtService
}
// 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) {
jwk, err := wkc.jwtService.GetJWK()
if err != nil {
@@ -28,6 +39,12 @@ func (wkc *WellKnownController) jwksHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"keys": []interface{}{jwk}})
}
// 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) {
appUrl := common.EnvConfig.AppURL
config := map[string]interface{}{