220 lines
5.9 KiB
TypeScript
220 lines
5.9 KiB
TypeScript
import { and, desc, eq } from 'drizzle-orm';
|
|
import { Router } from 'express';
|
|
import { z } from 'zod';
|
|
|
|
import { db } from '../db/client';
|
|
import { 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 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;
|