From a1f83c5ef8db71cb92d3a4781b52c3e4c5de0381 Mon Sep 17 00:00:00 2001 From: Jannis Mattheis Date: Sat, 24 Jan 2026 12:43:14 +0100 Subject: [PATCH] fix: enforce sort_key is unique per user --- api/application.go | 15 +++++++++++++-- api/application_test.go | 15 +++++++++++++++ database/database.go | 1 + model/application.go | 4 ++-- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/api/application.go b/api/application.go index 3efe806..2527842 100644 --- a/api/application.go +++ b/api/application.go @@ -12,6 +12,7 @@ import ( "github.com/gotify/server/v2/auth" "github.com/gotify/server/v2/model" "github.com/h2non/filetype" + "gorm.io/gorm" ) // The ApplicationDatabase interface for encapsulating database access. @@ -101,7 +102,8 @@ func (a *ApplicationAPI) CreateApplication(ctx *gin.Context) { Internal: false, } - if success := successOrAbort(ctx, 500, a.DB.CreateApplication(&app)); !success { + if err := a.DB.CreateApplication(&app); err != nil { + handleApplicationError(ctx, err) return } ctx.JSON(200, withResolvedImage(&app)) @@ -261,7 +263,8 @@ func (a *ApplicationAPI) UpdateApplication(ctx *gin.Context) { app.SortKey = applicationParams.SortKey } - if success := successOrAbort(ctx, 500, a.DB.UpdateApplication(app)); !success { + if err := a.DB.UpdateApplication(app); err != nil { + handleApplicationError(ctx, err) return } ctx.JSON(200, withResolvedImage(app)) @@ -476,3 +479,11 @@ func ValidApplicationImageExt(ext string) bool { return false } } + +func handleApplicationError(ctx *gin.Context, err error) { + if errors.Is(err, gorm.ErrDuplicatedKey) { + ctx.AbortWithError(400, errors.New("sort key is not unique")) + } else { + ctx.AbortWithError(500, err) + } +} diff --git a/api/application_test.go b/api/application_test.go index 117d09c..4070d0b 100644 --- a/api/application_test.go +++ b/api/application_test.go @@ -647,6 +647,21 @@ func (s *ApplicationSuite) Test_UpdateApplication_WithoutPermission_expectNotFou assert.Equal(s.T(), 404, s.recorder.Code) } +func (s *ApplicationSuite) Test_UpdateApplication_duplicateSortKey() { + user := s.db.User(5) + user.App(1) // sortKey=a0 + user.App(2) // sortKey=a1 + + s.withFormData("name=new_name&sortKey=a0") + test.WithUser(s.ctx, 5) + s.ctx.Params = gin.Params{{Key: "id", Value: "2"}} + + s.a.UpdateApplication(s.ctx) + + assert.EqualError(s.T(), s.ctx.Errors[0].Err, "sort key is not unique") + assert.Equal(s.T(), 400, s.recorder.Code) +} + func (s *ApplicationSuite) withFormData(formData string) { s.ctx.Request = httptest.NewRequest("POST", "/token", strings.NewReader(formData)) s.ctx.Request.Header.Set("Content-Type", "application/x-www-form-urlencoded") diff --git a/database/database.go b/database/database.go index 381108d..077e0eb 100644 --- a/database/database.go +++ b/database/database.go @@ -36,6 +36,7 @@ func New(dialect, connection, defaultUser, defaultPass string, strength int, cre gormConfig := &gorm.Config{ Logger: dbLogger, DisableForeignKeyConstraintWhenMigrating: true, + TranslateError: true, } var db *gorm.DB diff --git a/model/application.go b/model/application.go index 4a86cd3..2043d5a 100644 --- a/model/application.go +++ b/model/application.go @@ -20,7 +20,7 @@ type Application struct { // required: true // example: AWH0wZ5r0Mbac.r Token string `gorm:"type:varchar(180);uniqueIndex:uix_applications_token" json:"token"` - UserID uint `gorm:"index" json:"-"` + UserID uint `gorm:"index;uniqueIndex:uix_application_user_id_sort_key,priority:1" json:"-"` // The application name. This is how the application should be displayed to the user. // // required: true @@ -58,5 +58,5 @@ type Application struct { // // required: true // example: a1 - SortKey string `form:"sortKey" query:"sortKey" json:"sortKey"` + SortKey string `gorm:"uniqueIndex:uix_application_user_id_sort_key,priority:2" form:"sortKey" query:"sortKey" json:"sortKey"` }