1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-03-22 15:00:07 +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 {
Username string `json:"username" binding:"required,username,min=2,max=50" 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"`
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"`
Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"`
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"`
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"`
Locale *string `json:"locale"`
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",
},
{
name: "missing first name",
input: UserCreateDto{
Username: "testuser",
Email: new("test@example.com"),
LastName: "Doe",
},
wantErr: "",
},
{
name: "missing display name",
input: UserCreateDto{
Username: "testuser",
Email: new("test@example.com"),
FirstName: "John",
LastName: "Doe",
},
wantErr: "Field validation for 'DisplayName' failed on the 'required' tag",
wantErr: "",
},
{
name: "username contains invalid characters",
@@ -73,7 +83,7 @@ func TestUserCreateDto_Validate(t *testing.T) {
LastName: "Doe",
DisplayName: "John Doe",
},
wantErr: "Field validation for 'FirstName' failed on the 'required' tag",
wantErr: "",
},
{
name: "last name too long",

View File

@@ -39,7 +39,7 @@ func (u User) WebAuthnDisplayName() string {
if u.DisplayName != "" {
return u.DisplayName
}
return u.FirstName + " " + u.LastName
return u.FullName()
}
func (u User) WebAuthnIcon() string { return "" }
@@ -76,7 +76,16 @@ func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.Credential
}
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 {

View File

@@ -26,7 +26,7 @@
};
const formSchema = z.object({
firstName: z.string().min(1).max(50),
firstName: z.string().max(50),
lastName: emptyToUndefined(z.string().max(50).optional()),
username: usernameSchema,
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">
<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">
<FormInput label={m.first_name()} bind:input={$inputs.firstName} />
<FormInput label={m.last_name()} bind:input={$inputs.lastName} />
</div>
<FormInput label={m.username()} bind:input={$inputs.username} />
<FormInput label={m.email()} bind:input={$inputs.email} type="email" />
</div>
</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 {
// Handle string allow empty
if (fieldSchema instanceof z.ZodString) {
return fieldSchema.minLength !== null && fieldSchema.minLength > 0;
}
// Handle unions like callbackUrlSchema
if (fieldSchema instanceof z.ZodUnion) {
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) {
return false;
}
return true;
}

View File

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

View File

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

View File

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

View File

@@ -64,9 +64,9 @@ test('Change Locale', async ({ page }) => {
await expect(page.getByText('Taal', { exact: true })).toBeVisible();
// 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 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
await page.context().clearCookies();
@@ -74,9 +74,9 @@ test('Change Locale', async ({ page }) => {
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 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 }) => {