mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-28 11:39:04 +01:00
refactor: standardize API error handling (#1635)
This commit is contained in:
@@ -7,8 +7,9 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/apperror"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils/cookie"
|
||||
)
|
||||
@@ -24,19 +25,18 @@ func newHandler(service *Service, appConfig AppConfigResolver) *handler {
|
||||
return &handler{service: service, appConfig: appConfig}
|
||||
}
|
||||
|
||||
func (h *handler) checkInitialAdminSetupAvailable(c *gin.Context) {
|
||||
func (h *handler) checkInitialAdminSetupAvailable(c *gin.Context) error {
|
||||
setupCompleted, err := h.service.IsInitialAdminSetupCompleted(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
if setupCompleted {
|
||||
_ = c.Error(&common.SetupNotAvailableError{})
|
||||
return
|
||||
return apperror.SetupNotAvailable()
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// signUpInitialAdmin godoc
|
||||
@@ -48,37 +48,34 @@ func (h *handler) checkInitialAdminSetupAvailable(c *gin.Context) {
|
||||
// @Param body body signUpDto true "User information"
|
||||
// @Success 200 {object} dto.UserDto
|
||||
// @Router /api/signup/setup [post]
|
||||
func (h *handler) signUpInitialAdmin(c *gin.Context) {
|
||||
func (h *handler) signUpInitialAdmin(c *gin.Context) error {
|
||||
config, err := h.appConfig.GetConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
|
||||
return
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
var input signUpDto
|
||||
err = dto.ShouldBindWithNormalizedJSON(c, &input)
|
||||
err = httpserver.BindJSON(c, &input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
user, token, err := h.service.SignUpInitialAdmin(c.Request.Context(), config, input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var userDto dto.UserDto
|
||||
err = dto.MapStruct(user, &userDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
maxAge := int(config.SessionDuration.AsDurationMinutes().Seconds())
|
||||
cookie.AddAccessTokenCookie(c, maxAge, token)
|
||||
|
||||
c.JSON(http.StatusOK, userDto)
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSignupTokenHandler godoc
|
||||
@@ -90,11 +87,10 @@ func (h *handler) signUpInitialAdmin(c *gin.Context) {
|
||||
// @Param token body signupTokenCreateDto true "Signup token information"
|
||||
// @Success 201 {object} signupTokenDto
|
||||
// @Router /api/signup-tokens [post]
|
||||
func (h *handler) createSignupToken(c *gin.Context) {
|
||||
func (h *handler) createSignupToken(c *gin.Context) error {
|
||||
var input signupTokenCreateDto
|
||||
if err := c.ShouldBindJSON(&input); err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
if err := httpserver.BindJSON(c, &input); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ttl := input.TTL.Duration
|
||||
@@ -104,18 +100,17 @@ func (h *handler) createSignupToken(c *gin.Context) {
|
||||
|
||||
signupToken, err := h.service.CreateSignupToken(c.Request.Context(), ttl, input.UsageLimit, input.UserGroupIDs)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var tokenDto signupTokenDto
|
||||
err = dto.MapStruct(signupToken, &tokenDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, tokenDto)
|
||||
return nil
|
||||
}
|
||||
|
||||
// listSignupTokensHandler godoc
|
||||
@@ -128,26 +123,25 @@ func (h *handler) createSignupToken(c *gin.Context) {
|
||||
// @Param sort[direction] query string false "Sort direction (asc or desc)" default("asc")
|
||||
// @Success 200 {object} dto.Paginated[signupTokenDto]
|
||||
// @Router /api/signup-tokens [get]
|
||||
func (h *handler) listSignupTokens(c *gin.Context) {
|
||||
func (h *handler) listSignupTokens(c *gin.Context) error {
|
||||
listRequestOptions := utils.ParseListRequestOptions(c)
|
||||
|
||||
tokens, pagination, err := h.service.ListSignupTokens(c.Request.Context(), listRequestOptions)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
var tokensDto []signupTokenDto
|
||||
err = dto.MapStructList(tokens, &tokensDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dto.Paginated[signupTokenDto]{
|
||||
Data: tokensDto,
|
||||
Pagination: pagination,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteSignupTokenHandler godoc
|
||||
@@ -157,16 +151,16 @@ func (h *handler) listSignupTokens(c *gin.Context) {
|
||||
// @Param id path string true "Token ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /api/signup-tokens/{id} [delete]
|
||||
func (h *handler) deleteSignupToken(c *gin.Context) {
|
||||
func (h *handler) deleteSignupToken(c *gin.Context) error {
|
||||
tokenID := c.Param("id")
|
||||
|
||||
err := h.service.DeleteSignupToken(c.Request.Context(), tokenID)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
||||
|
||||
// signupHandler godoc
|
||||
@@ -178,24 +172,21 @@ func (h *handler) deleteSignupToken(c *gin.Context) {
|
||||
// @Param user body signUpDto true "User information"
|
||||
// @Success 201 {object} dto.UserDto
|
||||
// @Router /api/signup [post]
|
||||
func (h *handler) signup(c *gin.Context) {
|
||||
func (h *handler) signup(c *gin.Context) error {
|
||||
config, err := h.appConfig.GetConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
_ = c.Error(fmt.Errorf("error loading app configuration: %w", err))
|
||||
return
|
||||
return fmt.Errorf("error loading app configuration: %w", err)
|
||||
}
|
||||
|
||||
var input signUpDto
|
||||
err = dto.ShouldBindWithNormalizedJSON(c, &input)
|
||||
err = httpserver.BindJSON(c, &input)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
user, accessToken, err := h.service.SignUp(c.Request.Context(), config, input, c.ClientIP(), c.GetHeader("User-Agent"))
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
maxAge := int(config.SessionDuration.AsDurationMinutes().Seconds())
|
||||
@@ -204,9 +195,9 @@ func (h *handler) signup(c *gin.Context) {
|
||||
var userDto dto.UserDto
|
||||
err = dto.MapStruct(user, &userDto)
|
||||
if err != nil {
|
||||
_ = c.Error(err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, userDto)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// It's loaded here to create the per-token actors on first startup.
|
||||
|
||||
// signupTokensMigratedKey is the kv key under which the pre-actor signup tokens were frozen.
|
||||
const signupTokensMigratedKey = "signup_tokens_migrated" //nolint:gosec // G101 false positive: this is the name of a kv key, not a credential
|
||||
const signupTokensMigratedKey = "signup_tokens_migrated" // #nosec G101 -- database key name, not a credential
|
||||
|
||||
// migratedSignupToken is the JSON shape of a signup token frozen into the kv table by the migration.
|
||||
// All timestamps are expressed as Unix seconds.
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/httpserver"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
)
|
||||
|
||||
@@ -77,10 +78,10 @@ func (m *Module) RunSignupTokenMigration(ctx context.Context) error {
|
||||
// RegisterRoutes mounts the signup and signup-token management endpoints
|
||||
// adminAuth guards the admin token-management routes; signupRateLimit throttles public self-signup
|
||||
func (m *Module) RegisterRoutes(apiGroup *gin.RouterGroup, adminAuth, signupRateLimit gin.HandlerFunc) {
|
||||
apiGroup.POST("/signup-tokens", adminAuth, m.handler.createSignupToken)
|
||||
apiGroup.GET("/signup-tokens", adminAuth, m.handler.listSignupTokens)
|
||||
apiGroup.DELETE("/signup-tokens/:id", adminAuth, m.handler.deleteSignupToken)
|
||||
apiGroup.POST("/signup", signupRateLimit, m.handler.signup)
|
||||
apiGroup.GET("/signup/setup", m.handler.checkInitialAdminSetupAvailable)
|
||||
apiGroup.POST("/signup/setup", m.handler.signUpInitialAdmin)
|
||||
apiGroup.POST("/signup-tokens", adminAuth, httpserver.Handle(m.handler.createSignupToken))
|
||||
apiGroup.GET("/signup-tokens", adminAuth, httpserver.Handle(m.handler.listSignupTokens))
|
||||
apiGroup.DELETE("/signup-tokens/:id", adminAuth, httpserver.Handle(m.handler.deleteSignupToken))
|
||||
apiGroup.POST("/signup", signupRateLimit, httpserver.Handle(m.handler.signup))
|
||||
apiGroup.GET("/signup/setup", httpserver.Handle(m.handler.checkInitialAdminSetupAvailable))
|
||||
apiGroup.POST("/signup/setup", httpserver.Handle(m.handler.signUpInitialAdmin))
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/apperror"
|
||||
"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/model"
|
||||
@@ -46,7 +47,7 @@ func (s *Service) SignUp(ctx context.Context, config *appconfig.AppConfigModel,
|
||||
tokenProvided := signupData.Token != ""
|
||||
|
||||
if config.AllowUserSignups.String() != "open" && !tokenProvided {
|
||||
return model.User{}, "", &common.OpenSignupDisabledError{}
|
||||
return model.User{}, "", apperror.OpenSignupDisabled()
|
||||
}
|
||||
|
||||
var userGroupIDs []string
|
||||
@@ -65,7 +66,7 @@ func (s *Service) SignUp(ctx context.Context, config *appconfig.AppConfigModel,
|
||||
}
|
||||
|
||||
if consumeRes.Status != signupTokenConsumeOK {
|
||||
return model.User{}, "", &common.TokenInvalidOrExpiredError{}
|
||||
return model.User{}, "", apperror.TokenInvalidOrExpired()
|
||||
}
|
||||
userGroupIDs = consumeRes.UserGroupIDs
|
||||
}
|
||||
@@ -160,7 +161,7 @@ func (s *Service) SignUpInitialAdmin(ctx context.Context, config *appconfig.AppC
|
||||
return model.User{}, "", err
|
||||
}
|
||||
if setupCompleted {
|
||||
return model.User{}, "", &common.SetupNotAvailableError{}
|
||||
return model.User{}, "", apperror.SetupAlreadyCompleted()
|
||||
}
|
||||
|
||||
// Build the first user with administrator privileges
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/pocket-id/pocket-id/backend/internal/appconfig"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/common"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/apperror"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/model"
|
||||
"github.com/pocket-id/pocket-id/backend/internal/utils"
|
||||
@@ -114,8 +114,7 @@ func TestSignUpRejectsInvalidToken(t *testing.T) {
|
||||
Token: "not-a-real-token",
|
||||
}, "1.2.3.4", "test-agent")
|
||||
|
||||
var invalidErr *common.TokenInvalidOrExpiredError
|
||||
require.ErrorAs(t, err, &invalidErr)
|
||||
require.True(t, apperror.IsCode(err, apperror.CodeTokenInvalidOrExpired))
|
||||
}
|
||||
|
||||
func TestSignUpInitialAdminCreatesAdmin(t *testing.T) {
|
||||
@@ -154,8 +153,7 @@ func TestSignUpInitialAdminRejectsExistingInstallation(t *testing.T) {
|
||||
|
||||
// Reject setup when the installation already contains a user
|
||||
_, _, err := svc.SignUpInitialAdmin(t.Context(), appconfig.NewTestConfig(nil), signUpDto{Username: "new-admin"})
|
||||
var setupNotAvailableErr *common.SetupNotAvailableError
|
||||
require.ErrorAs(t, err, &setupNotAvailableErr)
|
||||
require.True(t, apperror.IsCode(err, apperror.CodeSetupAlreadyCompleted))
|
||||
}
|
||||
|
||||
// listAllOptions returns list options that return every token on a single page.
|
||||
|
||||
Reference in New Issue
Block a user