import { and, desc, eq } from 'drizzle-orm'; import { Router } from 'express'; import { z } from 'zod'; import { db } from '../db/client'; import { deviceLinks, devices } from '../db/schema'; import { requireAuth } from '../middleware/auth'; import { requireDeviceAuth } from '../middleware/device-auth'; import { createDeviceToken } from '../utils/device-token'; const router = Router(); const roleSchema = z.enum(['camera', 'client']); const registerSchema = z.object({ name: z.string().trim().min(1).max(255).optional(), role: roleSchema, platform: z.string().trim().min(1).max(32).optional(), appVersion: z.string().trim().min(1).max(64).optional(), pushToken: z.string().trim().min(1).optional(), }); const updateSchema = z.object({ name: z.string().trim().min(1).max(255).optional(), role: roleSchema.optional(), platform: z.string().trim().min(1).max(32).optional(), appVersion: z.string().trim().min(1).max(64).optional(), pushToken: z.string().trim().min(1).optional(), status: z.enum(['online', 'offline']).optional(), }); const heartbeatSchema = z.object({ status: z.enum(['online', 'offline']).default('online'), }); const deviceParamSchema = z.object({ deviceId: z.string().uuid(), }); router.post('/register', requireAuth, async (req, res) => { const parsed = registerSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() }); return; } const authSession = req.auth; if (!authSession?.user?.id) { res.status(401).json({ message: 'Unauthorized' }); return; } const now = new Date(); const [device] = await db .insert(devices) .values({ userId: authSession.user.id, name: parsed.data.name, role: parsed.data.role, isCamera: parsed.data.role === 'camera', platform: parsed.data.platform, appVersion: parsed.data.appVersion, pushToken: parsed.data.pushToken, status: 'online', lastSeenAt: now, updatedAt: now, }) .returning({ id: devices.id, userId: devices.userId, name: devices.name, role: devices.role, status: devices.status, platform: devices.platform, appVersion: devices.appVersion, lastSeenAt: devices.lastSeenAt, createdAt: devices.createdAt, updatedAt: devices.updatedAt, }); if (!device) { res.status(500).json({ message: 'Unable to register device' }); return; } const oppositeRole = device.role === 'camera' ? 'client' : 'camera'; const oppositeDevices = await db.query.devices.findMany({ where: and(eq(devices.userId, device.userId), eq(devices.role, oppositeRole)), }); if (oppositeDevices.length > 0) { const linksToCreate = oppositeDevices.map((otherDevice) => ({ ownerUserId: device.userId, cameraDeviceId: device.role === 'camera' ? device.id : otherDevice.id, clientDeviceId: device.role === 'client' ? device.id : otherDevice.id, status: 'active' as const, })); await db.insert(deviceLinks).values(linksToCreate).onConflictDoNothing(); } const deviceToken = createDeviceToken({ userId: device.userId, deviceId: device.id, role: device.role as 'camera' | 'client', }); res.status(201).json({ message: 'Device registered', device, deviceToken, }); }); router.get('/', requireAuth, async (req, res) => { const authSession = req.auth; if (!authSession?.user?.id) { res.status(401).json({ message: 'Unauthorized' }); return; } const result = await db.query.devices.findMany({ where: eq(devices.userId, authSession.user.id), orderBy: [desc(devices.updatedAt)], }); res.json({ count: result.length, devices: result }); }); router.patch('/:deviceId', requireAuth, async (req, res) => { const parsedParams = deviceParamSchema.safeParse(req.params); if (!parsedParams.success) { res.status(400).json({ message: 'Invalid device id', errors: parsedParams.error.flatten() }); return; } const parsed = updateSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() }); return; } const authSession = req.auth; if (!authSession?.user?.id) { res.status(401).json({ message: 'Unauthorized' }); return; } const existing = await db.query.devices.findFirst({ where: and(eq(devices.id, parsedParams.data.deviceId), eq(devices.userId, authSession.user.id)), }); if (!existing) { res.status(404).json({ message: 'Device not found' }); return; } const role = parsed.data.role ?? existing.role; const now = new Date(); const [updated] = await db .update(devices) .set({ name: parsed.data.name ?? existing.name, role, isCamera: role === 'camera', platform: parsed.data.platform ?? existing.platform, appVersion: parsed.data.appVersion ?? existing.appVersion, pushToken: parsed.data.pushToken ?? existing.pushToken, status: parsed.data.status ?? existing.status, updatedAt: now, lastSeenAt: parsed.data.status === 'online' ? now : existing.lastSeenAt, }) .where(eq(devices.id, existing.id)) .returning(); res.json({ message: 'Device updated', device: updated }); }); router.post('/:deviceId/heartbeat', requireDeviceAuth, async (req, res) => { const parsedParams = deviceParamSchema.safeParse(req.params); if (!parsedParams.success) { res.status(400).json({ message: 'Invalid device id', errors: parsedParams.error.flatten() }); return; } const parsed = heartbeatSchema.safeParse(req.body ?? {}); if (!parsed.success) { res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() }); return; } const deviceAuth = req.deviceAuth; if (!deviceAuth) { res.status(401).json({ message: 'Unauthorized' }); return; } if (deviceAuth.deviceId !== parsedParams.data.deviceId) { res.status(403).json({ message: 'Device token does not match target device' }); return; } const now = new Date(); const [updated] = await db .update(devices) .set({ status: parsed.data.status, lastSeenAt: now, updatedAt: now, }) .where(and(eq(devices.id, parsedParams.data.deviceId), eq(devices.userId, deviceAuth.userId))) .returning({ id: devices.id, status: devices.status, lastSeenAt: devices.lastSeenAt, updatedAt: devices.updatedAt, }); if (!updated) { res.status(404).json({ message: 'Device not found' }); return; } res.json({ message: 'Heartbeat recorded', device: updated }); }); export default router;