feat!: replace custom claims with custom fields

This commit is contained in:
Elias Schneider
2026-05-23 16:07:37 +02:00
parent 5db570bf66
commit 3428fb35d7
68 changed files with 3193 additions and 1016 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ type AppConfigUpdateDto struct {
AllowOwnAccountEdit string `json:"allowOwnAccountEdit" binding:"required"`
AllowUserSignups string `json:"allowUserSignups" binding:"required,oneof=disabled withToken open"`
SignupDefaultUserGroupIDs string `json:"signupDefaultUserGroupIDs" binding:"omitempty,json"`
SignupDefaultCustomClaims string `json:"signupDefaultCustomClaims" binding:"omitempty,json"`
CustomFields string `json:"customFields" binding:"omitempty,json"`
AccentColor string `json:"accentColor"`
RequireUserEmail string `json:"requireUserEmail" binding:"required"`
SmtpHost string `json:"smtpHost"`
-11
View File
@@ -1,11 +0,0 @@
package dto
type CustomClaimDto struct {
Key string `json:"key"`
Value string `json:"value"`
}
type CustomClaimCreateDto struct {
Key string `json:"key" binding:"required" unorm:"nfc"`
Value string `json:"value" binding:"required" unorm:"nfc"`
}
+42
View File
@@ -0,0 +1,42 @@
package dto
type CustomFieldValueDto struct {
CustomFieldID string `json:"customFieldId"`
Key string `json:"key,omitempty"`
Value string `json:"value"`
}
type CustomFieldValueCreateDto struct {
CustomFieldID string `json:"customFieldId" binding:"required_without=Key" unorm:"nfc"`
Key string `json:"key,omitempty" unorm:"nfc"`
Value string `json:"value" unorm:"nfc"`
}
type CustomFieldType string
const (
CustomFieldTypeString CustomFieldType = "string"
CustomFieldTypeNumber CustomFieldType = "number"
CustomFieldTypeBoolean CustomFieldType = "boolean"
)
type CustomFieldTarget string
const (
CustomFieldTargetUser CustomFieldTarget = "user"
CustomFieldTargetGroup CustomFieldTarget = "group"
CustomFieldTargetBoth CustomFieldTarget = "both"
)
type CustomFieldDto struct {
ID string `json:"id" binding:"required,uuid"`
Key string `json:"key" binding:"required" unorm:"nfc"`
DisplayName string `json:"displayName" binding:"required" unorm:"nfc"`
Type CustomFieldType `json:"type" binding:"required,oneof=string number boolean"`
Target CustomFieldTarget `json:"target" binding:"required,oneof=user group both"`
Required bool `json:"required"`
UserEditable bool `json:"userEditable"`
DefaultValue string `json:"defaultValue" unorm:"nfc"`
ValidationRegex string `json:"validationRegex" binding:"regex" unorm:"nfc"`
ValidationErrorMessage string `json:"validationErrorMessage" unorm:"nfc"`
}
+6 -5
View File
@@ -1,9 +1,10 @@
package dto
type SignUpDto struct {
Username string `json:"username" binding:"required,username,min=1,max=50" unorm:"nfc"`
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
Token string `json:"token"`
Username string `json:"username" binding:"required,username,min=1,max=50" unorm:"nfc"`
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
Token string `json:"token"`
CustomFieldValues []CustomFieldValueCreateDto `json:"customFieldValues"`
}
+25 -24
View File
@@ -7,33 +7,34 @@ import (
)
type UserDto struct {
ID string `json:"id"`
Username string `json:"username"`
Email *string `json:"email"`
EmailVerified bool `json:"emailVerified"`
FirstName string `json:"firstName"`
LastName *string `json:"lastName"`
DisplayName string `json:"displayName"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
CustomClaims []CustomClaimDto `json:"customClaims"`
UserGroups []UserGroupMinimalDto `json:"userGroups"`
LdapID *string `json:"ldapId"`
Disabled bool `json:"disabled"`
ID string `json:"id"`
Username string `json:"username"`
Email *string `json:"email"`
EmailVerified bool `json:"emailVerified"`
FirstName string `json:"firstName"`
LastName *string `json:"lastName"`
DisplayName string `json:"displayName"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
CustomFieldValues []CustomFieldValueDto `json:"customFieldValues"`
UserGroups []UserGroupMinimalDto `json:"userGroups"`
LdapID *string `json:"ldapId"`
Disabled bool `json:"disabled"`
}
type UserCreateDto struct {
Username string `json:"username" binding:"required,username,min=1,max=50" unorm:"nfc"`
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
EmailVerified bool `json:"emailVerified"`
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
DisplayName string `json:"displayName" binding:"max=100" unorm:"nfc"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
Disabled bool `json:"disabled"`
UserGroupIds []string `json:"userGroupIds"`
LdapID string `json:"-"`
Username string `json:"username" binding:"required,username,min=1,max=50" unorm:"nfc"`
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
EmailVerified bool `json:"emailVerified"`
FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
DisplayName string `json:"displayName" binding:"max=100" unorm:"nfc"`
IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"`
Disabled bool `json:"disabled"`
UserGroupIds []string `json:"userGroupIds"`
CustomFieldValues []CustomFieldValueCreateDto `json:"customFieldValues"`
LdapID string `json:"-"`
}
func (u UserCreateDto) Validate() error {
+5 -5
View File
@@ -11,7 +11,7 @@ type UserGroupDto struct {
ID string `json:"id"`
FriendlyName string `json:"friendlyName"`
Name string `json:"name"`
CustomClaims []CustomClaimDto `json:"customClaims"`
CustomFieldValues []CustomFieldValueDto `json:"customFieldValues"`
LdapID *string `json:"ldapId"`
CreatedAt datatype.DateTime `json:"createdAt"`
Users []UserDto `json:"users"`
@@ -22,7 +22,6 @@ type UserGroupMinimalDto struct {
ID string `json:"id"`
FriendlyName string `json:"friendlyName"`
Name string `json:"name"`
CustomClaims []CustomClaimDto `json:"customClaims"`
UserCount int64 `json:"userCount"`
LdapID *string `json:"ldapId"`
CreatedAt datatype.DateTime `json:"createdAt"`
@@ -33,9 +32,10 @@ type UserGroupUpdateAllowedOidcClientsDto struct {
}
type UserGroupCreateDto struct {
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50" unorm:"nfc"`
Name string `json:"name" binding:"required,min=2,max=255" unorm:"nfc"`
LdapID string `json:"-"`
FriendlyName string `json:"friendlyName" binding:"required,min=2,max=50" unorm:"nfc"`
Name string `json:"name" binding:"required,min=2,max=255" unorm:"nfc"`
CustomFieldValues []CustomFieldValueCreateDto `json:"customFieldValues"`
LdapID string `json:"-"`
}
func (g UserGroupCreateDto) Validate() error {
+32
View File
@@ -1,11 +1,13 @@
package dto
import (
"errors"
"net/url"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/pocket-id/pocket-id/backend/internal/utils"
"github.com/gin-gonic/gin/binding"
@@ -33,6 +35,12 @@ func init() {
"client_id": func(fl validator.FieldLevel) bool {
return ValidateClientID(fl.Field().String())
},
"regex": func(fl validator.FieldLevel) bool {
return ValidateRegex(fl.Field().String())
},
"uuid": func(fl validator.FieldLevel) bool {
return ValidateUUID(fl.Field().String())
},
"ttl": func(fl validator.FieldLevel) bool {
ttl, ok := fl.Field().Interface().(utils.JSONDuration)
if !ok {
@@ -59,6 +67,16 @@ func init() {
}
}
func ValidateStruct(input any) error {
e, ok := binding.Validator.Engine().(interface {
Struct(any) error
})
if !ok {
return errors.New("validator does not implement the expected interface")
}
return e.Struct(input)
}
// ValidateUsername validates username inputs
func ValidateUsername(username string) bool {
return validateUsernameRegex.MatchString(username)
@@ -69,6 +87,20 @@ func ValidateClientID(clientID string) bool {
return validateClientIDRegex.MatchString(clientID)
}
// ValidateRegex validates that the input is either empty or a compilable regular expression.
func ValidateRegex(value string) bool {
if value == "" {
return true
}
_, err := regexp.Compile(value)
return err == nil
}
// ValidateUUID validates UUID inputs.
func ValidateUUID(value string) bool {
return uuid.Validate(value) == nil
}
// ValidateCallbackURL validates the input callback URL
func ValidateCallbackURL(str string) bool {
// Ensure the URL is a valid one and that the protocol is not "javascript:" or "data:"
+36
View File
@@ -58,6 +58,42 @@ func TestValidateClientID(t *testing.T) {
}
}
func TestValidateRegex(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{"empty", "", true},
{"valid", "^EMP-[0-9]+$", true},
{"invalid", "[", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, ValidateRegex(tt.input))
})
}
}
func TestValidateUUID(t *testing.T) {
tests := []struct {
name string
input string
expected bool
}{
{"valid", "89bc9c8f-2cd8-4cfd-82c5-5fa14e874f03", true},
{"invalid", "field-1", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, ValidateUUID(tt.input))
})
}
}
func TestValidateResponseMode(t *testing.T) {
tests := []struct {
name string