Browse Source

normalize common database errors

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 1 year ago
parent
commit
87451560e3

+ 7 - 7
go.mod

@@ -10,10 +10,10 @@ require (
 	github.com/alexedwards/argon2id v1.0.0
 	github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
 	github.com/aws/aws-sdk-go-v2 v1.24.1
-	github.com/aws/aws-sdk-go-v2/config v1.26.3
-	github.com/aws/aws-sdk-go-v2/credentials v1.16.14
+	github.com/aws/aws-sdk-go-v2/config v1.26.4
+	github.com/aws/aws-sdk-go-v2/credentials v1.16.15
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11
-	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11
+	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12
 	github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.19.6
 	github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
 	github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2
@@ -95,7 +95,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
 	github.com/aws/smithy-go v1.19.0 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/boombuler/barcode v1.0.1 // indirect
@@ -171,9 +171,9 @@ require (
 	golang.org/x/tools v0.17.0 // indirect
 	golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
-	google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
+	google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect
 	google.golang.org/grpc v1.60.1 // indirect
 	google.golang.org/protobuf v1.32.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect

+ 14 - 14
go.sum

@@ -37,14 +37,14 @@ github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3
 github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 h1:OCs21ST2LrepDfD3lwlQiOqIGp6JiEUqG84GzTDoyJs=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4/go.mod h1:usURWEKSNNAcAZuzRn/9ZYPT8aZQkR7xcCtunK/LkJo=
-github.com/aws/aws-sdk-go-v2/config v1.26.3 h1:dKuc2jdp10y13dEEvPqWxqLoc0vF3Z9FC45MvuQSxOA=
-github.com/aws/aws-sdk-go-v2/config v1.26.3/go.mod h1:Bxgi+DeeswYofcYO0XyGClwlrq3DZEXli0kLf4hkGA0=
-github.com/aws/aws-sdk-go-v2/credentials v1.16.14 h1:mMDTwwYO9A0/JbOCOG7EOZHtYM+o7OfGWfu0toa23VE=
-github.com/aws/aws-sdk-go-v2/credentials v1.16.14/go.mod h1:cniAUh3ErQPHtCQGPT5ouvSAQ0od8caTO9OOuufZOAE=
+github.com/aws/aws-sdk-go-v2/config v1.26.4 h1:Juj7LhtxNudNUlfX22K5AnLafO+v4eq9PA3VWSCIQs4=
+github.com/aws/aws-sdk-go-v2/config v1.26.4/go.mod h1:tioqQ7wvxMYnTDpoTTLHhV3Zh+z261i/f2oz+ds8eNI=
+github.com/aws/aws-sdk-go-v2/credentials v1.16.15 h1:P0/m1LU08MF2kRzx4P//+7lNjiJod1z4xI2WpWhdpTQ=
+github.com/aws/aws-sdk-go-v2/credentials v1.16.15/go.mod h1:pgtMCf7Dx4GWw5EpHOTc2Sy17LIP0A0N2C9nQ83pQ/0=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11 h1:I6lAa3wBWfCz/cKkOpAcumsETRkFAl70sWi8ItcMEsM=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11/go.mod h1:be1NIO30kJA23ORBLqPo1LttEM6tPNSEcjkd1eKzNW0=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12 h1:0FMZy36RSYvcvVzEf1xbNdebLHZewW40QWP+P8jCMVk=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.12/go.mod h1:+chyahvarkb3HibkNei9IQEM9P5cWD5w2kgXCa3Hh0I=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
 github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
@@ -69,8 +69,8 @@ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2 h1:A5sGOT/mukuU+4At1
 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.26.2/go.mod h1:qutL00aW8GSo2D0I6UEOqMvRS3ZyuBrOC1BLe5D2jPc=
 github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 h1:dGrs+Q/WzhsiUKh82SfTVN66QzyulXuMDTV/G8ZxOac=
 github.com/aws/aws-sdk-go-v2/service/sso v1.18.6/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 h1:Yf2MIo9x+0tyv76GljxzqA3WtC5mw7NmazD2chwjxE4=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
 github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
 github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
 github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
@@ -531,12 +531,12 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1 h1:/IWabOtPziuXTEtI1KYCpM6Ss7vaAkeMxk+uXV/xvZs=
-google.golang.org/genproto v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
-google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1 h1:OPXtXn7fNMaXwO3JvOmF1QyTc00jsSFFz1vXXBOdCDo=
-google.golang.org/genproto/googleapis/api v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
+google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg=
+google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k=
+google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s=
+google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=

+ 29 - 8
internal/dataprovider/bolt.go

@@ -387,7 +387,10 @@ func (p *BoltProvider) addAdmin(admin *Admin) error {
 			return err
 		}
 		if a := bucket.Get([]byte(admin.Username)); a != nil {
-			return fmt.Errorf("admin %q already exists", admin.Username)
+			return util.NewI18nError(
+				fmt.Errorf("%w: admin %q already exists", ErrDuplicatedKey, admin.Username),
+				util.I18nErrorDuplicatedUsername,
+			)
 		}
 		id, err := bucket.NextSequence()
 		if err != nil {
@@ -649,7 +652,10 @@ func (p *BoltProvider) addUser(user *User) error {
 			return err
 		}
 		if u := bucket.Get([]byte(user.Username)); u != nil {
-			return fmt.Errorf("username %v already exists", user.Username)
+			return util.NewI18nError(
+				fmt.Errorf("%w: username %v already exists", ErrDuplicatedKey, user.Username),
+				util.I18nErrorDuplicatedUsername,
+			)
 		}
 		id, err := bucket.NextSequence()
 		if err != nil {
@@ -1121,7 +1127,10 @@ func (p *BoltProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
 			return err
 		}
 		if f := bucket.Get([]byte(folder.Name)); f != nil {
-			return fmt.Errorf("folder %v already exists", folder.Name)
+			return util.NewI18nError(
+				fmt.Errorf("%w: folder %q already exists", ErrDuplicatedKey, folder.Name),
+				util.I18nErrorDuplicatedUsername,
+			)
 		}
 		folder.Users = nil
 		folder.Groups = nil
@@ -1435,7 +1444,10 @@ func (p *BoltProvider) addGroup(group *Group) error {
 			return err
 		}
 		if u := bucket.Get([]byte(group.Name)); u != nil {
-			return fmt.Errorf("group %v already exists", group.Name)
+			return util.NewI18nError(
+				fmt.Errorf("%w: group %q already exists", ErrDuplicatedKey, group.Name),
+				util.I18nErrorDuplicatedUsername,
+			)
 		}
 		id, err := bucket.NextSequence()
 		if err != nil {
@@ -1808,7 +1820,7 @@ func (p *BoltProvider) addShare(share *Share) error {
 			return err
 		}
 		if a := bucket.Get([]byte(share.ShareID)); a != nil {
-			return fmt.Errorf("share %v already exists", share.ShareID)
+			return fmt.Errorf("share %q already exists", share.ShareID)
 		}
 		id, err := bucket.NextSequence()
 		if err != nil {
@@ -2188,7 +2200,10 @@ func (p *BoltProvider) addEventAction(action *BaseEventAction) error {
 			return err
 		}
 		if a := bucket.Get([]byte(action.Name)); a != nil {
-			return fmt.Errorf("event action %s already exists", action.Name)
+			return util.NewI18nError(
+				fmt.Errorf("%w: event action %q already exists", ErrDuplicatedKey, action.Name),
+				util.I18nErrorDuplicatedName,
+			)
 		}
 		id, err := bucket.NextSequence()
 		if err != nil {
@@ -2449,7 +2464,10 @@ func (p *BoltProvider) addEventRule(rule *EventRule) error {
 			return err
 		}
 		if r := bucket.Get([]byte(rule.Name)); r != nil {
-			return fmt.Errorf("event rule %q already exists", rule.Name)
+			return util.NewI18nError(
+				fmt.Errorf("%w: event rule %q already exists", ErrDuplicatedKey, rule.Name),
+				util.I18nErrorDuplicatedName,
+			)
 		}
 		id, err := bucket.NextSequence()
 		if err != nil {
@@ -2618,7 +2636,10 @@ func (p *BoltProvider) addRole(role *Role) error {
 			return err
 		}
 		if r := bucket.Get([]byte(role.Name)); r != nil {
-			return fmt.Errorf("role %q already exists", role.Name)
+			return util.NewI18nError(
+				fmt.Errorf("%w: role %q already exists", ErrDuplicatedKey, role.Name),
+				util.I18nErrorDuplicatedName,
+			)
 		}
 		id, err := bucket.NextSequence()
 		if err != nil {

+ 16 - 7
internal/dataprovider/dataprovider.go

@@ -148,6 +148,11 @@ const (
 	DumpScopeConfigs = "configs"
 )
 
+const (
+	fieldUsername = 1
+	fieldName     = 2
+)
+
 var (
 	// SupportedProviders defines the supported data providers
 	SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName,
@@ -176,13 +181,17 @@ var (
 	ErrInvalidCredentials = errors.New("invalid credentials")
 	// ErrLoginNotAllowedFromIP defines the error to return if login is denied from the current IP
 	ErrLoginNotAllowedFromIP = errors.New("login is not allowed from this IP")
-	isAdminCreated           atomic.Bool
-	validTLSUsernames        = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}
-	config                   Config
-	provider                 Provider
-	sqlPlaceholders          []string
-	internalHashPwdPrefixes  = []string{argonPwdPrefix, bcryptPwdPrefix}
-	hashPwdPrefixes          = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
+	// ErrDuplicatedKey occurs when there is a unique key constraint violation
+	ErrDuplicatedKey = errors.New("duplicated key not allowed")
+	// ErrForeignKeyViolated occurs when there is a foreign key constraint violation
+	ErrForeignKeyViolated   = errors.New("violates foreign key constraint")
+	isAdminCreated          atomic.Bool
+	validTLSUsernames       = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}
+	config                  Config
+	provider                Provider
+	sqlPlaceholders         []string
+	internalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix}
+	hashPwdPrefixes         = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
 		pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, md5DigestPwdPrefix,
 		sha256DigestPwdPrefix, sha512DigestPwdPrefix, sha256cryptPwdPrefix, sha512cryptPwdPrefix, yescryptPwdPrefix}
 	pbkdfPwdPrefixes        = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix}

+ 28 - 7
internal/dataprovider/memory.go

@@ -332,7 +332,10 @@ func (p *MemoryProvider) addUser(user *User) error {
 
 	_, err = p.userExistsInternal(user.Username)
 	if err == nil {
-		return fmt.Errorf("username %q already exists", user.Username)
+		return util.NewI18nError(
+			fmt.Errorf("%w: username %v already exists", ErrDuplicatedKey, user.Username),
+			util.I18nErrorDuplicatedUsername,
+		)
 	}
 	user.ID = p.getNextID()
 	user.LastQuotaUpdate = 0
@@ -730,7 +733,10 @@ func (p *MemoryProvider) addAdmin(admin *Admin) error {
 	}
 	_, err = p.adminExistsInternal(admin.Username)
 	if err == nil {
-		return fmt.Errorf("admin %q already exists", admin.Username)
+		return util.NewI18nError(
+			fmt.Errorf("%w: admin %q already exists", ErrDuplicatedKey, admin.Username),
+			util.I18nErrorDuplicatedUsername,
+		)
 	}
 	admin.ID = p.getNextAdminID()
 	admin.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
@@ -1041,7 +1047,10 @@ func (p *MemoryProvider) addGroup(group *Group) error {
 
 	_, err := p.groupExistsInternal(group.Name)
 	if err == nil {
-		return fmt.Errorf("group %q already exists", group.Name)
+		return util.NewI18nError(
+			fmt.Errorf("%w: group %q already exists", ErrDuplicatedKey, group.Name),
+			util.I18nErrorDuplicatedUsername,
+		)
 	}
 	group.ID = p.getNextGroupID()
 	group.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
@@ -1512,7 +1521,10 @@ func (p *MemoryProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
 
 	_, err = p.folderExistsInternal(folder.Name)
 	if err == nil {
-		return fmt.Errorf("folder %q already exists", folder.Name)
+		return util.NewI18nError(
+			fmt.Errorf("%w: folder %q already exists", ErrDuplicatedKey, folder.Name),
+			util.I18nErrorDuplicatedUsername,
+		)
 	}
 	folder.ID = p.getNextFolderID()
 	folder.Users = nil
@@ -2172,7 +2184,10 @@ func (p *MemoryProvider) addEventAction(action *BaseEventAction) error {
 	}
 	_, err = p.actionExistsInternal(action.Name)
 	if err == nil {
-		return fmt.Errorf("event action %q already exists", action.Name)
+		return util.NewI18nError(
+			fmt.Errorf("%w: event action %q already exists", ErrDuplicatedKey, action.Name),
+			util.I18nErrorDuplicatedName,
+		)
 	}
 	action.ID = p.getNextActionID()
 	action.Rules = nil
@@ -2348,7 +2363,10 @@ func (p *MemoryProvider) addEventRule(rule *EventRule) error {
 	}
 	_, err := p.ruleExistsInternal(rule.Name)
 	if err == nil {
-		return fmt.Errorf("event rule %q already exists", rule.Name)
+		return util.NewI18nError(
+			fmt.Errorf("%w: event rule %q already exists", ErrDuplicatedKey, rule.Name),
+			util.I18nErrorDuplicatedName,
+		)
 	}
 	rule.ID = p.getNextRuleID()
 	rule.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())
@@ -2499,7 +2517,10 @@ func (p *MemoryProvider) addRole(role *Role) error {
 
 	_, err := p.roleExistsInternal(role.Name)
 	if err == nil {
-		return fmt.Errorf("role %q already exists", role.Name)
+		return util.NewI18nError(
+			fmt.Errorf("%w: role %q already exists", ErrDuplicatedKey, role.Name),
+			util.I18nErrorDuplicatedName,
+		)
 	}
 	role.ID = p.getNextRoleID()
 	role.CreatedAt = util.GetTimeAsMsSinceEpoch(time.Now())

+ 31 - 7
internal/dataprovider/mysql.go

@@ -32,6 +32,7 @@ import (
 	"github.com/go-sql-driver/mysql"
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
+	"github.com/drakkan/sftpgo/v2/internal/util"
 	"github.com/drakkan/sftpgo/v2/internal/version"
 	"github.com/drakkan/sftpgo/v2/internal/vfs"
 )
@@ -341,7 +342,7 @@ func (p *MySQLProvider) userExists(username, role string) (User, error) {
 }
 
 func (p *MySQLProvider) addUser(user *User) error {
-	return sqlCommonAddUser(user, p.dbHandle)
+	return p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername)
 }
 
 func (p *MySQLProvider) updateUser(user *User) error {
@@ -387,7 +388,7 @@ func (p *MySQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, err
 }
 
 func (p *MySQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
-	return sqlCommonAddFolder(folder, p.dbHandle)
+	return p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName)
 }
 
 func (p *MySQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
@@ -423,7 +424,7 @@ func (p *MySQLProvider) groupExists(name string) (Group, error) {
 }
 
 func (p *MySQLProvider) addGroup(group *Group) error {
-	return sqlCommonAddGroup(group, p.dbHandle)
+	return p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName)
 }
 
 func (p *MySQLProvider) updateGroup(group *Group) error {
@@ -443,7 +444,7 @@ func (p *MySQLProvider) adminExists(username string) (Admin, error) {
 }
 
 func (p *MySQLProvider) addAdmin(admin *Admin) error {
-	return sqlCommonAddAdmin(admin, p.dbHandle)
+	return p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername)
 }
 
 func (p *MySQLProvider) updateAdmin(admin *Admin) error {
@@ -603,7 +604,7 @@ func (p *MySQLProvider) eventActionExists(name string) (BaseEventAction, error)
 }
 
 func (p *MySQLProvider) addEventAction(action *BaseEventAction) error {
-	return sqlCommonAddEventAction(action, p.dbHandle)
+	return p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName)
 }
 
 func (p *MySQLProvider) updateEventAction(action *BaseEventAction) error {
@@ -631,7 +632,7 @@ func (p *MySQLProvider) eventRuleExists(name string) (EventRule, error) {
 }
 
 func (p *MySQLProvider) addEventRule(rule *EventRule) error {
-	return sqlCommonAddEventRule(rule, p.dbHandle)
+	return p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName)
 }
 
 func (p *MySQLProvider) updateEventRule(rule *EventRule) error {
@@ -683,7 +684,7 @@ func (p *MySQLProvider) roleExists(name string) (Role, error) {
 }
 
 func (p *MySQLProvider) addRole(role *Role) error {
-	return sqlCommonAddRole(role, p.dbHandle)
+	return p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName)
 }
 
 func (p *MySQLProvider) updateRole(role *Role) error {
@@ -824,3 +825,26 @@ func (p *MySQLProvider) resetDatabase() error {
 	sql := sqlReplaceAll(mysqlResetSQL)
 	return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, strings.Split(sql, ";"), 0, false)
 }
+
+func (p *MySQLProvider) normalizeError(err error, fieldType int) error {
+	if err == nil {
+		return nil
+	}
+	var mysqlErr *mysql.MySQLError
+	if errors.As(err, &mysqlErr) {
+		switch mysqlErr.Number {
+		case 1062:
+			message := util.I18nErrorDuplicatedName
+			if fieldType == fieldUsername {
+				message = util.I18nErrorDuplicatedUsername
+			}
+			return util.NewI18nError(
+				fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()),
+				message,
+			)
+		case 1452:
+			return fmt.Errorf("%w: %s", ErrForeignKeyViolated, err.Error())
+		}
+	}
+	return err
+}

+ 32 - 7
internal/dataprovider/pgsql.go

@@ -29,9 +29,11 @@ import (
 	"time"
 
 	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/pgconn"
 	"github.com/jackc/pgx/v5/stdlib"
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
+	"github.com/drakkan/sftpgo/v2/internal/util"
 	"github.com/drakkan/sftpgo/v2/internal/version"
 	"github.com/drakkan/sftpgo/v2/internal/vfs"
 )
@@ -353,7 +355,7 @@ func (p *PGSQLProvider) userExists(username, role string) (User, error) {
 }
 
 func (p *PGSQLProvider) addUser(user *User) error {
-	return sqlCommonAddUser(user, p.dbHandle)
+	return p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername)
 }
 
 func (p *PGSQLProvider) updateUser(user *User) error {
@@ -399,7 +401,7 @@ func (p *PGSQLProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, err
 }
 
 func (p *PGSQLProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
-	return sqlCommonAddFolder(folder, p.dbHandle)
+	return p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName)
 }
 
 func (p *PGSQLProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
@@ -435,7 +437,7 @@ func (p *PGSQLProvider) groupExists(name string) (Group, error) {
 }
 
 func (p *PGSQLProvider) addGroup(group *Group) error {
-	return sqlCommonAddGroup(group, p.dbHandle)
+	return p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName)
 }
 
 func (p *PGSQLProvider) updateGroup(group *Group) error {
@@ -455,7 +457,7 @@ func (p *PGSQLProvider) adminExists(username string) (Admin, error) {
 }
 
 func (p *PGSQLProvider) addAdmin(admin *Admin) error {
-	return sqlCommonAddAdmin(admin, p.dbHandle)
+	return p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername)
 }
 
 func (p *PGSQLProvider) updateAdmin(admin *Admin) error {
@@ -615,7 +617,7 @@ func (p *PGSQLProvider) eventActionExists(name string) (BaseEventAction, error)
 }
 
 func (p *PGSQLProvider) addEventAction(action *BaseEventAction) error {
-	return sqlCommonAddEventAction(action, p.dbHandle)
+	return p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName)
 }
 
 func (p *PGSQLProvider) updateEventAction(action *BaseEventAction) error {
@@ -643,7 +645,7 @@ func (p *PGSQLProvider) eventRuleExists(name string) (EventRule, error) {
 }
 
 func (p *PGSQLProvider) addEventRule(rule *EventRule) error {
-	return sqlCommonAddEventRule(rule, p.dbHandle)
+	return p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName)
 }
 
 func (p *PGSQLProvider) updateEventRule(rule *EventRule) error {
@@ -695,7 +697,7 @@ func (p *PGSQLProvider) roleExists(name string) (Role, error) {
 }
 
 func (p *PGSQLProvider) addRole(role *Role) error {
-	return sqlCommonAddRole(role, p.dbHandle)
+	return p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName)
 }
 
 func (p *PGSQLProvider) updateRole(role *Role) error {
@@ -842,3 +844,26 @@ func (p *PGSQLProvider) resetDatabase() error {
 	sql := sqlReplaceAll(pgsqlResetSQL)
 	return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
 }
+
+func (p *PGSQLProvider) normalizeError(err error, fieldType int) error {
+	if err == nil {
+		return nil
+	}
+	var pgsqlErr *pgconn.PgError
+	if errors.As(err, &pgsqlErr) {
+		switch pgsqlErr.Code {
+		case "23505":
+			message := util.I18nErrorDuplicatedName
+			if fieldType == fieldUsername {
+				message = util.I18nErrorDuplicatedUsername
+			}
+			return util.NewI18nError(
+				fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()),
+				message,
+			)
+		case "23503":
+			return fmt.Errorf("%w: %s", ErrForeignKeyViolated, err.Error())
+		}
+	}
+	return err
+}

+ 30 - 9
internal/dataprovider/sqlite.go

@@ -26,8 +26,7 @@ import (
 	"path/filepath"
 	"time"
 
-	// we import go-sqlite3 here to be able to disable SQLite support using a build tag
-	_ "github.com/mattn/go-sqlite3"
+	"github.com/mattn/go-sqlite3"
 
 	"github.com/drakkan/sftpgo/v2/internal/logger"
 	"github.com/drakkan/sftpgo/v2/internal/util"
@@ -264,7 +263,7 @@ func (p *SQLiteProvider) userExists(username, role string) (User, error) {
 }
 
 func (p *SQLiteProvider) addUser(user *User) error {
-	return sqlCommonAddUser(user, p.dbHandle)
+	return p.normalizeError(sqlCommonAddUser(user, p.dbHandle), fieldUsername)
 }
 
 func (p *SQLiteProvider) updateUser(user *User) error {
@@ -310,7 +309,7 @@ func (p *SQLiteProvider) getFolderByName(name string) (vfs.BaseVirtualFolder, er
 }
 
 func (p *SQLiteProvider) addFolder(folder *vfs.BaseVirtualFolder) error {
-	return sqlCommonAddFolder(folder, p.dbHandle)
+	return p.normalizeError(sqlCommonAddFolder(folder, p.dbHandle), fieldName)
 }
 
 func (p *SQLiteProvider) updateFolder(folder *vfs.BaseVirtualFolder) error {
@@ -346,7 +345,7 @@ func (p *SQLiteProvider) groupExists(name string) (Group, error) {
 }
 
 func (p *SQLiteProvider) addGroup(group *Group) error {
-	return sqlCommonAddGroup(group, p.dbHandle)
+	return p.normalizeError(sqlCommonAddGroup(group, p.dbHandle), fieldName)
 }
 
 func (p *SQLiteProvider) updateGroup(group *Group) error {
@@ -366,7 +365,7 @@ func (p *SQLiteProvider) adminExists(username string) (Admin, error) {
 }
 
 func (p *SQLiteProvider) addAdmin(admin *Admin) error {
-	return sqlCommonAddAdmin(admin, p.dbHandle)
+	return p.normalizeError(sqlCommonAddAdmin(admin, p.dbHandle), fieldUsername)
 }
 
 func (p *SQLiteProvider) updateAdmin(admin *Admin) error {
@@ -526,7 +525,7 @@ func (p *SQLiteProvider) eventActionExists(name string) (BaseEventAction, error)
 }
 
 func (p *SQLiteProvider) addEventAction(action *BaseEventAction) error {
-	return sqlCommonAddEventAction(action, p.dbHandle)
+	return p.normalizeError(sqlCommonAddEventAction(action, p.dbHandle), fieldName)
 }
 
 func (p *SQLiteProvider) updateEventAction(action *BaseEventAction) error {
@@ -554,7 +553,7 @@ func (p *SQLiteProvider) eventRuleExists(name string) (EventRule, error) {
 }
 
 func (p *SQLiteProvider) addEventRule(rule *EventRule) error {
-	return sqlCommonAddEventRule(rule, p.dbHandle)
+	return p.normalizeError(sqlCommonAddEventRule(rule, p.dbHandle), fieldName)
 }
 
 func (p *SQLiteProvider) updateEventRule(rule *EventRule) error {
@@ -606,7 +605,7 @@ func (p *SQLiteProvider) roleExists(name string) (Role, error) {
 }
 
 func (p *SQLiteProvider) addRole(role *Role) error {
-	return sqlCommonAddRole(role, p.dbHandle)
+	return p.normalizeError(sqlCommonAddRole(role, p.dbHandle), fieldName)
 }
 
 func (p *SQLiteProvider) updateRole(role *Role) error {
@@ -747,6 +746,28 @@ func (p *SQLiteProvider) resetDatabase() error {
 	return sqlCommonExecSQLAndUpdateDBVersion(p.dbHandle, []string{sql}, 0, false)
 }
 
+func (p *SQLiteProvider) normalizeError(err error, fieldType int) error {
+	if err == nil {
+		return nil
+	}
+	if e, ok := err.(sqlite3.Error); ok {
+		switch e.ExtendedCode {
+		case 1555, 2067:
+			message := util.I18nErrorDuplicatedName
+			if fieldType == fieldUsername {
+				message = util.I18nErrorDuplicatedUsername
+			}
+			return util.NewI18nError(
+				fmt.Errorf("%w: %s", ErrDuplicatedKey, err.Error()),
+				message,
+			)
+		case 787:
+			return fmt.Errorf("%w: %s", ErrForeignKeyViolated, err.Error())
+		}
+	}
+	return err
+}
+
 /*func setPragmaFK(dbHandle *sql.DB, value string) error {
 	ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
 	defer cancel()

+ 3 - 0
internal/httpd/api_utils.go

@@ -105,6 +105,9 @@ func getRespStatus(err error) int {
 	if errors.Is(err, plugin.ErrNoSearcher) || errors.Is(err, dataprovider.ErrNotImplemented) {
 		return http.StatusNotImplemented
 	}
+	if errors.Is(err, dataprovider.ErrDuplicatedKey) || errors.Is(err, dataprovider.ErrForeignKeyViolated) {
+		return http.StatusConflict
+	}
 	return http.StatusInternalServerError
 }
 

+ 2 - 0
internal/httpd/httpd_test.go

@@ -600,6 +600,8 @@ func TestBasicUserHandling(t *testing.T) {
 	u.Email = "[email protected]"
 	user, resp, err := httpdtest.AddUser(u, http.StatusCreated)
 	assert.NoError(t, err, string(resp))
+	_, resp, err = httpdtest.AddUser(u, http.StatusConflict)
+	assert.NoError(t, err, string(resp))
 	lastPwdChange := user.LastPasswordChange
 	assert.Greater(t, lastPwdChange, int64(0))
 	user.MaxSessions = 10

+ 2 - 0
internal/util/i18n.go

@@ -189,6 +189,8 @@ const (
 	I18nAddFolderTitle                 = "title.add_folder"
 	I18nUpdateFolderTitle              = "title.update_folder"
 	I18nTemplateFolderTitle            = "title.template_folder"
+	I18nErrorDuplicatedUsername        = "general.duplicated_username"
+	I18nErrorDuplicatedName            = "general.duplicated_name"
 )
 
 // NewI18nError returns a I18nError wrappring the provided error

+ 3 - 1
static/locales/en/translation.json

@@ -211,7 +211,9 @@
         "mandatory_encryption": "Mandatory encryption",
         "name_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
         "associations": "Associations",
-        "template_placeholders": "The following placeholders are supported"
+        "template_placeholders": "The following placeholders are supported",
+        "duplicated_username": "The specified username already exists",
+        "duplicated_name": "The specified name already exists"
     },
     "fs": {
         "view_file": "View file \"{{- path}}\"",

+ 3 - 1
static/locales/it/translation.json

@@ -211,7 +211,9 @@
         "mandatory_encryption": "Crittografia obbligatoria",
         "name_invalid": "Il nome specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~",
         "associations": "Associazioni",
-        "template_placeholders": "Sono supportati i seguenti segnaposto"
+        "template_placeholders": "Sono supportati i seguenti segnaposto",
+        "duplicated_username": "Il nome utente specificato esiste già",
+        "duplicated_name": "Il nome specificato esiste già"
     },
     "fs": {
         "view_file": "Visualizza file \"{{- path}}\"",