refactor: centralize validation, error handling, and formatting logic
Extract shared auth logic and validation rules to shared/auth.ts Introduce utility functions for HTTP errors and user input parsing Standardize error messages and date formatting across the app
This commit is contained in:
@@ -1,14 +1,10 @@
|
||||
import {
|
||||
DEFAULT_USER_PASSWORD,
|
||||
USERNAME_PATTERN,
|
||||
isValidPhoneNumber,
|
||||
normalizePhoneNumber,
|
||||
type UserRole
|
||||
} from '~~/shared/auth'
|
||||
import { DEFAULT_USER_PASSWORD, type UserRole } from '~~/shared/auth'
|
||||
|
||||
import { normalizeUsername, requireRole } from '../../utils/auth'
|
||||
import { requireRole } from '../../utils/auth'
|
||||
import { mapDatabaseError } from '../../utils/http'
|
||||
import { hashPassword } from '../../utils/password'
|
||||
import { createUser } from '../../utils/user-repository'
|
||||
import { parseCreateUserInput } from '../../utils/users'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const auth = await requireRole(event, 'super_admin')
|
||||
@@ -19,31 +15,12 @@ export default defineEventHandler(async (event) => {
|
||||
role?: UserRole
|
||||
}>(event)
|
||||
|
||||
const username = normalizeUsername(body.username || '')
|
||||
const fullName = body.fullName?.trim() || ''
|
||||
const phoneNumber = normalizePhoneNumber(body.phoneNumber || '')
|
||||
const role = body.role === 'super_admin' ? 'super_admin' : 'staff'
|
||||
|
||||
if (!USERNAME_PATTERN.test(username)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Username must be 3 to 32 characters using lowercase letters, numbers, dot, dash, or underscore'
|
||||
})
|
||||
}
|
||||
|
||||
if (fullName.length < 2) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Full name must be at least 2 characters'
|
||||
})
|
||||
}
|
||||
|
||||
if (!isValidPhoneNumber(phoneNumber)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Phone number must contain 8 to 15 digits'
|
||||
})
|
||||
}
|
||||
const {
|
||||
username,
|
||||
fullName,
|
||||
phoneNumber,
|
||||
role
|
||||
} = parseCreateUserInput(body)
|
||||
|
||||
const passwordHash = await hashPassword(DEFAULT_USER_PASSWORD)
|
||||
|
||||
@@ -61,14 +38,12 @@ export default defineEventHandler(async (event) => {
|
||||
user,
|
||||
defaultPassword: DEFAULT_USER_PASSWORD
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.code === '23505') {
|
||||
throw createError({
|
||||
} catch (error) {
|
||||
mapDatabaseError(error, {
|
||||
'23505': {
|
||||
statusCode: 409,
|
||||
statusMessage: 'Username already exists'
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import {
|
||||
isValidPhoneNumber,
|
||||
normalizePhoneNumber,
|
||||
type UserRole
|
||||
} from '~~/shared/auth'
|
||||
import type { UserRole } from '~~/shared/auth'
|
||||
|
||||
import { requireRole } from '../../../utils/auth'
|
||||
import { getUserById, updateUserProfile } from '../../../utils/user-repository'
|
||||
import { updateUserProfile } from '../../../utils/user-repository'
|
||||
import { httpError } from '../../../utils/http'
|
||||
import { parseUserProfileInput, requireExistingUser, requireUserIdParam } from '../../../utils/users'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const auth = await requireRole(event, 'super_admin')
|
||||
const userId = getRouterParam(event, 'id')
|
||||
|
||||
if (!userId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'User id is required'
|
||||
})
|
||||
}
|
||||
const userId = requireUserIdParam(event)
|
||||
|
||||
const body = await readBody<{
|
||||
fullName?: string
|
||||
@@ -24,45 +15,12 @@ export default defineEventHandler(async (event) => {
|
||||
role?: UserRole
|
||||
}>(event)
|
||||
|
||||
const fullName = body.fullName?.trim() || ''
|
||||
const phoneNumber = normalizePhoneNumber(body.phoneNumber || '')
|
||||
const role = body.role
|
||||
const { fullName, phoneNumber, role } = parseUserProfileInput(body)
|
||||
|
||||
if (fullName.length < 2) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Display name must be at least 2 characters'
|
||||
})
|
||||
}
|
||||
|
||||
if (!isValidPhoneNumber(phoneNumber)) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Phone number must contain 8 to 15 digits'
|
||||
})
|
||||
}
|
||||
|
||||
if (role !== 'super_admin' && role !== 'staff') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Role is invalid'
|
||||
})
|
||||
}
|
||||
|
||||
const user = await getUserById(userId)
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'User not found'
|
||||
})
|
||||
}
|
||||
await requireExistingUser(userId)
|
||||
|
||||
if (auth.user.id === userId && role !== 'super_admin') {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'You cannot remove your own super admin access'
|
||||
})
|
||||
httpError(400, 'You cannot remove your own super admin access')
|
||||
}
|
||||
|
||||
const updatedUser = await updateUserProfile({
|
||||
|
||||
@@ -2,28 +2,15 @@ import { DEFAULT_USER_PASSWORD } from '~~/shared/auth'
|
||||
|
||||
import { requireRole } from '../../../../utils/auth'
|
||||
import { hashPassword } from '../../../../utils/password'
|
||||
import { getUserById, updateUserPassword } from '../../../../utils/user-repository'
|
||||
import { updateUserPassword } from '../../../../utils/user-repository'
|
||||
import { requireExistingUser, requireUserIdParam } from '../../../../utils/users'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireRole(event, 'super_admin')
|
||||
|
||||
const userId = getRouterParam(event, 'id')
|
||||
const userId = requireUserIdParam(event)
|
||||
|
||||
if (!userId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'User id is required'
|
||||
})
|
||||
}
|
||||
|
||||
const user = await getUserById(userId)
|
||||
|
||||
if (!user) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'User not found'
|
||||
})
|
||||
}
|
||||
await requireExistingUser(userId)
|
||||
|
||||
const passwordHash = await hashPassword(DEFAULT_USER_PASSWORD)
|
||||
|
||||
@@ -33,7 +20,7 @@ export default defineEventHandler(async (event) => {
|
||||
mustChangePassword: true
|
||||
})
|
||||
|
||||
const updatedUser = await getUserById(userId)
|
||||
const updatedUser = await requireExistingUser(userId, 'Unable to load updated user')
|
||||
|
||||
return {
|
||||
user: updatedUser,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { MIN_PASSWORD_LENGTH } from '~~/shared/auth'
|
||||
|
||||
import { assertBadRequest, httpError } from '../../utils/http'
|
||||
import { requireAuth } from '../../utils/auth'
|
||||
import { hashPassword, verifyPassword } from '../../utils/password'
|
||||
import { getUserById, updateUserPassword } from '../../utils/user-repository'
|
||||
import { updateUserPassword } from '../../utils/user-repository'
|
||||
import { requireExistingUser } from '../../utils/users'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const auth = await requireAuth(event)
|
||||
@@ -14,34 +16,21 @@ export default defineEventHandler(async (event) => {
|
||||
const currentPassword = body.currentPassword?.trim() || ''
|
||||
const newPassword = body.newPassword?.trim() || ''
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Current password and new password are required'
|
||||
})
|
||||
}
|
||||
assertBadRequest(currentPassword, 'Current password and new password are required')
|
||||
assertBadRequest(newPassword, 'Current password and new password are required')
|
||||
|
||||
if (newPassword.length < MIN_PASSWORD_LENGTH) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: `New password must be at least ${MIN_PASSWORD_LENGTH} characters`
|
||||
})
|
||||
httpError(400, `New password must be at least ${MIN_PASSWORD_LENGTH} characters`)
|
||||
}
|
||||
|
||||
if (currentPassword === newPassword) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'New password must be different from the current password'
|
||||
})
|
||||
httpError(400, 'New password must be different from the current password')
|
||||
}
|
||||
|
||||
const currentPasswordMatches = await verifyPassword(currentPassword, auth.user.passwordHash)
|
||||
|
||||
if (!currentPasswordMatches) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Current password is incorrect'
|
||||
})
|
||||
httpError(400, 'Current password is incorrect')
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(newPassword)
|
||||
@@ -52,7 +41,7 @@ export default defineEventHandler(async (event) => {
|
||||
mustChangePassword: false
|
||||
})
|
||||
|
||||
const updatedUser = await getUserById(auth.user.id)
|
||||
const updatedUser = await requireExistingUser(auth.user.id, 'Unable to load updated user')
|
||||
|
||||
return {
|
||||
user: updatedUser
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { normalizeUsername } from '~~/shared/auth'
|
||||
|
||||
import { signInUser } from '../../utils/auth'
|
||||
import { assertBadRequest, httpError } from '../../utils/http'
|
||||
import { verifyPassword } from '../../utils/password'
|
||||
import { normalizeUsername, signInUser } from '../../utils/auth'
|
||||
import { getUserByUsername } from '../../utils/user-repository'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -13,29 +16,19 @@ export default defineEventHandler(async (event) => {
|
||||
const password = body.password?.trim() || ''
|
||||
const remember = body.remember !== false
|
||||
|
||||
if (!username || !password) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Username and password are required'
|
||||
})
|
||||
}
|
||||
assertBadRequest(username, 'Username and password are required')
|
||||
assertBadRequest(password, 'Username and password are required')
|
||||
|
||||
const user = await getUserByUsername(username)
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Invalid username or password'
|
||||
})
|
||||
httpError(401, 'Invalid username or password')
|
||||
}
|
||||
|
||||
const passwordMatches = await verifyPassword(password, user.passwordHash)
|
||||
|
||||
if (!passwordMatches) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Invalid username or password'
|
||||
})
|
||||
httpError(401, 'Invalid username or password')
|
||||
}
|
||||
|
||||
const authenticatedUser = await signInUser(event, user, remember)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { verifyAuthenticationResponse, type AuthenticationResponseJSON } from '@simplewebauthn/server'
|
||||
|
||||
import { getUserById, getCredentialForVerification, updateCredentialCounter } from '../../../../utils/user-repository'
|
||||
import { consumeLoginChallenge, getWebAuthnConfig, toWebAuthnCredential } from '../../../../utils/webauthn'
|
||||
import { signInUser } from '../../../../utils/auth'
|
||||
import { assertBadRequest, httpError } from '../../../../utils/http'
|
||||
import { getCredentialForVerification, updateCredentialCounter } from '../../../../utils/user-repository'
|
||||
import { requireExistingUser } from '../../../../utils/users'
|
||||
import { consumeLoginChallenge, getWebAuthnConfig, toWebAuthnCredential } from '../../../../utils/webauthn'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody<{
|
||||
@@ -15,29 +17,19 @@ export default defineEventHandler(async (event) => {
|
||||
const challengeToken = body.challengeToken?.trim() || ''
|
||||
const remember = body.remember !== false
|
||||
|
||||
if (!response || !challengeToken) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Passkey login payload is incomplete'
|
||||
})
|
||||
}
|
||||
assertBadRequest(response, 'Passkey login payload is incomplete')
|
||||
assertBadRequest(challengeToken, 'Passkey login payload is incomplete')
|
||||
|
||||
const expectedChallenge = await consumeLoginChallenge(challengeToken)
|
||||
|
||||
if (!expectedChallenge) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Passkey login challenge expired. Try again.'
|
||||
})
|
||||
httpError(400, 'Passkey login challenge expired. Try again.')
|
||||
}
|
||||
|
||||
const storedCredential = await getCredentialForVerification(response.id)
|
||||
|
||||
if (!storedCredential) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Passkey is not recognized'
|
||||
})
|
||||
httpError(401, 'Passkey is not recognized')
|
||||
}
|
||||
|
||||
const config = getWebAuthnConfig(event)
|
||||
@@ -50,19 +42,13 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
|
||||
if (!verification.verified) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'Passkey authentication failed'
|
||||
})
|
||||
httpError(401, 'Passkey authentication failed')
|
||||
}
|
||||
|
||||
const user = await getUserById(storedCredential.userId)
|
||||
const user = await requireExistingUser(storedCredential.userId, 'User account is not available')
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: 'User account is not available'
|
||||
})
|
||||
if (!user.isActive) {
|
||||
httpError(401, 'User account is not available')
|
||||
}
|
||||
|
||||
await updateCredentialCounter({
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { verifyRegistrationResponse, type RegistrationResponseJSON } from '@simplewebauthn/server'
|
||||
|
||||
import { requireAuth } from '../../../../utils/auth'
|
||||
import { createUserPasskey, getUserById, listUserPasskeys } from '../../../../utils/user-repository'
|
||||
import { assertBadRequest, httpError, mapDatabaseError } from '../../../../utils/http'
|
||||
import { createUserPasskey, listUserPasskeys } from '../../../../utils/user-repository'
|
||||
import { requireExistingUser } from '../../../../utils/users'
|
||||
import { buildPasskeyLabel, consumeRegistrationChallenge, getWebAuthnConfig } from '../../../../utils/webauthn'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
@@ -10,20 +12,12 @@ export default defineEventHandler(async (event) => {
|
||||
response?: RegistrationResponseJSON
|
||||
}>(event)
|
||||
|
||||
if (!body.response) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Passkey registration response is required'
|
||||
})
|
||||
}
|
||||
assertBadRequest(body.response, 'Passkey registration response is required')
|
||||
|
||||
const expectedChallenge = await consumeRegistrationChallenge(auth.user.id)
|
||||
|
||||
if (!expectedChallenge) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Passkey registration challenge expired. Try again.'
|
||||
})
|
||||
httpError(400, 'Passkey registration challenge expired. Try again.')
|
||||
}
|
||||
|
||||
const config = getWebAuthnConfig(event)
|
||||
@@ -35,10 +29,7 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
|
||||
if (!verification.verified || !verification.registrationInfo) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: 'Passkey registration could not be verified'
|
||||
})
|
||||
httpError(400, 'Passkey registration could not be verified')
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -52,18 +43,16 @@ export default defineEventHandler(async (event) => {
|
||||
transports: body.response.response.transports || [],
|
||||
label: buildPasskeyLabel()
|
||||
})
|
||||
} catch (error: any) {
|
||||
if (error?.code === '23505') {
|
||||
throw createError({
|
||||
} catch (error) {
|
||||
mapDatabaseError(error, {
|
||||
'23505': {
|
||||
statusCode: 409,
|
||||
statusMessage: 'This passkey is already registered'
|
||||
})
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updatedUser = await getUserById(auth.user.id)
|
||||
const updatedUser = await requireExistingUser(auth.user.id, 'Unable to load updated user')
|
||||
const passkeys = await listUserPasskeys(auth.user.id)
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
import type { UserRole } from '~~/shared/auth'
|
||||
import { normalizeUsername, type UserRole } from '~~/shared/auth'
|
||||
|
||||
import { createUserSession, destroyUserSession, getUserSession } from './session'
|
||||
import { getUserById, updateLastLogin, type UserAuthRecord } from './user-repository'
|
||||
|
||||
export function normalizeUsername(value: string) {
|
||||
return value.trim().toLowerCase()
|
||||
}
|
||||
|
||||
export async function getAuthContext(event: H3Event): Promise<{
|
||||
session: Awaited<ReturnType<typeof getUserSession>>
|
||||
user: UserAuthRecord
|
||||
|
||||
46
server/utils/http.ts
Normal file
46
server/utils/http.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
import { getRouterParam } from 'h3'
|
||||
|
||||
export function httpError(statusCode: number, statusMessage: string): never {
|
||||
throw createError({
|
||||
statusCode,
|
||||
statusMessage
|
||||
})
|
||||
}
|
||||
|
||||
export function assertHttp(
|
||||
condition: unknown,
|
||||
statusCode: number,
|
||||
statusMessage: string
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
httpError(statusCode, statusMessage)
|
||||
}
|
||||
}
|
||||
|
||||
export function assertBadRequest(condition: unknown, statusMessage: string): asserts condition {
|
||||
assertHttp(condition, 400, statusMessage)
|
||||
}
|
||||
|
||||
export function getRequiredRouteParam(event: H3Event, name: string, label = name) {
|
||||
const value = getRouterParam(event, name)
|
||||
|
||||
assertBadRequest(value, `${label} is required`)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function mapDatabaseError(
|
||||
error: unknown,
|
||||
handlers: Partial<Record<string, { statusCode: number, statusMessage: string }>>
|
||||
): never {
|
||||
const code = (error as { code?: string } | null)?.code
|
||||
const handler = code ? handlers[code] : undefined
|
||||
|
||||
if (handler) {
|
||||
httpError(handler.statusCode, handler.statusMessage)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
@@ -56,11 +56,7 @@ export interface PasskeyRecord {
|
||||
lastUsedAt: string | null
|
||||
}
|
||||
|
||||
function parseCount(value: number | string): number {
|
||||
return typeof value === 'number' ? value : Number.parseInt(value, 10)
|
||||
}
|
||||
|
||||
function parseCounter(value: number | string): number {
|
||||
function parseInteger(value: number | string): number {
|
||||
return typeof value === 'number' ? value : Number.parseInt(value, 10)
|
||||
}
|
||||
|
||||
@@ -78,7 +74,7 @@ function parseTransports(value: AuthenticatorTransportFuture[] | string): Authen
|
||||
}
|
||||
|
||||
function mapAuthUser(row: DbUserRow): AuthUser {
|
||||
const passkeyCount = parseCount(row.passkey_count)
|
||||
const passkeyCount = parseInteger(row.passkey_count)
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -116,7 +112,7 @@ function mapPasskeyRecord(row: DbPasskeyRow): PasskeyRecord {
|
||||
userId: row.user_id,
|
||||
credentialId: row.credential_id,
|
||||
publicKey: row.public_key,
|
||||
counter: parseCounter(row.counter),
|
||||
counter: parseInteger(row.counter),
|
||||
deviceType: row.device_type,
|
||||
backedUp: row.backed_up,
|
||||
transports: parseTransports(row.transports),
|
||||
|
||||
75
server/utils/users.ts
Normal file
75
server/utils/users.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { H3Event } from 'h3'
|
||||
|
||||
import {
|
||||
hasValidFullName,
|
||||
isUserRole,
|
||||
isValidPhoneNumber,
|
||||
isValidUsername,
|
||||
normalizeFullName,
|
||||
normalizePhoneNumber,
|
||||
normalizeUsername,
|
||||
type UserRole
|
||||
} from '~~/shared/auth'
|
||||
|
||||
import { assertBadRequest, getRequiredRouteParam, httpError } from './http'
|
||||
import { getUserById } from './user-repository'
|
||||
|
||||
export function requireUserIdParam(event: H3Event) {
|
||||
return getRequiredRouteParam(event, 'id', 'User id')
|
||||
}
|
||||
|
||||
export async function requireExistingUser(userId: string, statusMessage = 'User not found') {
|
||||
const user = await getUserById(userId)
|
||||
|
||||
if (!user) {
|
||||
httpError(404, statusMessage)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
export function parseCreateUserInput(body: {
|
||||
username?: string
|
||||
fullName?: string
|
||||
phoneNumber?: string
|
||||
role?: UserRole
|
||||
}) {
|
||||
const username = normalizeUsername(body.username || '')
|
||||
const fullName = normalizeFullName(body.fullName || '')
|
||||
const phoneNumber = normalizePhoneNumber(body.phoneNumber || '')
|
||||
const role = body.role === 'super_admin' ? 'super_admin' : 'staff'
|
||||
|
||||
assertBadRequest(
|
||||
isValidUsername(username),
|
||||
'Username must be 3 to 32 characters using lowercase letters, numbers, dot, dash, or underscore'
|
||||
)
|
||||
assertBadRequest(hasValidFullName(fullName), 'Full name must be at least 2 characters')
|
||||
assertBadRequest(isValidPhoneNumber(phoneNumber), 'Phone number must contain 8 to 15 digits')
|
||||
|
||||
return {
|
||||
username,
|
||||
fullName,
|
||||
phoneNumber,
|
||||
role
|
||||
}
|
||||
}
|
||||
|
||||
export function parseUserProfileInput(body: {
|
||||
fullName?: string
|
||||
phoneNumber?: string
|
||||
role?: UserRole
|
||||
}) {
|
||||
const fullName = normalizeFullName(body.fullName || '')
|
||||
const phoneNumber = normalizePhoneNumber(body.phoneNumber || '')
|
||||
const role = body.role
|
||||
|
||||
assertBadRequest(hasValidFullName(fullName), 'Display name must be at least 2 characters')
|
||||
assertBadRequest(isValidPhoneNumber(phoneNumber), 'Phone number must contain 8 to 15 digits')
|
||||
assertBadRequest(isUserRole(role), 'Role is invalid')
|
||||
|
||||
return {
|
||||
fullName,
|
||||
phoneNumber,
|
||||
role
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user