88 lines
3.6 KiB
JavaScript
88 lines
3.6 KiB
JavaScript
const DEVICE_TYPES = Object.freeze(['alpr', 'nvr', 'doorbell', 'feed', 'traffic', 'ip', 'drone'])
|
|
const SOURCE_TYPES = Object.freeze(['mjpeg', 'hls'])
|
|
|
|
const sanitizeStreamUrl = (url) => {
|
|
if (typeof url !== 'string' || !url.trim()) return ''
|
|
const u = url.trim()
|
|
return (u.startsWith('https://') || u.startsWith('http://')) ? u : ''
|
|
}
|
|
|
|
/** @typedef {{ id: string, name: string, device_type: string, vendor: string | null, lat: number, lng: number, stream_url: string, source_type: string, config: string | null }} DeviceRow */
|
|
|
|
/**
|
|
* @param {string} s
|
|
* @returns {string} 'mjpeg' or 'hls'
|
|
*/
|
|
function normalizeSourceType(s) {
|
|
return SOURCE_TYPES.includes(s) ? s : 'mjpeg'
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} row
|
|
* @returns {DeviceRow | null} Normalized device row or null if invalid
|
|
*/
|
|
export function rowToDevice(row) {
|
|
if (!row || typeof row !== 'object') return null
|
|
const r = /** @type {Record<string, unknown>} */ (row)
|
|
if (typeof r.id !== 'string' || typeof r.name !== 'string' || typeof r.device_type !== 'string') return null
|
|
if (typeof r.lat !== 'number' && typeof r.lat !== 'string') return null
|
|
if (typeof r.lng !== 'number' && typeof r.lng !== 'string') return null
|
|
const lat = Number(r.lat)
|
|
const lng = Number(r.lng)
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null
|
|
return {
|
|
id: r.id,
|
|
name: r.name,
|
|
device_type: r.device_type,
|
|
vendor: typeof r.vendor === 'string' ? r.vendor : null,
|
|
lat,
|
|
lng,
|
|
stream_url: typeof r.stream_url === 'string' ? r.stream_url : '',
|
|
source_type: normalizeSourceType(r.source_type),
|
|
config: typeof r.config === 'string' ? r.config : null,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize device for API response (safe stream URL, valid sourceType).
|
|
* @param {DeviceRow} device
|
|
* @returns {{ id: string, name: string, device_type: string, vendor: string | null, lat: number, lng: number, streamUrl: string, sourceType: string, config: string | null }} Sanitized device for API response
|
|
*/
|
|
export function sanitizeDeviceForResponse(device) {
|
|
return {
|
|
id: device.id,
|
|
name: device.name,
|
|
device_type: device.device_type,
|
|
vendor: device.vendor,
|
|
lat: device.lat,
|
|
lng: device.lng,
|
|
streamUrl: sanitizeStreamUrl(device.stream_url),
|
|
sourceType: normalizeSourceType(device.source_type),
|
|
config: device.config,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate and normalize device body for POST.
|
|
* @param {unknown} body
|
|
* @returns {{ name: string, device_type: string, vendor: string | null, lat: number, lng: number, stream_url: string, source_type: string, config: string | null }} Validated and normalized body fields
|
|
*/
|
|
export function validateDeviceBody(body) {
|
|
if (!body || typeof body !== 'object') throw createError({ statusCode: 400, message: 'body required' })
|
|
const b = /** @type {Record<string, unknown>} */ (body)
|
|
const name = typeof b.name === 'string' ? b.name.trim() || '' : ''
|
|
const deviceType = typeof b.device_type === 'string' && DEVICE_TYPES.includes(b.device_type) ? b.device_type : 'feed'
|
|
const vendor = typeof b.vendor === 'string' ? b.vendor.trim() || null : null
|
|
const lat = Number(b.lat)
|
|
const lng = Number(b.lng)
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
|
throw createError({ statusCode: 400, message: 'lat and lng required as finite numbers' })
|
|
}
|
|
const streamUrl = typeof b.stream_url === 'string' ? b.stream_url.trim() : ''
|
|
const sourceType = normalizeSourceType(b.source_type)
|
|
const config = typeof b.config === 'string' ? b.config : (b.config != null ? JSON.stringify(b.config) : null)
|
|
return { name, device_type: deviceType, vendor, lat, lng, stream_url: streamUrl, source_type: sourceType, config }
|
|
}
|
|
|
|
export { DEVICE_TYPES, SOURCE_TYPES }
|