diff --git a/apps/api/src/modules/auth/auth.account.schemas.ts b/apps/api/src/modules/auth/auth.account.schemas.ts index fd870d9..5f2c8f2 100644 --- a/apps/api/src/modules/auth/auth.account.schemas.ts +++ b/apps/api/src/modules/auth/auth.account.schemas.ts @@ -5,6 +5,9 @@ import { z } from 'zod' * See progressive-signup-plan.md Section 3. */ export const accountStartSchema = z.object({ + firstName: z.string().trim().min(1).max(80), + lastName: z.string().trim().min(1).max(80), + companyName: z.string().trim().min(2).max(120), email: z.string().email(), password: z.string().min(8).max(128), preferredLanguage: z.enum(['en', 'fr', 'ar']).default('en'), diff --git a/apps/api/src/modules/auth/auth.account.service.ts b/apps/api/src/modules/auth/auth.account.service.ts index 0f0ee93..ff5254f 100644 --- a/apps/api/src/modules/auth/auth.account.service.ts +++ b/apps/api/src/modules/auth/auth.account.service.ts @@ -42,7 +42,7 @@ export async function startAccount(body: AccountStartInput) { const company = await tx.company.create({ data: { - name: '', + name: body.companyName, slug, email: body.email, address: {}, @@ -53,7 +53,7 @@ export async function startAccount(body: AccountStartInput) { await tx.brandSettings.create({ data: { companyId: company.id, - displayName: body.email.split('@')[0] || 'My Workspace', + displayName: body.companyName, subdomain: slug, defaultLocale: body.preferredLanguage, defaultCurrency: 'MAD', @@ -78,8 +78,8 @@ export async function startAccount(body: AccountStartInput) { data: { companyId: company.id, clerkUserId: `local_owner_${company.id}`, - firstName: '', - lastName: '', + firstName: body.firstName, + lastName: body.lastName, email: body.email, passwordHash, emailVerificationToken: hashPublicAccessToken(verificationToken), diff --git a/apps/api/src/tests/api/auth-boundaries.api.test.ts b/apps/api/src/tests/api/auth-boundaries.api.test.ts index 261a703..22d5593 100644 --- a/apps/api/src/tests/api/auth-boundaries.api.test.ts +++ b/apps/api/src/tests/api/auth-boundaries.api.test.ts @@ -128,6 +128,9 @@ describe('auth API boundaries', () => { it('POST /auth/account/start validates minimal signup and returns 201', async () => { const res = await request(app).post('/api/v1/auth/account/start').send({ + firstName: 'Aya', + lastName: 'Benali', + companyName: 'Atlas Cars', email: 'owner@example.com', password: 'super-secret', preferredLanguage: 'fr', @@ -141,6 +144,9 @@ describe('auth API boundaries', () => { }, }) expect(accountService.startAccount).toHaveBeenCalledWith({ + firstName: 'Aya', + lastName: 'Benali', + companyName: 'Atlas Cars', email: 'owner@example.com', password: 'super-secret', preferredLanguage: 'fr', @@ -149,6 +155,9 @@ describe('auth API boundaries', () => { it('POST /auth/account/start rejects malformed minimal signup payloads before service execution', async () => { const res = await request(app).post('/api/v1/auth/account/start').send({ + firstName: 'Aya', + lastName: 'Benali', + companyName: 'A', email: 'not-email', password: 'short', }) diff --git a/apps/dashboard/src/app/sign-up/[[...sign-up]]/SignUpForm.tsx b/apps/dashboard/src/app/sign-up/[[...sign-up]]/SignUpForm.tsx index d36cb4d..606cd82 100644 --- a/apps/dashboard/src/app/sign-up/[[...sign-up]]/SignUpForm.tsx +++ b/apps/dashboard/src/app/sign-up/[[...sign-up]]/SignUpForm.tsx @@ -48,8 +48,12 @@ export default function SignUpForm({ ? requestedLanguage : language; + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [companyName, setCompanyName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [preferredLanguage, setPreferredLanguage] = useState< "en" | "fr" | "ar" @@ -131,11 +135,18 @@ export default function SignUpForm({ const dict = { en: { title: "Create account", - subtitle: "Enter your email and password to get started.", + subtitle: "Enter your details to create your workspace.", + firstName: "First name", + firstNamePlaceholder: "Aya", + lastName: "Last name", + lastNamePlaceholder: "Benali", + company: "Company", + companyPlaceholder: "Atlas Cars", email: "Email", emailPlaceholder: "you@company.com", password: "Password", passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", + confirmPassword: "Confirm password", passwordHint: "Minimum 8 characters", languageLabel: "Preferred language", create: "Create account", @@ -143,6 +154,7 @@ export default function SignUpForm({ alreadyHave: "Already have an account?", signIn: "Sign in", errorEmailTaken: "An account with this email already exists.", + errorPasswordMismatch: "Passwords do not match.", errorGeneric: "Something went wrong. Please try again.", successTitle: "Check your email", successMessage: @@ -151,11 +163,18 @@ export default function SignUpForm({ }, fr: { title: "Cr\u00e9er un compte", - subtitle: "Saisissez votre email et mot de passe pour commencer.", + subtitle: "Saisissez vos informations pour cr\u00e9er votre espace.", + firstName: "Pr\u00e9nom", + firstNamePlaceholder: "Aya", + lastName: "Nom", + lastNamePlaceholder: "Benali", + company: "Entreprise", + companyPlaceholder: "Atlas Cars", email: "Email", emailPlaceholder: "vous@entreprise.com", password: "Mot de passe", passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", + confirmPassword: "Confirmer le mot de passe", passwordHint: "Minimum 8 caract\u00e8res", languageLabel: "Langue pr\u00e9f\u00e9r\u00e9e", create: "Cr\u00e9er un compte", @@ -163,6 +182,7 @@ export default function SignUpForm({ alreadyHave: "Vous avez d\u00e9j\u00e0 un compte ?", signIn: "Se connecter", errorEmailTaken: "Un compte avec cet email existe d\u00e9j\u00e0.", + errorPasswordMismatch: "Les mots de passe ne correspondent pas.", errorGeneric: "Une erreur est survenue. Veuillez r\u00e9essayer.", successTitle: "V\u00e9rifiez votre email", successMessage: @@ -173,12 +193,20 @@ export default function SignUpForm({ title: "\u0623\u0646\u0634\u0626 \u062d\u0633\u0627\u0628\u064b\u0627", subtitle: - "\u0623\u062f\u062e\u0644 \u0628\u0631\u064a\u062f\u0643 \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a \u0648\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631 \u0644\u0644\u0628\u062f\u0621.", + "\u0623\u062f\u062e\u0644 \u0628\u064a\u0627\u0646\u0627\u062a\u0643 \u0644\u0625\u0646\u0634\u0627\u0621 \u0645\u0633\u0627\u062d\u0629 \u0639\u0645\u0644\u0643.", + firstName: "\u0627\u0644\u0627\u0633\u0645 \u0627\u0644\u0623\u0648\u0644", + firstNamePlaceholder: "Aya", + lastName: "\u0627\u0633\u0645 \u0627\u0644\u0639\u0627\u0626\u0644\u0629", + lastNamePlaceholder: "Benali", + company: "\u0627\u0644\u0634\u0631\u0643\u0629", + companyPlaceholder: "Atlas Cars", email: "\u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a", emailPlaceholder: "you@company.com", password: "\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631", passwordPlaceholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", + confirmPassword: + "\u062a\u0623\u0643\u064a\u062f \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631", passwordHint: "\u0627\u0644\u062d\u062f \u0627\u0644\u0623\u062f\u0646\u0649 8 \u0623\u062d\u0631\u0641", languageLabel: @@ -193,6 +221,8 @@ export default function SignUpForm({ "\u062a\u0633\u062c\u064a\u0644 \u0627\u0644\u062f\u062e\u0648\u0644", errorEmailTaken: "\u064a\u0648\u062c\u062f \u062d\u0633\u0627\u0628 \u0628\u0647\u0630\u0627 \u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a \u0628\u0627\u0644\u0641\u0639\u0644.", + errorPasswordMismatch: + "\u0643\u0644\u0645\u062a\u0627 \u0627\u0644\u0645\u0631\u0648\u0631 \u063a\u064a\u0631 \u0645\u062a\u0637\u0627\u0628\u0642\u062a\u064a\u0646.", errorGeneric: "\u062d\u062f\u062b \u062e\u0637\u0623 \u0645\u0627. \u064a\u0631\u062c\u0649 \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629 \u0645\u0631\u0629 \u0623\u062e\u0631\u0649.", successTitle: @@ -212,13 +242,25 @@ export default function SignUpForm({ async function handleSubmit(e: React.FormEvent) { e.preventDefault(); + if (password !== confirmPassword) { + setError(dict.errorPasswordMismatch); + return; + } + setLoading(true); setError(null); try { await apiFetch("/auth/account/start", { method: "POST", - body: JSON.stringify({ email, password, preferredLanguage }), + body: JSON.stringify({ + firstName, + lastName, + companyName, + email, + password, + preferredLanguage, + }), }); setLanguage(preferredLanguage); @@ -277,7 +319,7 @@ export default function SignUpForm({ return (
-
+
@@ -307,6 +349,55 @@ export default function SignUpForm({ ) : null}
+
+
+ + setFirstName(e.target.value)} + placeholder={dict.firstNamePlaceholder} + autoFocus + className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500" + /> +
+ +
+ + setLastName(e.target.value)} + placeholder={dict.lastNamePlaceholder} + className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500" + /> +
+
+ +
+ + setCompanyName(e.target.value)} + placeholder={dict.companyPlaceholder} + className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500" + /> +
+
@@ -387,6 +477,21 @@ export default function SignUpForm({

+
+ + setConfirmPassword(e.target.value)} + placeholder={dict.passwordPlaceholder} + className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 text-sm text-stone-900 transition-colors placeholder:text-stone-400 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-blue-950/80 dark:text-stone-100 dark:placeholder:text-stone-500" + /> +
+