Files
dticket.tootaio.com/shared/auth.ts
xiaomai 06165f80db feat(auth): make passkey enrollment optional on first login
Remove passkey requirement from user onboarding flow
Update UI badges to show passkeys as optional rather than pending
Update documentation to reflect the new behavior
2026-04-27 13:25:05 +08:00

110 lines
2.5 KiB
TypeScript

export const DEFAULT_USER_PASSWORD = '123456'
export const MIN_PASSWORD_LENGTH = 8
export const MIN_FULL_NAME_LENGTH = 2
export const USERNAME_PATTERN = /^[a-z0-9._-]{3,32}$/
export const PHONE_NUMBER_PATTERN = /^\+?\d{8,15}$/
export const DEFAULT_PHONE_COUNTRY_CODE = '+60'
export type UserRole = 'super_admin' | 'staff'
export function normalizeUsername(value: string) {
return value.trim().toLowerCase()
}
export function normalizeFullName(value: string) {
return value.trim()
}
export function normalizePhoneNumber(value: string) {
const trimmed = value.trim()
if (!trimmed) {
return ''
}
const hasPlusPrefix = trimmed.startsWith('+')
const digitsOnly = trimmed.replace(/\D/g, '')
if (!digitsOnly) {
return ''
}
if (hasPlusPrefix) {
return `+${digitsOnly}`
}
const defaultCountryDigits = DEFAULT_PHONE_COUNTRY_CODE.replace(/\D/g, '')
if (digitsOnly.startsWith(defaultCountryDigits)) {
return `+${digitsOnly}`
}
return `+${defaultCountryDigits}${digitsOnly.replace(/^0+/, '')}`
}
export function isValidPhoneNumber(value: string) {
return PHONE_NUMBER_PATTERN.test(normalizePhoneNumber(value))
}
export function isValidUsername(value: string) {
return USERNAME_PATTERN.test(normalizeUsername(value))
}
export function hasValidFullName(value: string) {
return normalizeFullName(value).length >= MIN_FULL_NAME_LENGTH
}
export function isUserRole(value: string | null | undefined): value is UserRole {
return value === 'super_admin' || value === 'staff'
}
export interface AuthUser {
id: string
username: string
fullName: string
phoneNumber: string | null
role: UserRole
isActive: boolean
mustChangePassword: boolean
needsPasskeySetup: boolean
passkeyCount: number
createdAt: string
lastLoginAt: string | null
}
export interface ManagedUser extends AuthUser {
createdBy: string | null
}
export interface PublicContact {
id: string
fullName: string
phoneNumber: string
role: UserRole
}
export interface PasskeySummary {
id: string
label: string
createdAt: string
lastUsedAt: string | null
deviceType: 'singleDevice' | 'multiDevice'
backedUp: boolean
}
export function needsUserOnboarding(
user: Pick<AuthUser, 'mustChangePassword'> | null | undefined
) {
return Boolean(user?.mustChangePassword)
}
export function getDefaultAuthenticatedPath(
user: Pick<AuthUser, 'role' | 'mustChangePassword'>
) {
if (needsUserOnboarding(user)) {
return '/security'
}
return user.role === 'super_admin' ? '/management/users' : '/bookings'
}