make kestrel a tak server, so that it can send and receive pois as cots data
Some checks failed
ci/woodpecker/pr/pr Pipeline failed
Some checks failed
ci/woodpecker/pr/pr Pipeline failed
This commit is contained in:
128
server/utils/validation.js
Normal file
128
server/utils/validation.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Validation schemas - pure functions for consistent input validation.
|
||||
*/
|
||||
|
||||
import { sanitizeString, sanitizeIdentifier, sanitizeLabel } from './sanitize.js'
|
||||
import { DEVICE_TYPES, SOURCE_TYPES } from './deviceUtils.js'
|
||||
import { POI_ICON_TYPES } from './poiConstants.js'
|
||||
|
||||
const ROLES = ['admin', 'leader', 'member']
|
||||
|
||||
const validateNumber = (value, field) => {
|
||||
const num = Number(value)
|
||||
return Number.isFinite(num) ? { valid: true, value: num } : { valid: false, error: `${field} must be a finite number` }
|
||||
}
|
||||
|
||||
const validateEnum = (value, allowed, field) => allowed.includes(value) ? { valid: true, value } : { valid: false, error: `Invalid ${field}` }
|
||||
|
||||
const handleField = (d, field, handler, updates, errors, outputField = null) => {
|
||||
if (d[field] !== undefined) {
|
||||
const result = handler(d[field])
|
||||
if (result.valid) updates[outputField || field] = result.value
|
||||
else errors.push(result.error)
|
||||
}
|
||||
}
|
||||
|
||||
export function validateDevice(data) {
|
||||
if (!data || typeof data !== 'object') return { valid: false, errors: ['body required'] }
|
||||
const d = /** @type {Record<string, unknown>} */ (data)
|
||||
const errors = []
|
||||
const latCheck = validateNumber(d.lat, 'lat')
|
||||
const lngCheck = validateNumber(d.lng, 'lng')
|
||||
if (!latCheck.valid || !lngCheck.valid) errors.push('lat and lng required as finite numbers')
|
||||
if (errors.length > 0) return { valid: false, errors }
|
||||
return {
|
||||
valid: true,
|
||||
errors: [],
|
||||
data: {
|
||||
name: sanitizeString(d.name, 1000),
|
||||
device_type: validateEnum(d.device_type, DEVICE_TYPES, 'device_type').value || 'feed',
|
||||
vendor: d.vendor !== undefined ? sanitizeString(d.vendor, 255) : null,
|
||||
lat: latCheck.value,
|
||||
lng: lngCheck.value,
|
||||
stream_url: typeof d.stream_url === 'string' ? sanitizeString(d.stream_url, 2000) : '',
|
||||
source_type: validateEnum(d.source_type, SOURCE_TYPES, 'source_type').value || 'mjpeg',
|
||||
config: d.config == null ? null : (typeof d.config === 'string' ? d.config : JSON.stringify(d.config)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function validateUpdateDevice(data) {
|
||||
if (!data || typeof data !== 'object') return { valid: true, errors: [], data: {} }
|
||||
const d = /** @type {Record<string, unknown>} */ (data)
|
||||
const errors = []
|
||||
const updates = {}
|
||||
if (d.name !== undefined) updates.name = sanitizeString(d.name, 1000)
|
||||
handleField(d, 'device_type', v => validateEnum(v, DEVICE_TYPES, 'device_type'), updates, errors)
|
||||
if (d.vendor !== undefined) updates.vendor = d.vendor === null || d.vendor === '' ? null : sanitizeString(d.vendor, 255)
|
||||
handleField(d, 'lat', v => validateNumber(v, 'lat'), updates, errors)
|
||||
handleField(d, 'lng', v => validateNumber(v, 'lng'), updates, errors)
|
||||
if (d.stream_url !== undefined) updates.stream_url = sanitizeString(d.stream_url, 2000)
|
||||
handleField(d, 'source_type', v => validateEnum(v, SOURCE_TYPES, 'source_type'), updates, errors)
|
||||
if (d.config !== undefined) updates.config = d.config === null ? null : (typeof d.config === 'string' ? d.config : JSON.stringify(d.config))
|
||||
return errors.length > 0 ? { valid: false, errors } : { valid: true, errors: [], data: updates }
|
||||
}
|
||||
|
||||
export function validateUser(data) {
|
||||
if (!data || typeof data !== 'object') return { valid: false, errors: ['body required'] }
|
||||
const d = /** @type {Record<string, unknown>} */ (data)
|
||||
const errors = []
|
||||
const identifier = sanitizeIdentifier(d.identifier)
|
||||
const password = typeof d.password === 'string' ? d.password : ''
|
||||
const role = typeof d.role === 'string' ? d.role : ''
|
||||
if (!identifier) errors.push('identifier required')
|
||||
if (!password) errors.push('password required')
|
||||
if (!role || !ROLES.includes(role)) errors.push('role must be admin, leader, or member')
|
||||
return errors.length > 0 ? { valid: false, errors } : { valid: true, errors: [], data: { identifier, password, role: role || 'member' } }
|
||||
}
|
||||
|
||||
export function validateUpdateUser(data) {
|
||||
if (!data || typeof data !== 'object') return { valid: true, errors: [], data: {} }
|
||||
const d = /** @type {Record<string, unknown>} */ (data)
|
||||
const errors = []
|
||||
const updates = {}
|
||||
if (d.role !== undefined) {
|
||||
if (ROLES.includes(d.role)) updates.role = d.role
|
||||
else errors.push('role must be admin, leader, or member')
|
||||
}
|
||||
if (d.identifier !== undefined) {
|
||||
const identifier = sanitizeIdentifier(d.identifier)
|
||||
if (!identifier) errors.push('identifier cannot be empty')
|
||||
else updates.identifier = identifier
|
||||
}
|
||||
if (d.password !== undefined && d.password !== '') {
|
||||
if (typeof d.password !== 'string' || !d.password) errors.push('password cannot be empty')
|
||||
else updates.password = d.password
|
||||
}
|
||||
return errors.length > 0 ? { valid: false, errors } : { valid: true, errors: [], data: updates }
|
||||
}
|
||||
|
||||
export function validatePoi(data) {
|
||||
if (!data || typeof data !== 'object') return { valid: false, errors: ['body required'] }
|
||||
const d = /** @type {Record<string, unknown>} */ (data)
|
||||
const latCheck = validateNumber(d.lat, 'lat')
|
||||
const lngCheck = validateNumber(d.lng, 'lng')
|
||||
if (!latCheck.valid || !lngCheck.valid) return { valid: false, errors: ['lat and lng required as finite numbers'] }
|
||||
return {
|
||||
valid: true,
|
||||
errors: [],
|
||||
data: {
|
||||
lat: latCheck.value,
|
||||
lng: lngCheck.value,
|
||||
label: sanitizeLabel(d.label, 500),
|
||||
icon_type: validateEnum(d.iconType, POI_ICON_TYPES, 'iconType').value || 'pin',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function validateUpdatePoi(data) {
|
||||
if (!data || typeof data !== 'object') return { valid: true, errors: [], data: {} }
|
||||
const d = /** @type {Record<string, unknown>} */ (data)
|
||||
const errors = []
|
||||
const updates = {}
|
||||
if (d.label !== undefined) updates.label = sanitizeLabel(d.label, 500)
|
||||
handleField(d, 'iconType', v => validateEnum(v, POI_ICON_TYPES, 'iconType'), updates, errors, 'icon_type')
|
||||
handleField(d, 'lat', v => validateNumber(v, 'lat'), updates, errors)
|
||||
handleField(d, 'lng', v => validateNumber(v, 'lng'), updates, errors)
|
||||
return errors.length > 0 ? { valid: false, errors } : { valid: true, errors: [], data: updates }
|
||||
}
|
||||
Reference in New Issue
Block a user