diff --git a/src/components/admin/users/user-form.tsx b/src/components/admin/users/user-form.tsx index ccb19d44..80bc79aa 100644 --- a/src/components/admin/users/user-form.tsx +++ b/src/components/admin/users/user-form.tsx @@ -93,6 +93,9 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) { // password here. Toggle off to set one manually. const [sendSetupEmail, setSendSetupEmail] = useState(true); const [displayName, setDisplayName] = useState(user?.displayName ?? ''); + // New users: optional sign-in username (they can also sign in with their + // email). Lowercased on the way out; the API validates shape + uniqueness. + const [username, setUsername] = useState(''); const [phoneValue, setPhoneValue] = useState( user?.phone ? { e164: user.phone, country: 'US' } : null, ); @@ -153,6 +156,7 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) { password: sendSetupEmail ? undefined : password, sendSetupEmail, displayName, + username: username.trim() ? username.trim().toLowerCase() : undefined, phone: phoneE164 ?? undefined, roleId, residentialAccess, @@ -236,6 +240,26 @@ function UserFormBody({ open, onOpenChange, user, onSuccess }: UserFormProps) {

+ {!isEdit && ( +
+ + setUsername(e.target.value)} + placeholder="e.g. abbie" + autoCapitalize="none" + autoCorrect="off" + spellCheck={false} + /> +

+ Lets them sign in with a short username instead of their email. 2–30 lowercase + letters, digits, dot, underscore, or hyphen. Leave blank to sign in by email + only. +

+
+ )} +
0) { + throw new ConflictError('That username is already taken.'); + } + username = candidate; + } + // Two onboarding modes: // - setup-email (default when no password is supplied): provision the // account with a throwaway random password the admin never sees, then @@ -179,6 +205,7 @@ export async function createUser(portId: string, data: CreateUserInput, meta: Au await db.insert(userProfiles).values({ userId: newUserId, displayName: data.displayName, + username, firstName: data.firstName ?? null, lastName: data.lastName ?? null, phone: data.phone ?? null, diff --git a/src/lib/validators/users.ts b/src/lib/validators/users.ts index 27a2fe60..059af9d1 100644 --- a/src/lib/validators/users.ts +++ b/src/lib/validators/users.ts @@ -13,6 +13,9 @@ export const createUserSchema = z * password. When false, `password` must be supplied inline. */ sendSetupEmail: z.boolean().optional(), displayName: z.string().min(1).max(200), + /** Optional sign-in username. Shape/uniqueness/reserved checks run in the + * service (mirrors the self-service /api/v1/me path). Omit for email-only. */ + username: z.string().optional(), firstName: z.string().min(1).max(200).nullable().optional(), lastName: z.string().min(1).max(200).nullable().optional(), phone: z.string().optional(),