1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-03-22 18:30:09 +00:00

feat: allow first name and display name to be optional (#1288)

Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
taoso
2026-03-04 05:37:39 +08:00
committed by GitHub
parent d7f19ad5e5
commit 8fecc22888
10 changed files with 56 additions and 30 deletions

View File

@@ -3,7 +3,7 @@ package dto
type SignUpDto struct { type SignUpDto 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:"omitempty,email" unorm:"nfc"` Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"` FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"` LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
Token string `json:"token"` Token string `json:"token"`
} }

View File

@@ -26,9 +26,9 @@ 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:"omitempty,email" unorm:"nfc"` Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
EmailVerified bool `json:"emailVerified"` EmailVerified bool `json:"emailVerified"`
FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"` FirstName string `json:"firstName" binding:"max=50" unorm:"nfc"`
LastName string `json:"lastName" binding:"max=50" unorm:"nfc"` LastName string `json:"lastName" binding:"max=50" unorm:"nfc"`
DisplayName string `json:"displayName" binding:"required,min=1,max=100" unorm:"nfc"` DisplayName string `json:"displayName" binding:"max=100" unorm:"nfc"`
IsAdmin bool `json:"isAdmin"` IsAdmin bool `json:"isAdmin"`
Locale *string `json:"locale"` Locale *string `json:"locale"`
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`

View File

@@ -33,14 +33,24 @@ func TestUserCreateDto_Validate(t *testing.T) {
}, },
wantErr: "Field validation for 'Username' failed on the 'required' tag", wantErr: "Field validation for 'Username' failed on the 'required' tag",
}, },
{
name: "missing first name",
input: UserCreateDto{
Username: "testuser",
Email: new("test@example.com"),
LastName: "Doe",
},
wantErr: "",
},
{ {
name: "missing display name", name: "missing display name",
input: UserCreateDto{ input: UserCreateDto{
Username: "testuser",
Email: new("test@example.com"), Email: new("test@example.com"),
FirstName: "John", FirstName: "John",
LastName: "Doe", LastName: "Doe",
}, },
wantErr: "Field validation for 'DisplayName' failed on the 'required' tag", wantErr: "",
}, },
{ {
name: "username contains invalid characters", name: "username contains invalid characters",
@@ -73,7 +83,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
LastName: "Doe", LastName: "Doe",
DisplayName: "John Doe", DisplayName: "John Doe",
}, },
wantErr: "Field validation for 'FirstName' failed on the 'required' tag", wantErr: "",
}, },
{ {
name: "last name too long", name: "last name too long",

View File

@@ -39,7 +39,7 @@ func (u User) WebAuthnDisplayName() string {
if u.DisplayName != "" { if u.DisplayName != "" {
return u.DisplayName return u.DisplayName
} }
return u.FirstName + " " + u.LastName return u.FullName()
} }
func (u User) WebAuthnIcon() string { return "" } func (u User) WebAuthnIcon() string { return "" }
@@ -76,7 +76,16 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential
} }
func (u User) FullName() string { func (u User) FullName() string {
return u.FirstName + " " + u.LastName fullname := strings.TrimSpace(u.FirstName + " " + u.LastName)
if fullname != "" {
return fullname
}
if u.DisplayName != "" {
return u.DisplayName
}
return u.Username
} }
func (u User) Initials() string { func (u User) Initials() string {

View File

@@ -26,7 +26,7 @@
}; };
const formSchema = z.object({ const formSchema = z.object({
firstName: z.string().min(1).max(50), firstName: z.string().max(50),
lastName: emptyToUndefined(z.string().max(50).optional()), lastName: emptyToUndefined(z.string().max(50).optional()),
username: usernameSchema, username: usernameSchema,
email: get(appConfigStore).requireUserEmail ? z.email() : emptyToUndefined(z.email().optional()) email: get(appConfigStore).requireUserEmail ? z.email() : emptyToUndefined(z.email().optional())
@@ -52,12 +52,12 @@
<form id="sign-up-form" onsubmit={preventDefault(onSubmit)} class="w-full"> <form id="sign-up-form" onsubmit={preventDefault(onSubmit)} class="w-full">
<div class="mt-7 space-y-4"> <div class="mt-7 space-y-4">
<FormInput label={m.username()} bind:input={$inputs.username} />
<FormInput label={m.email()} bind:input={$inputs.email} type="email" />
<div class="grid grid-cols-1 gap-4 md:grid-cols-2"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<FormInput label={m.first_name()} bind:input={$inputs.firstName} /> <FormInput label={m.first_name()} bind:input={$inputs.firstName} />
<FormInput label={m.last_name()} bind:input={$inputs.lastName} /> <FormInput label={m.last_name()} bind:input={$inputs.lastName} />
</div> </div>
<FormInput label={m.username()} bind:input={$inputs.username} />
<FormInput label={m.email()} bind:input={$inputs.email} type="email" />
</div> </div>
</form> </form>

View File

@@ -122,6 +122,11 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
} }
function isRequired(fieldSchema: z.ZodTypeAny): boolean { function isRequired(fieldSchema: z.ZodTypeAny): boolean {
// Handle string allow empty
if (fieldSchema instanceof z.ZodString) {
return fieldSchema.minLength !== null && fieldSchema.minLength > 0;
}
// Handle unions like callbackUrlSchema // Handle unions like callbackUrlSchema
if (fieldSchema instanceof z.ZodUnion) { if (fieldSchema instanceof z.ZodUnion) {
return !fieldSchema.def.options.some((o: any) => { return !fieldSchema.def.options.some((o: any) => {
@@ -138,6 +143,7 @@ export function createForm<T extends z.ZodType<any, any>>(schema: T, initialValu
if (fieldSchema instanceof z.ZodOptional || fieldSchema instanceof z.ZodDefault) { if (fieldSchema instanceof z.ZodOptional || fieldSchema instanceof z.ZodDefault) {
return false; return false;
} }
return true; return true;
} }

View File

@@ -35,9 +35,9 @@
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().max(50),
lastName: emptyToUndefined(z.string().max(50).optional()), lastName: emptyToUndefined(z.string().max(50).optional()),
displayName: z.string().min(1).max(100), displayName: z.string().max(100),
username: usernameSchema, username: usernameSchema,
email: get(appConfigStore).requireUserEmail ? z.email() : emptyToUndefined(z.email().optional()) email: get(appConfigStore).requireUserEmail ? z.email() : emptyToUndefined(z.email().optional())
}); });
@@ -52,7 +52,7 @@
if (!hasManualDisplayNameEdit) { if (!hasManualDisplayNameEdit) {
$inputs.displayName.value = `${$inputs.firstName.value}${ $inputs.displayName.value = `${$inputs.firstName.value}${
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : '' $inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
}`; }`.trim();
} }
} }
@@ -91,6 +91,8 @@
<fieldset disabled={userInfoInputDisabled}> <fieldset disabled={userInfoInputDisabled}>
<Field.Group class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <Field.Group class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormInput label={m.username()} bind:input={$inputs.username} />
<FormInput label={m.email()} type="email" bind:input={$inputs.email} />
<FormInput label={m.first_name()} bind:input={$inputs.firstName} onInput={onNameInput} /> <FormInput label={m.first_name()} bind:input={$inputs.firstName} onInput={onNameInput} />
<FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} /> <FormInput label={m.last_name()} bind:input={$inputs.lastName} onInput={onNameInput} />
<FormInput <FormInput
@@ -98,8 +100,6 @@
bind:input={$inputs.displayName} bind:input={$inputs.displayName}
onInput={() => (hasManualDisplayNameEdit = true)} onInput={() => (hasManualDisplayNameEdit = true)}
/> />
<FormInput label={m.username()} bind:input={$inputs.username} />
<FormInput label={m.email()} type="email" bind:input={$inputs.email} />
</Field.Group> </Field.Group>
<div class="flex justify-end pt-4"> <div class="flex justify-end pt-4">

View File

@@ -40,9 +40,9 @@
}; };
const formSchema = z.object({ const formSchema = z.object({
firstName: z.string().min(1).max(50), firstName: z.string().max(50),
lastName: emptyToUndefined(z.string().max(50).optional()), lastName: emptyToUndefined(z.string().max(50).optional()),
displayName: z.string().min(1).max(100), displayName: z.string().max(100),
username: usernameSchema, username: usernameSchema,
email: get(appConfigStore).requireUserEmail email: get(appConfigStore).requireUserEmail
? z.email() ? z.email()
@@ -67,7 +67,7 @@
if (!hasManualDisplayNameEdit) { if (!hasManualDisplayNameEdit) {
$inputs.displayName.value = `${$inputs.firstName.value}${ $inputs.displayName.value = `${$inputs.firstName.value}${
$inputs.lastName?.value ? ' ' + $inputs.lastName.value : '' $inputs.lastName?.value ? ' ' + $inputs.lastName.value : ''
}`; }`.trim();
} }
} }
</script> </script>
@@ -75,13 +75,6 @@
<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()} oninput={onNameInput} bind:input={$inputs.firstName} />
<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} />
<div class="flex items-end"> <div class="flex items-end">
<FormInput <FormInput
@@ -111,6 +104,13 @@
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
</div> </div>
<FormInput label={m.first_name()} oninput={onNameInput} bind:input={$inputs.firstName} />
<FormInput label={m.last_name()} oninput={onNameInput} bind:input={$inputs.lastName} />
<FormInput
label={m.display_name()}
oninput={() => (hasManualDisplayNameEdit = true)}
bind:input={$inputs.displayName}
/>
</div> </div>
<div class="mt-5 grid grid-cols-1 items-start gap-5 md:grid-cols-2"> <div class="mt-5 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
<SwitchWithLabel <SwitchWithLabel

View File

@@ -6,6 +6,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
environment: environment:
- LLDAP_JWT_SECRET=secret - LLDAP_JWT_SECRET=secret
- LLDAP_LDAP_USER_EMAIL=admin@pocket-id.org
- LLDAP_LDAP_USER_PASS=admin_password - LLDAP_LDAP_USER_PASS=admin_password
- LLDAP_LDAP_BASE_DN=dc=pocket-id,dc=org - LLDAP_LDAP_BASE_DN=dc=pocket-id,dc=org
scim-test-server: scim-test-server:

View File

@@ -64,9 +64,9 @@ test('Change Locale', async ({ page }) => {
await expect(page.getByText('Taal', { exact: true })).toBeVisible(); await expect(page.getByText('Taal', { exact: true })).toBeVisible();
// Check if the validation messages are translated because they are provided by Zod // Check if the validation messages are translated because they are provided by Zod
await page.getByRole('textbox', { name: 'Voornaam' }).fill(''); await page.getByRole('textbox', { name: 'Gebruikersnaam' }).fill('');
await page.getByRole('button', { name: 'Opslaan' }).click(); await page.getByRole('button', { name: 'Opslaan' }).click();
await expect(page.getByText('Te kort: verwacht dat string >=1 tekens heeft')).toBeVisible(); await expect(page.getByText('Te kort: verwacht dat string >=2 tekens heeft')).toBeVisible();
// Clear all cookies and sign in again to check if the language is still set to Dutch // Clear all cookies and sign in again to check if the language is still set to Dutch
await page.context().clearCookies(); await page.context().clearCookies();
@@ -74,9 +74,9 @@ test('Change Locale', async ({ page }) => {
await expect(page.getByText('Taal', { exact: true })).toBeVisible(); await expect(page.getByText('Taal', { exact: true })).toBeVisible();
await page.getByRole('textbox', { name: 'Voornaam' }).fill(''); await page.getByRole('textbox', { name: 'Gebruikersnaam' }).fill('');
await page.getByRole('button', { name: 'Opslaan' }).click(); await page.getByRole('button', { name: 'Opslaan' }).click();
await expect(page.getByText('Te kort: verwacht dat string >=1 tekens heeft')).toBeVisible(); await expect(page.getByText('Te kort: verwacht dat string >=2 tekens heeft')).toBeVisible();
}); });
test('Add passkey to an account', async ({ page }) => { test('Add passkey to an account', async ({ page }) => {