🐛 fix(auth): require name field in registration forms

Add required name field (min 2 chars) to all registration forms to fix
Better Auth validation error. Updates backend DTO, shared-auth service,
shared-auth-ui RegisterPage component, i18n translations, and all app
auth stores and register pages.
This commit is contained in:
Wuesteon 2025-12-16 20:28:28 +01:00
parent 11324b5e68
commit d3e11b320a
28 changed files with 151 additions and 56 deletions

View file

@ -111,16 +111,16 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
*/
async signUp(email: string, password: string) {
async signUp(email: string, password: string, name: string) {
const authService = await getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password);
const result = await authService.signUp(email, password, name);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };

View file

@ -12,8 +12,8 @@
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
async function handleSignUp(email: string, password: string, name: string) {
return authStore.signUp(email, password, name);
}
</script>

View file

@ -110,16 +110,16 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
*/
async signUp(email: string, password: string) {
async signUp(email: string, password: string, name: string) {
const authService = await getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password);
const result = await authService.signUp(email, password, name);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };

View file

@ -12,8 +12,8 @@
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
async function handleSignUp(email: string, password: string, name: string) {
return authStore.signUp(email, password, name);
}
</script>

View file

@ -110,16 +110,16 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
*/
async signUp(email: string, password: string) {
async signUp(email: string, password: string, name: string) {
const authService = await getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password);
const result = await authService.signUp(email, password, name);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };

View file

@ -10,8 +10,8 @@
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
async function handleSignUp(email: string, password: string, name: string) {
return authStore.signUp(email, password, name);
}
</script>

View file

@ -110,16 +110,16 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
*/
async signUp(email: string, password: string) {
async signUp(email: string, password: string, name: string) {
const authService = await getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password);
const result = await authService.signUp(email, password, name);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };

View file

@ -11,8 +11,8 @@
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
async function handleSignUp(email: string, password: string, name: string) {
return authStore.signUp(email, password, name);
}
</script>

View file

@ -116,19 +116,20 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password and name
* @param email User email
* @param password User password
* @param name User's display name
* @param referralCode Optional referral code for bonus credits
*/
async signUp(email: string, password: string, referralCode?: string) {
async signUp(email: string, password: string, name: string, referralCode?: string) {
const authService = await getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password, referralCode);
const result = await authService.signUp(email, password, name, referralCode);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };

View file

