Files
dticket.tootaio.com/shared/auth.ts
xiaomai 4e40bfd804 feat(users): add drag-and-drop reordering for PICs
Introduce pic_sort_order to persist custom user ordering
Replace data table with a custom draggable grid layout
Add API endpoint to handle bulk order updates
2026-05-04 14:07:43 +08:00

112 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
picSortOrder: number
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
picSortOrder: number
}
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'
}