mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-01-09 22:28:25 -05:00
feat: add user display name field (#898)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -41,6 +41,7 @@ type AppConfigUpdateDto struct {
|
|||||||
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
LdapAttributeUserEmail string `json:"ldapAttributeUserEmail"`
|
||||||
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
LdapAttributeUserFirstName string `json:"ldapAttributeUserFirstName"`
|
||||||
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
LdapAttributeUserLastName string `json:"ldapAttributeUserLastName"`
|
||||||
|
LdapAttributeUserDisplayName string `json:"ldapAttributeUserDisplayName"`
|
||||||
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
LdapAttributeUserProfilePicture string `json:"ldapAttributeUserProfilePicture"`
|
||||||
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
LdapAttributeGroupMember string `json:"ldapAttributeGroupMember"`
|
||||||
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ type UserDto struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Email string `json:"email" `
|
Email string `json:"email" `
|
||||||
FirstName string `json:"firstName"`
|
FirstName string `json:"firstName"`
|
||||||
LastName string `json:"lastName"`
|
LastName *string `json:"lastName"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
Locale *string `json:"locale"`
|
Locale *string `json:"locale"`
|
||||||
CustomClaims []CustomClaimDto `json:"customClaims"`
|
CustomClaims []CustomClaimDto `json:"customClaims"`
|
||||||
@@ -22,14 +23,15 @@ type UserDto struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserCreateDto struct {
|
type UserCreateDto struct {
|
||||||
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"`
|
||||||
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
Email string `json:"email" binding:"required,email" unorm:"nfc"`
|
||||||
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"`
|
||||||
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
|
||||||
IsAdmin bool `json:"isAdmin"`
|
DisplayName string `json:"displayName" binding:"required,max=100" unorm:"nfc"`
|
||||||
Locale *string `json:"locale"`
|
IsAdmin bool `json:"isAdmin"`
|
||||||
Disabled bool `json:"disabled"`
|
Locale *string `json:"locale"`
|
||||||
LdapID string `json:"-"`
|
Disabled bool `json:"disabled"`
|
||||||
|
LdapID string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u UserCreateDto) Validate() error {
|
func (u UserCreateDto) Validate() error {
|
||||||
|
|||||||
@@ -15,59 +15,74 @@ func TestUserCreateDto_Validate(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "valid input",
|
name: "valid input",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
|
DisplayName: "John Doe",
|
||||||
},
|
},
|
||||||
wantErr: "",
|
wantErr: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing username",
|
name: "missing username",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
|
DisplayName: "John Doe",
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'Username' failed on the 'required' tag",
|
wantErr: "Field validation for 'Username' failed on the 'required' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "username contains invalid characters",
|
name: "missing display name",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "test/ser",
|
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
},
|
},
|
||||||
|
wantErr: "Field validation for 'DisplayName' failed on the 'required' tag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "username contains invalid characters",
|
||||||
|
input: UserCreateDto{
|
||||||
|
Username: "test/ser",
|
||||||
|
Email: "test@example.com",
|
||||||
|
FirstName: "John",
|
||||||
|
LastName: "Doe",
|
||||||
|
DisplayName: "John Doe",
|
||||||
|
},
|
||||||
wantErr: "Field validation for 'Username' failed on the 'username' tag",
|
wantErr: "Field validation for 'Username' failed on the 'username' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid email",
|
name: "invalid email",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "not-an-email",
|
Email: "not-an-email",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
|
DisplayName: "John Doe",
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'Email' failed on the 'email' tag",
|
wantErr: "Field validation for 'Email' failed on the 'email' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "first name too short",
|
name: "first name too short",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "",
|
FirstName: "",
|
||||||
LastName: "Doe",
|
LastName: "Doe",
|
||||||
|
DisplayName: "John Doe",
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'FirstName' failed on the 'required' tag",
|
wantErr: "Field validation for 'FirstName' failed on the 'required' tag",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "last name too long",
|
name: "last name too long",
|
||||||
input: UserCreateDto{
|
input: UserCreateDto{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
FirstName: "John",
|
FirstName: "John",
|
||||||
LastName: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
|
LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
|
||||||
|
DisplayName: "John Doe",
|
||||||
},
|
},
|
||||||
wantErr: "Field validation for 'LastName' failed on the 'max' tag",
|
wantErr: "Field validation for 'LastName' failed on the 'max' tag",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type AppConfig struct {
|
|||||||
LdapAttributeUserEmail AppConfigVariable `key:"ldapAttributeUserEmail"`
|
LdapAttributeUserEmail AppConfigVariable `key:"ldapAttributeUserEmail"`
|
||||||
LdapAttributeUserFirstName AppConfigVariable `key:"ldapAttributeUserFirstName"`
|
LdapAttributeUserFirstName AppConfigVariable `key:"ldapAttributeUserFirstName"`
|
||||||
LdapAttributeUserLastName AppConfigVariable `key:"ldapAttributeUserLastName"`
|
LdapAttributeUserLastName AppConfigVariable `key:"ldapAttributeUserLastName"`
|
||||||
|
LdapAttributeUserDisplayName AppConfigVariable `key:"ldapAttributeUserDisplayName"`
|
||||||
LdapAttributeUserProfilePicture AppConfigVariable `key:"ldapAttributeUserProfilePicture"`
|
LdapAttributeUserProfilePicture AppConfigVariable `key:"ldapAttributeUserProfilePicture"`
|
||||||
LdapAttributeGroupMember AppConfigVariable `key:"ldapAttributeGroupMember"`
|
LdapAttributeGroupMember AppConfigVariable `key:"ldapAttributeGroupMember"`
|
||||||
LdapAttributeGroupUniqueIdentifier AppConfigVariable `key:"ldapAttributeGroupUniqueIdentifier"`
|
LdapAttributeGroupUniqueIdentifier AppConfigVariable `key:"ldapAttributeGroupUniqueIdentifier"`
|
||||||
|
|||||||
@@ -13,14 +13,15 @@ import (
|
|||||||
type User struct {
|
type User struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
Username string `sortable:"true"`
|
Username string `sortable:"true"`
|
||||||
Email string `sortable:"true"`
|
Email string `sortable:"true"`
|
||||||
FirstName string `sortable:"true"`
|
FirstName string `sortable:"true"`
|
||||||
LastName string `sortable:"true"`
|
LastName string `sortable:"true"`
|
||||||
IsAdmin bool `sortable:"true"`
|
DisplayName string `sortable:"true"`
|
||||||
Locale *string
|
IsAdmin bool `sortable:"true"`
|
||||||
LdapID *string
|
Locale *string
|
||||||
Disabled bool `sortable:"true"`
|
LdapID *string
|
||||||
|
Disabled bool `sortable:"true"`
|
||||||
|
|
||||||
CustomClaims []CustomClaim
|
CustomClaims []CustomClaim
|
||||||
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
UserGroups []UserGroup `gorm:"many2many:user_groups_users;"`
|
||||||
@@ -31,7 +32,12 @@ func (u User) WebAuthnID() []byte { return []byte(u.ID) }
|
|||||||
|
|
||||||
func (u User) WebAuthnName() string { return u.Username }
|
func (u User) WebAuthnName() string { return u.Username }
|
||||||
|
|
||||||
func (u User) WebAuthnDisplayName() string { return u.FirstName + " " + u.LastName }
|
func (u User) WebAuthnDisplayName() string {
|
||||||
|
if u.DisplayName != "" {
|
||||||
|
return u.DisplayName
|
||||||
|
}
|
||||||
|
return u.FirstName + " " + u.LastName
|
||||||
|
}
|
||||||
|
|
||||||
func (u User) WebAuthnIcon() string { return "" }
|
func (u User) WebAuthnIcon() string { return "" }
|
||||||
|
|
||||||
@@ -66,7 +72,9 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential
|
|||||||
return descriptors
|
return descriptors
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) FullName() string { return u.FirstName + " " + u.LastName }
|
func (u User) FullName() string {
|
||||||
|
return u.FirstName + " " + u.LastName
|
||||||
|
}
|
||||||
|
|
||||||
func (u User) Initials() string {
|
func (u User) Initials() string {
|
||||||
first := utils.GetFirstCharacter(u.FirstName)
|
first := utils.GetFirstCharacter(u.FirstName)
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig {
|
|||||||
LdapAttributeUserEmail: model.AppConfigVariable{},
|
LdapAttributeUserEmail: model.AppConfigVariable{},
|
||||||
LdapAttributeUserFirstName: model.AppConfigVariable{},
|
LdapAttributeUserFirstName: model.AppConfigVariable{},
|
||||||
LdapAttributeUserLastName: model.AppConfigVariable{},
|
LdapAttributeUserLastName: model.AppConfigVariable{},
|
||||||
|
LdapAttributeUserDisplayName: model.AppConfigVariable{Value: "cn"},
|
||||||
LdapAttributeUserProfilePicture: model.AppConfigVariable{},
|
LdapAttributeUserProfilePicture: model.AppConfigVariable{},
|
||||||
LdapAttributeGroupMember: model.AppConfigVariable{Value: "member"},
|
LdapAttributeGroupMember: model.AppConfigVariable{Value: "member"},
|
||||||
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{},
|
LdapAttributeGroupUniqueIdentifier: model.AppConfigVariable{},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func isReservedClaim(key string) bool {
|
|||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
"preferred_username",
|
"preferred_username",
|
||||||
|
"display_name",
|
||||||
"groups",
|
"groups",
|
||||||
TokenTypeClaim,
|
TokenTypeClaim,
|
||||||
"sub",
|
"sub",
|
||||||
|
|||||||
@@ -78,21 +78,23 @@ func (s *TestService) SeedDatabase(baseURL string) error {
|
|||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
|
ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e",
|
||||||
},
|
},
|
||||||
Username: "tim",
|
Username: "tim",
|
||||||
Email: "tim.cook@test.com",
|
Email: "tim.cook@test.com",
|
||||||
FirstName: "Tim",
|
FirstName: "Tim",
|
||||||
LastName: "Cook",
|
LastName: "Cook",
|
||||||
IsAdmin: true,
|
DisplayName: "Tim Cook",
|
||||||
|
IsAdmin: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Base: model.Base{
|
Base: model.Base{
|
||||||
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
|
ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036",
|
||||||
},
|
},
|
||||||
Username: "craig",
|
Username: "craig",
|
||||||
Email: "craig.federighi@test.com",
|
Email: "craig.federighi@test.com",
|
||||||
FirstName: "Craig",
|
FirstName: "Craig",
|
||||||
LastName: "Federighi",
|
LastName: "Federighi",
|
||||||
IsAdmin: false,
|
DisplayName: "Craig Federighi",
|
||||||
|
IsAdmin: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
dbConfig.LdapAttributeUserFirstName.Value,
|
dbConfig.LdapAttributeUserFirstName.Value,
|
||||||
dbConfig.LdapAttributeUserLastName.Value,
|
dbConfig.LdapAttributeUserLastName.Value,
|
||||||
dbConfig.LdapAttributeUserProfilePicture.Value,
|
dbConfig.LdapAttributeUserProfilePicture.Value,
|
||||||
|
dbConfig.LdapAttributeUserDisplayName.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filters must start and finish with ()!
|
// Filters must start and finish with ()!
|
||||||
@@ -346,12 +347,13 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
newUser := dto.UserCreateDto{
|
newUser := dto.UserCreateDto{
|
||||||
Username: value.GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value),
|
Username: value.GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value),
|
||||||
Email: value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value),
|
Email: value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value),
|
||||||
FirstName: value.GetAttributeValue(dbConfig.LdapAttributeUserFirstName.Value),
|
FirstName: value.GetAttributeValue(dbConfig.LdapAttributeUserFirstName.Value),
|
||||||
LastName: value.GetAttributeValue(dbConfig.LdapAttributeUserLastName.Value),
|
LastName: value.GetAttributeValue(dbConfig.LdapAttributeUserLastName.Value),
|
||||||
IsAdmin: isAdmin,
|
DisplayName: value.GetAttributeValue(dbConfig.LdapAttributeUserDisplayName.Value),
|
||||||
LdapID: ldapId,
|
IsAdmin: isAdmin,
|
||||||
|
LdapID: ldapId,
|
||||||
}
|
}
|
||||||
dto.Normalize(newUser)
|
dto.Normalize(newUser)
|
||||||
|
|
||||||
|
|||||||
@@ -1838,13 +1838,6 @@ func (s *OidcService) getUserClaims(ctx context.Context, user *model.User, scope
|
|||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(scopes, "profile") {
|
if slices.Contains(scopes, "profile") {
|
||||||
// Add profile claims
|
|
||||||
claims["given_name"] = user.FirstName
|
|
||||||
claims["family_name"] = user.LastName
|
|
||||||
claims["name"] = user.FullName()
|
|
||||||
claims["preferred_username"] = user.Username
|
|
||||||
claims["picture"] = common.EnvConfig.AppURL + "/api/users/" + user.ID + "/profile-picture.png"
|
|
||||||
|
|
||||||
// Add custom claims
|
// Add custom claims
|
||||||
customClaims, err := s.customClaimService.GetCustomClaimsForUserWithUserGroups(ctx, user.ID, tx)
|
customClaims, err := s.customClaimService.GetCustomClaimsForUserWithUserGroups(ctx, user.ID, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1863,6 +1856,15 @@ func (s *OidcService) getUserClaims(ctx context.Context, user *model.User, scope
|
|||||||
claims[customClaim.Key] = customClaim.Value
|
claims[customClaim.Key] = customClaim.Value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add profile claims
|
||||||
|
claims["given_name"] = user.FirstName
|
||||||
|
claims["family_name"] = user.LastName
|
||||||
|
claims["name"] = user.FullName()
|
||||||
|
claims["display_name"] = user.DisplayName
|
||||||
|
|
||||||
|
claims["preferred_username"] = user.Username
|
||||||
|
claims["picture"] = common.EnvConfig.AppURL + "/api/users/" + user.ID + "/profile-picture.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
if slices.Contains(scopes, "email") {
|
if slices.Contains(scopes, "email") {
|
||||||
|
|||||||
@@ -245,12 +245,13 @@ func (s *UserService) CreateUser(ctx context.Context, input dto.UserCreateDto) (
|
|||||||
|
|
||||||
func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error) {
|
||||||
user := model.User{
|
user := model.User{
|
||||||
FirstName: input.FirstName,
|
FirstName: input.FirstName,
|
||||||
LastName: input.LastName,
|
LastName: input.LastName,
|
||||||
Email: input.Email,
|
DisplayName: input.DisplayName,
|
||||||
Username: input.Username,
|
Email: input.Email,
|
||||||
IsAdmin: input.IsAdmin,
|
Username: input.Username,
|
||||||
Locale: input.Locale,
|
IsAdmin: input.IsAdmin,
|
||||||
|
Locale: input.Locale,
|
||||||
}
|
}
|
||||||
if input.LdapID != "" {
|
if input.LdapID != "" {
|
||||||
user.LdapID = &input.LdapID
|
user.LdapID = &input.LdapID
|
||||||
@@ -362,6 +363,7 @@ func (s *UserService) updateUserInternal(ctx context.Context, userID string, upd
|
|||||||
// Full update: Allow updating all personal fields
|
// Full update: Allow updating all personal fields
|
||||||
user.FirstName = updatedUser.FirstName
|
user.FirstName = updatedUser.FirstName
|
||||||
user.LastName = updatedUser.LastName
|
user.LastName = updatedUser.LastName
|
||||||
|
user.DisplayName = updatedUser.DisplayName
|
||||||
user.Email = updatedUser.Email
|
user.Email = updatedUser.Email
|
||||||
user.Username = updatedUser.Username
|
user.Username = updatedUser.Username
|
||||||
user.Locale = updatedUser.Locale
|
user.Locale = updatedUser.Locale
|
||||||
@@ -600,11 +602,12 @@ func (s *UserService) SignUpInitialAdmin(ctx context.Context, signUpData dto.Sig
|
|||||||
}
|
}
|
||||||
|
|
||||||
userToCreate := dto.UserCreateDto{
|
userToCreate := dto.UserCreateDto{
|
||||||
FirstName: signUpData.FirstName,
|
FirstName: signUpData.FirstName,
|
||||||
LastName: signUpData.LastName,
|
LastName: signUpData.LastName,
|
||||||
Username: signUpData.Username,
|
DisplayName: strings.TrimSpace(signUpData.FirstName + " " + signUpData.LastName),
|
||||||
Email: signUpData.Email,
|
Username: signUpData.Username,
|
||||||
IsAdmin: true,
|
Email: signUpData.Email,
|
||||||
|
IsAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
||||||
@@ -736,10 +739,11 @@ func (s *UserService) SignUp(ctx context.Context, signupData dto.SignUpDto, ipAd
|
|||||||
}
|
}
|
||||||
|
|
||||||
userToCreate := dto.UserCreateDto{
|
userToCreate := dto.UserCreateDto{
|
||||||
Username: signupData.Username,
|
Username: signupData.Username,
|
||||||
Email: signupData.Email,
|
Email: signupData.Email,
|
||||||
FirstName: signupData.FirstName,
|
FirstName: signupData.FirstName,
|
||||||
LastName: signupData.LastName,
|
LastName: signupData.LastName,
|
||||||
|
DisplayName: strings.TrimSpace(signupData.FirstName + " " + signupData.LastName),
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
user, err := s.createUserInternal(ctx, userToCreate, false, tx)
|
||||||
|
|||||||
@@ -3,3 +3,11 @@ package utils
|
|||||||
func Ptr[T any](v T) *T {
|
func Ptr[T any](v T) *T {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PtrValueOrZero[T any](ptr *T) T {
|
||||||
|
if ptr == nil {
|
||||||
|
var zero T
|
||||||
|
return zero
|
||||||
|
}
|
||||||
|
return *ptr
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users DROP COLUMN display_name;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN display_name TEXT;
|
||||||
|
|
||||||
|
UPDATE users
|
||||||
|
SET display_name = trim(coalesce(first_name,'') || ' ' || coalesce(last_name,''));
|
||||||
|
|
||||||
|
ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
BEGIN;
|
||||||
|
ALTER TABLE users DROP COLUMN display_name;
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
PRAGMA foreign_keys = OFF;
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE users_new
|
||||||
|
(
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_at DATETIME,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
first_name TEXT,
|
||||||
|
last_name TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
is_admin NUMERIC NOT NULL DEFAULT FALSE,
|
||||||
|
ldap_id TEXT,
|
||||||
|
locale TEXT,
|
||||||
|
disabled NUMERIC NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO users_new (id, created_at, username, email, first_name, last_name, display_name, is_admin, ldap_id, locale,
|
||||||
|
disabled)
|
||||||
|
SELECT id,
|
||||||
|
created_at,
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
first_name,
|
||||||
|
COALESCE(last_name, ''),
|
||||||
|
TRIM(COALESCE(first_name, '') || ' ' || COALESCE(last_name, '')),
|
||||||
|
is_admin,
|
||||||
|
ldap_id,
|
||||||
|
locale,
|
||||||
|
disabled
|
||||||
|
FROM users;
|
||||||
|
|
||||||
|
DROP TABLE users;
|
||||||
|
|
||||||
|
ALTER TABLE users_new
|
||||||
|
RENAME TO users;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
@@ -443,7 +443,10 @@
|
|||||||
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
"custom_client_id_description": "Set a custom client ID if this is required by your application. Otherwise, leave it blank to generate a random one.",
|
||||||
"generated": "Generated",
|
"generated": "Generated",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN). Recommended value: `cn`",
|
"group_rdn_attribute_description": "The attribute used in the groups distinguished name (DN).",
|
||||||
|
"display_name_attribute": "Display Name Attribute",
|
||||||
|
"display_name": "Display Name",
|
||||||
|
"configure_application_images": "Configure Application Images",
|
||||||
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
"ui_config_disabled_info_title": "UI Configuration Disabled",
|
||||||
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable."
|
"ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
import { tryCatch } from '$lib/utils/try-catch-util';
|
import { tryCatch } from '$lib/utils/try-catch-util';
|
||||||
|
import { emptyToUndefined } from '$lib/utils/zod-util';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: z.string().max(50).optional(),
|
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(2)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type AllAppConfig = AppConfig & {
|
|||||||
ldapAttributeUserEmail: string;
|
ldapAttributeUserEmail: string;
|
||||||
ldapAttributeUserFirstName: string;
|
ldapAttributeUserFirstName: string;
|
||||||
ldapAttributeUserLastName: string;
|
ldapAttributeUserLastName: string;
|
||||||
|
ldapAttributeUserDisplayName: string;
|
||||||
ldapAttributeUserProfilePicture: string;
|
ldapAttributeUserProfilePicture: string;
|
||||||
ldapAttributeGroupMember: string;
|
ldapAttributeGroupMember: string;
|
||||||
ldapAttributeGroupUniqueIdentifier: string;
|
ldapAttributeGroupUniqueIdentifier: string;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type User = {
|
|||||||
email: string;
|
email: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName?: string;
|
lastName?: string;
|
||||||
|
displayName: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
userGroups: UserGroup[];
|
userGroups: UserGroup[];
|
||||||
customClaims: CustomClaim[];
|
customClaims: CustomClaim[];
|
||||||
@@ -18,6 +19,6 @@ export type User = {
|
|||||||
|
|
||||||
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
export type UserCreate = Omit<User, 'id' | 'customClaims' | 'ldapId' | 'userGroups'>;
|
||||||
|
|
||||||
export type UserSignUp = Omit<UserCreate, 'isAdmin' | 'disabled'> & {
|
export type UserSignUp = Omit<UserCreate, 'isAdmin' | 'disabled' | 'displayName'> & {
|
||||||
token?: string;
|
token?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime';
|
import {
|
||||||
|
extractLocaleFromCookie,
|
||||||
|
setLocale as setParaglideLocale,
|
||||||
|
type Locale
|
||||||
|
} from '$lib/paraglide/runtime';
|
||||||
import { setDefaultOptions } from 'date-fns';
|
import { setDefaultOptions } from 'date-fns';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
export async function setLocale(locale: Locale, reload = true) {
|
export async function setLocale(locale: Locale, reload = true) {
|
||||||
|
await setLocaleForLibraries(locale);
|
||||||
|
setParaglideLocale(locale, { reload });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setLocaleForLibraries(
|
||||||
|
locale: Locale = (extractLocaleFromCookie() as Locale) || 'en'
|
||||||
|
) {
|
||||||
const [zodResult, dateFnsResult] = await Promise.allSettled([
|
const [zodResult, dateFnsResult] = await Promise.allSettled([
|
||||||
import(`../../../node_modules/zod/v4/locales/${locale}.js`),
|
import(`../../../node_modules/zod/v4/locales/${locale}.js`),
|
||||||
import(`../../../node_modules/date-fns/locale/${locale}.js`)
|
import(`../../../node_modules/date-fns/locale/${locale}.js`)
|
||||||
@@ -14,8 +25,6 @@ export async function setLocale(locale: Locale, reload = true) {
|
|||||||
console.warn(`Failed to load zod locale for ${locale}:`, zodResult.reason);
|
console.warn(`Failed to load zod locale for ${locale}:`, zodResult.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
setParaglideLocale(locale, { reload });
|
|
||||||
|
|
||||||
if (dateFnsResult.status === 'fulfilled') {
|
if (dateFnsResult.status === 'fulfilled') {
|
||||||
setDefaultOptions({
|
setDefaultOptions({
|
||||||
locale: dateFnsResult.value.default
|
locale: dateFnsResult.value.default
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import z from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
|
export const emptyToUndefined = <T>(validation: z.ZodType<T>) =>
|
||||||
z.preprocess((v) => (v === '' ? undefined : v), validation);
|
z.preprocess((v) => (v === '' ? undefined : v), validation.optional());
|
||||||
|
|
||||||
export const optionalUrl = z
|
export const optionalUrl = z
|
||||||
.url()
|
.url()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import AppConfigService from '$lib/services/app-config-service';
|
|||||||
import UserService from '$lib/services/user-service';
|
import UserService from '$lib/services/user-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import userStore from '$lib/stores/user-store';
|
import userStore from '$lib/stores/user-store';
|
||||||
|
import { setLocaleForLibraries } from '$lib/utils/locale.util';
|
||||||
import type { LayoutLoad } from './$types';
|
import type { LayoutLoad } from './$types';
|
||||||
|
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
@@ -29,6 +30,8 @@ export const load: LayoutLoad = async () => {
|
|||||||
appConfigStore.set(appConfig);
|
appConfigStore.set(appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await setLocaleForLibraries();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
appConfig
|
appConfig
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
|
import { emptyToUndefined } from '$lib/utils/zod-util';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
@@ -26,12 +27,14 @@
|
|||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
|
let hasManualDisplayNameEdit = $state(!!account.displayName);
|
||||||
|
|
||||||
const userService = new UserService();
|
const userService = new UserService();
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: z.string().max(50).optional(),
|
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||||
|
displayName: z.string().max(100),
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(2)
|
||||||
@@ -44,6 +47,14 @@
|
|||||||
|
|
||||||
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
|
const { inputs, ...form } = createForm<FormSchema>(formSchema, account);
|
||||||
|
|
||||||
|
function onNameInput() {
|
||||||
|
if (!hasManualDisplayNameEdit) {
|
||||||
|
$inputs.displayName.value = `${$inputs.firstName.value}${
|
||||||
|
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const data = form.validate();
|
const data = form.validate();
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
@@ -68,7 +79,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={preventDefault(onSubmit)} class="space-y-6">
|
<form onsubmit={preventDefault(onSubmit)} class="space-y-6">
|
||||||
<!-- Profile Picture Section -->
|
|
||||||
<ProfilePictureSettings
|
<ProfilePictureSettings
|
||||||
{userId}
|
{userId}
|
||||||
{isLdapUser}
|
{isLdapUser}
|
||||||
@@ -76,31 +86,32 @@
|
|||||||
resetCallback={resetProfilePicture}
|
resetCallback={resetProfilePicture}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Divider -->
|
|
||||||
<hr class="border-border" />
|
<hr class="border-border" />
|
||||||
|
|
||||||
<!-- User Information -->
|
|
||||||
<fieldset disabled={userInfoInputDisabled}>
|
<fieldset disabled={userInfoInputDisabled}>
|
||||||
<div>
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<div class="flex flex-col gap-3 sm:flex-row">
|
<div>
|
||||||
<div class="w-full">
|
<FormInput label={m.first_name()} bind:input={$inputs.firstName} onInput={onNameInput} />
|
||||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
|
||||||
</div>
|
|
||||||
<div class="w-full">
|
|
||||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 flex flex-col gap-3 sm:flex-row">
|
<div>
|
||||||
<div class="w-full">
|
<FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} />
|
||||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div class="w-full">
|
<FormInput
|
||||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
label={m.display_name()}
|
||||||
</div>
|
bind:input={$inputs.displayName}
|
||||||
|
onInput={() => (hasManualDisplayNameEdit = true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end pt-2">
|
<div class="flex justify-end pt-4">
|
||||||
<Button {isLoading} type="submit">{m.save()}</Button>
|
<Button {isLoading} type="submit">{m.save()}</Button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -120,7 +120,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<CollapsibleCard id="application-configuration-images" icon={LucideImage} title={m.images()}>
|
<CollapsibleCard
|
||||||
|
id="application-configuration-images"
|
||||||
|
icon={LucideImage}
|
||||||
|
title={m.images()}
|
||||||
|
description={m.configure_application_images()}
|
||||||
|
>
|
||||||
<UpdateApplicationImages callback={updateImages} />
|
<UpdateApplicationImages callback={updateImages} />
|
||||||
</CollapsibleCard>
|
</CollapsibleCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
ldapAttributeUserEmail: z.string().min(1),
|
ldapAttributeUserEmail: z.string().min(1),
|
||||||
ldapAttributeUserFirstName: z.string().min(1),
|
ldapAttributeUserFirstName: z.string().min(1),
|
||||||
ldapAttributeUserLastName: z.string().min(1),
|
ldapAttributeUserLastName: z.string().min(1),
|
||||||
|
ldapAttributeUserDisplayName: z.string().min(1),
|
||||||
ldapAttributeUserProfilePicture: z.string(),
|
ldapAttributeUserProfilePicture: z.string(),
|
||||||
ldapAttributeGroupMember: z.string(),
|
ldapAttributeGroupMember: z.string(),
|
||||||
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
ldapAttributeGroupUniqueIdentifier: z.string().min(1),
|
||||||
@@ -159,6 +160,11 @@
|
|||||||
placeholder="sn"
|
placeholder="sn"
|
||||||
bind:input={$inputs.ldapAttributeUserLastName}
|
bind:input={$inputs.ldapAttributeUserLastName}
|
||||||
/>
|
/>
|
||||||
|
<FormInput
|
||||||
|
label={m.display_name_attribute()}
|
||||||
|
placeholder="displayName"
|
||||||
|
bind:input={$inputs.ldapAttributeUserDisplayName}
|
||||||
|
/>
|
||||||
<FormInput
|
<FormInput
|
||||||
label={m.user_profile_picture_attribute()}
|
label={m.user_profile_picture_attribute()}
|
||||||
description={m.the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image()}
|
description={m.the_value_of_this_attribute_can_either_be_a_url_binary_or_base64_encoded_image()}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
|
||||||
import FormInput from '$lib/components/form/form-input.svelte';
|
import FormInput from '$lib/components/form/form-input.svelte';
|
||||||
|
import SwitchWithLabel from '$lib/components/form/switch-with-label.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { User, UserCreate } from '$lib/types/user.type';
|
import type { User, UserCreate } from '$lib/types/user.type';
|
||||||
import { preventDefault } from '$lib/utils/event-util';
|
import { preventDefault } from '$lib/utils/event-util';
|
||||||
import { createForm } from '$lib/utils/form-util';
|
import { createForm } from '$lib/utils/form-util';
|
||||||
|
import { emptyToUndefined } from '$lib/utils/zod-util';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -19,10 +20,12 @@
|
|||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
let inputDisabled = $derived(!!existingUser?.ldapId && $appConfigStore.ldapEnabled);
|
let inputDisabled = $derived(!!existingUser?.ldapId && $appConfigStore.ldapEnabled);
|
||||||
|
let hasManualDisplayNameEdit = $state(!!existingUser?.displayName);
|
||||||
|
|
||||||
const user = {
|
const user = {
|
||||||
firstName: existingUser?.firstName || '',
|
firstName: existingUser?.firstName || '',
|
||||||
lastName: existingUser?.lastName || '',
|
lastName: existingUser?.lastName || '',
|
||||||
|
displayName: existingUser?.displayName || '',
|
||||||
email: existingUser?.email || '',
|
email: existingUser?.email || '',
|
||||||
username: existingUser?.username || '',
|
username: existingUser?.username || '',
|
||||||
isAdmin: existingUser?.isAdmin || false,
|
isAdmin: existingUser?.isAdmin || false,
|
||||||
@@ -31,7 +34,8 @@
|
|||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
firstName: z.string().min(1).max(50),
|
firstName: z.string().min(1).max(50),
|
||||||
lastName: z.string().max(50),
|
lastName: emptyToUndefined(z.string().max(50).optional()),
|
||||||
|
displayName: z.string().max(100),
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
.min(2)
|
.min(2)
|
||||||
@@ -53,15 +57,29 @@
|
|||||||
if (success && !existingUser) form.reset();
|
if (success && !existingUser) form.reset();
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
function onNameInput() {
|
||||||
|
if (!hasManualDisplayNameEdit) {
|
||||||
|
$inputs.displayName.value = `${$inputs.firstName.value}${
|
||||||
|
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={preventDefault(onSubmit)}>
|
<form onsubmit={preventDefault(onSubmit)}>
|
||||||
<fieldset disabled={inputDisabled}>
|
<fieldset disabled={inputDisabled}>
|
||||||
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
<div class="grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||||
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
|
<FormInput label={m.first_name()} oninput={onNameInput} bind:input={$inputs.firstName} />
|
||||||
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
|
<FormInput label={m.last_name()} oninput={onNameInput} bind:input={$inputs.lastName} />
|
||||||
|
<FormInput
|
||||||
|
label={m.display_name()}
|
||||||
|
oninput={() => (hasManualDisplayNameEdit = true)}
|
||||||
|
bind:input={$inputs.displayName}
|
||||||
|
/>
|
||||||
<FormInput label={m.username()} bind:input={$inputs.username} />
|
<FormInput label={m.username()} bind:input={$inputs.username} />
|
||||||
<FormInput label={m.email()} bind:input={$inputs.email} />
|
<FormInput label={m.email()} bind:input={$inputs.email} />
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
|
||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
id="admin-privileges"
|
id="admin-privileges"
|
||||||
label={m.admin_privileges()}
|
label={m.admin_privileges()}
|
||||||
|
|||||||
@@ -103,6 +103,7 @@
|
|||||||
columns={[
|
columns={[
|
||||||
{ label: m.first_name(), sortColumn: 'firstName' },
|
{ label: m.first_name(), sortColumn: 'firstName' },
|
||||||
{ label: m.last_name(), sortColumn: 'lastName' },
|
{ label: m.last_name(), sortColumn: 'lastName' },
|
||||||
|
{ label: m.display_name(), sortColumn: 'displayName' },
|
||||||
{ label: m.email(), sortColumn: 'email' },
|
{ label: m.email(), sortColumn: 'email' },
|
||||||
{ label: m.username(), sortColumn: 'username' },
|
{ label: m.username(), sortColumn: 'username' },
|
||||||
{ label: m.role(), sortColumn: 'isAdmin' },
|
{ label: m.role(), sortColumn: 'isAdmin' },
|
||||||
@@ -114,6 +115,7 @@
|
|||||||
{#snippet rows({ item })}
|
{#snippet rows({ item })}
|
||||||
<Table.Cell>{item.firstName}</Table.Cell>
|
<Table.Cell>{item.firstName}</Table.Cell>
|
||||||
<Table.Cell>{item.lastName}</Table.Cell>
|
<Table.Cell>{item.lastName}</Table.Cell>
|
||||||
|
<Table.Cell>{item.displayName}</Table.Cell>
|
||||||
<Table.Cell>{item.email}</Table.Cell>
|
<Table.Cell>{item.email}</Table.Cell>
|
||||||
<Table.Cell>{item.username}</Table.Cell>
|
<Table.Cell>{item.username}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const users = {
|
|||||||
id: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e',
|
id: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e',
|
||||||
firstname: 'Tim',
|
firstname: 'Tim',
|
||||||
lastname: 'Cook',
|
lastname: 'Cook',
|
||||||
|
displayName: 'Tim Cook',
|
||||||
email: 'tim.cook@test.com',
|
email: 'tim.cook@test.com',
|
||||||
username: 'tim'
|
username: 'tim'
|
||||||
},
|
},
|
||||||
@@ -10,12 +11,14 @@ export const users = {
|
|||||||
id: '1cd19686-f9a6-43f4-a41f-14a0bf5b4036',
|
id: '1cd19686-f9a6-43f4-a41f-14a0bf5b4036',
|
||||||
firstname: 'Craig',
|
firstname: 'Craig',
|
||||||
lastname: 'Federighi',
|
lastname: 'Federighi',
|
||||||
|
displayName: 'Craig Federighi',
|
||||||
email: 'craig.federighi@test.com',
|
email: 'craig.federighi@test.com',
|
||||||
username: 'craig'
|
username: 'craig'
|
||||||
},
|
},
|
||||||
steve: {
|
steve: {
|
||||||
firstname: 'Steve',
|
firstname: 'Steve',
|
||||||
lastname: 'Jobs',
|
lastname: 'Jobs',
|
||||||
|
displayName: 'Steve Jobs',
|
||||||
email: 'steve.jobs@test.com',
|
email: 'steve.jobs@test.com',
|
||||||
username: 'steve'
|
username: 'steve'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ test.beforeEach(async () => await cleanupBackend());
|
|||||||
test('Update account details', async ({ page }) => {
|
test('Update account details', async ({ page }) => {
|
||||||
await page.goto('/settings/account');
|
await page.goto('/settings/account');
|
||||||
|
|
||||||
|
await page.getByLabel('Display Name').fill('Tim Apple');
|
||||||
await page.getByLabel('First name').fill('Timothy');
|
await page.getByLabel('First name').fill('Timothy');
|
||||||
await page.getByLabel('Last name').fill('Apple');
|
await page.getByLabel('Last name').fill('Apple');
|
||||||
|
await page.getByLabel('Display Name').fill('Timothy Apple');
|
||||||
await page.getByLabel('Email').fill('timothy.apple@test.com');
|
await page.getByLabel('Email').fill('timothy.apple@test.com');
|
||||||
await page.getByLabel('Username').fill('timothy');
|
await page.getByLabel('Username').fill('timothy');
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ test('Create user', async ({ page }) => {
|
|||||||
await page.getByLabel('Last name').fill(user.lastname);
|
await page.getByLabel('Last name').fill(user.lastname);
|
||||||
await page.getByLabel('Email').fill(user.email);
|
await page.getByLabel('Email').fill(user.email);
|
||||||
await page.getByLabel('Username').fill(user.username);
|
await page.getByLabel('Username').fill(user.username);
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Display Name')).toHaveValue(`${user.firstname} ${user.lastname}`);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('row', { name: `${user.firstname} ${user.lastname}` })).toBeVisible();
|
await expect(page.getByRole('row', { name: `${user.firstname} ${user.lastname}` })).toBeVisible();
|
||||||
@@ -106,6 +109,7 @@ test('Update user', async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByLabel('First name').fill('Crack');
|
await page.getByLabel('First name').fill('Crack');
|
||||||
await page.getByLabel('Last name').fill('Apple');
|
await page.getByLabel('Last name').fill('Apple');
|
||||||
|
await page.getByLabel('Display Name').fill('Crack Apple');
|
||||||
await page.getByLabel('Email').fill('crack.apple@test.com');
|
await page.getByLabel('Email').fill('crack.apple@test.com');
|
||||||
await page.getByLabel('Username').fill('crack');
|
await page.getByLabel('Username').fill('crack');
|
||||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ test.describe('User Signup', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Sign Up' }).click();
|
await page.getByRole('button', { name: 'Sign Up' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Invalid input').first()).toBeVisible();
|
await expect(page.getByText('Invalid email address').first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Open signup - duplicate email shows error', async ({ page }) => {
|
test('Open signup - duplicate email shows error', async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user