@ -9,8 +9,13 @@
// Get referral code from URL if present
let initialReferralCode = $derived($page.url.searchParams.get('ref') || '');
async function handleSignUp(email: string, password: string, referralCode?: string) {
return authStore.signUp(email, password, referralCode);
async function handleSignUp(
email: string,
password: string,
name: string,
referralCode?: string
) {
return authStore.signUp(email, password, name, referralCode);
}
async function handleValidateReferralCode(code: string) {

View file

@ -92,10 +92,10 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
*/
async signUp(email: string, password: string) {
const result = await authService.signUp(email, password);
async signUp(email: string, password: string, name: string) {
const result = await authService.signUp(email, password, name);
if (result.success && !result.needsVerification) {
const userData = await authService.getUserFromToken();
user = toManaUser(userData);

View file

@ -5,8 +5,8 @@
import AppSlider from '$lib/components/AppSlider.svelte';
import { authStore } from '$lib/stores/auth.svelte';
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
async function handleSignUp(email: string, password: string, name: string) {
return authStore.signUp(email, password, name);
}
</script>

View file

@ -115,7 +115,7 @@ export const authStore = {
}
},
async signUp(email: string, password: string): Promise<AuthResult> {
async signUp(email: string, password: string, name: string): Promise<AuthResult> {
const authService = await getAuthService();
if (!authService) {
return { success: false, error: 'Auth service not available' };
@ -123,7 +123,7 @@ export const authStore = {
try {
loading = true;
const result = await authService.signUp(email, password);
const result = await authService.signUp(email, password, name);
if (result.success) {
// Auto-login after signup

View file

@ -143,16 +143,16 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
*/
async signUp(email: string, password: string) {
async signUp(email: string, password: string, name: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password);
const result = await authService.signUp(email, password, name);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };

View file

@ -11,8 +11,8 @@
// Get translations based on current locale
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
async function handleSignUp(email: string, password: string, name: string) {
return authStore.signUp(email, password, name);
}
</script>

View file

@ -131,16 +131,16 @@ export const authStore = {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
*/
async signUp(email: string, password: string) {
async signUp(email: string, password: string, name: string) {
const authService = getAuthService();
if (!authService) {
return { success: false, error: 'Auth not available on server', needsVerification: false };
}
try {
const result = await authService.signUp(email, password);
const result = await authService.signUp(email, password, name);
if (!result.success) {
return { success: false, error: result.error || 'Signup failed', needsVerification: false };

View file

@ -10,8 +10,8 @@
const translations = $derived(getRegisterTranslations($locale || 'de'));
async function handleSignUp(email: string, password: string) {
return authStore.signUp(email, password);
async function handleSignUp(email: string, password: string, name: string) {
return authStore.signUp(email, password, name);
}
</script>

View file

@ -6,6 +6,7 @@
const dispatch = createEventDispatcher();
// Formular-Zustände
let name = '';
let email = '';
let password = '';
let confirmPassword = '';
@ -16,11 +17,16 @@
// Formular absenden
async function handleSubmit() {
// Validierung
if (!email || !password || !confirmPassword) {
if (!name || !email || !password || !confirmPassword) {
errorMessage = 'Bitte fülle alle Felder aus.';
return;
}
if (name.length < 2) {
errorMessage = 'Der Name muss mindestens 2 Zeichen lang sein.';
return;
}
if (password !== confirmPassword) {
errorMessage = 'Die Passwörter stimmen nicht überein.';
return;
@ -35,12 +41,13 @@
isLoading = true;
errorMessage = '';
const success = await AuthService.register(email, password);
const success = await AuthService.register(email, password, name);
if (success) {
successMessage =
'Registrierung erfolgreich! Bitte überprüfe deine E-Mails, um dein Konto zu bestätigen.';
// Formular zurücksetzen
name = '';
email = '';
password = '';
confirmPassword = '';
@ -83,6 +90,19 @@
{/if}
<form on:submit|preventDefault={handleSubmit}>
<div class="form-group">
<label for="name">Name</label>
<input
type="text"
id="name"
bind:value={name}
placeholder="Dein Name"
disabled={isLoading}
required
minlength="2"
/>
</div>
<div class="form-group">
<label for="email">E-Mail</label>
<input

View file

@ -8,6 +8,9 @@
/** Translation strings for the register page */
export interface RegisterTranslations {
title: string;
namePlaceholder: string;
nameRequired: string;
nameTooShort: string;
emailPlaceholder: string;
passwordPlaceholder: string;
confirmPasswordPlaceholder: string;
@ -46,6 +49,9 @@
/** Default English translations */
const defaultTranslations: RegisterTranslations = {
title: 'Create Account',
namePlaceholder: 'Name',
nameRequired: 'Name is required',
nameTooShort: 'Name must be at least 2 characters',
emailPlaceholder: 'Email',
passwordPlaceholder: 'Password',
confirmPasswordPlaceholder: 'Confirm Password',
@ -81,8 +87,13 @@
logo: Component<{ size?: number; color?: string }>;
/** Primary color (hex) */
primaryColor: string;
/** Sign up function (with optional referral code) */
onSignUp: (email: string, password: string, referralCode?: string) => Promise<AuthResult>;
/** Sign up function (with name and optional referral code) */
onSignUp: (
email: string,
password: string,
name: string,
referralCode?: string
) => Promise<AuthResult>;
/** Navigation function */
goto: (path: string) => void;
/** Success redirect path */
@ -132,6 +143,7 @@
let error = $state<string | null>(null);
let success = $state(false);
let needsVerification = $state(false);
let name = $state('');
let email = $state('');
let password = $state('');
let confirmPassword = $state('');
@ -249,6 +261,18 @@
success = false;
// Validation
if (!name) {
error = t.nameRequired;
loading = false;
return;
}
if (name.length < 2) {
error = t.nameTooShort;
loading = false;
return;
}
if (!email) {
error = t.emailRequired;
loading = false;
@ -293,7 +317,7 @@
// Pass referral code if valid
const validReferralCode = referralValidation?.valid ? referralCode : undefined;
const result = await onSignUp(email, password, validReferralCode);
const result = await onSignUp(email, password, name, validReferralCode);
loading = false;
@ -301,6 +325,7 @@
if (result.needsVerification) {
needsVerification = true;
success = true;
name = '';
password = '';
confirmPassword = '';
} else {
@ -407,6 +432,24 @@
}}
class="pb-4"
>
<div class="mb-2">
<input
type="text"
bind:value={name}
placeholder={t.namePlaceholder}
required
minlength={2}
class="h-14 w-full rounded-xl border px-4 text-lg transition-colors focus:outline-none focus:ring-2"
style="background-color: {isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.8)'}; border-color: {isDark
? 'rgba(255, 255, 255, 0.2)'
: 'rgba(0, 0, 0, 0.1)'}; color: {isDark
? '#ffffff'
: '#000000'}; --tw-ring-color: {primaryColor};"
/>
</div>
<div class="mb-2">
<input
type="email"
@ -444,7 +487,8 @@
<button
type="button"
onclick={() => (showPassword = !showPassword)}
class="absolute inset-y-0 right-0 flex items-center justify-center w-14 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
class="absolute top-1/2 -translate-y-1/2 right-4 flex items-center justify-center transition-colors"
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
aria-label={showPassword ? t.hidePassword : t.showPassword}
>
{#if showPassword}
@ -476,7 +520,8 @@
<button
type="button"
onclick={() => (showConfirmPassword = !showConfirmPassword)}
class="absolute inset-y-0 right-0 flex items-center justify-center w-14 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
class="absolute top-1/2 -translate-y-1/2 right-4 flex items-center justify-center transition-colors"
style="color: {isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'};"
aria-label={showConfirmPassword ? t.hidePassword : t.showPassword}
>
{#if showConfirmPassword}

View file

@ -103,14 +103,20 @@ export function createAuthService(config: AuthServiceConfig) {
},
/**
* Sign up with email and password
* Sign up with email, password, and name
* @param email User email
* @param password User password
* @param name User display name (min 2 characters)
* @param referralCode Optional referral code for bonus credits
*/
async signUp(email: string, password: string, referralCode?: string): Promise<AuthResult> {
async signUp(
email: string,
password: string,
name: string,
referralCode?: string
): Promise<AuthResult> {
try {
const body: Record<string, string> = { email, password };
const body: Record<string, string> = { email, password, name };
if (referralCode) {
body.referralCode = referralCode;
}

View file

@ -25,6 +25,9 @@
},
"register": {
"title": "Konto erstellen",
"namePlaceholder": "Name",
"nameRequired": "Name ist erforderlich",
"nameTooShort": "Name muss mindestens 2 Zeichen lang sein",
"emailPlaceholder": "E-Mail",
"passwordPlaceholder": "Passwort",
"confirmPasswordPlaceholder": "Passwort bestätigen",

View file

@ -25,6 +25,9 @@
},
"register": {
"title": "Create Account",
"namePlaceholder": "Name",
"nameRequired": "Name is required",
"nameTooShort": "Name must be at least 2 characters",
"emailPlaceholder": "Email",
"passwordPlaceholder": "Password",
"confirmPasswordPlaceholder": "Confirm Password",

View file

@ -25,6 +25,9 @@
},
"register": {
"title": "Crear Cuenta",
"namePlaceholder": "Nombre",
"nameRequired": "El nombre es obligatorio",
"nameTooShort": "El nombre debe tener al menos 2 caracteres",
"emailPlaceholder": "Correo electrónico",
"passwordPlaceholder": "Contraseña",
"confirmPasswordPlaceholder": "Confirmar Contraseña",

View file

@ -25,6 +25,9 @@
},
"register": {
"title": "Créer un compte",
"namePlaceholder": "Nom",
"nameRequired": "Le nom est requis",
"nameTooShort": "Le nom doit contenir au moins 2 caractères",
"emailPlaceholder": "Email",
"passwordPlaceholder": "Mot de passe",
"confirmPasswordPlaceholder": "Confirmer le mot de passe",

View file

@ -40,6 +40,9 @@ export interface AuthTranslations {
};
register: {
title: string;
namePlaceholder: string;
nameRequired: string;
nameTooShort: string;
emailPlaceholder: string;
passwordPlaceholder: string;
confirmPasswordPlaceholder: string;

View file

@ -25,6 +25,9 @@
},
"register": {
"title": "Crea Account",
"namePlaceholder": "Nome",
"nameRequired": "Il nome è obbligatorio",
"nameTooShort": "Il nome deve contenere almeno 2 caratteri",
"emailPlaceholder": "Email",
"passwordPlaceholder": "Password",
"confirmPasswordPlaceholder": "Conferma Password",

View file

@ -85,7 +85,7 @@ export class AuthController {
return this.betterAuthService.registerB2C({
email: registerDto.email,
password: registerDto.password,
name: registerDto.name || '',
name: registerDto.name,
});
}

View file

@ -1,4 +1,4 @@
import { IsEmail, IsString, MinLength, MaxLength, IsOptional } from 'class-validator';
import { IsEmail, IsString, MinLength, MaxLength } from 'class-validator';
export class RegisterDto {
@IsEmail()
@ -10,7 +10,7 @@ export class RegisterDto {
password: string;
@IsString()
@IsOptional()
@MinLength(2, { message: 'Name must be at least 2 characters' })
@MaxLength(255)
name?: string;
name: string;
}