feat: workstations API + agent WebSocket — паркинг агентов и управление оборудованием
- migration 041: таблицы workstations, agent_pair_codes, workstation_devices - routes/workstations: CRUD рабочих мест, генерация 6-значного кода, CRUD устройств - POST /api/agent/pair: сопряжение агента по коду → JWT токен - agent-ws.ts: WebSocket /ws/agent для постоянного соединения агентов, heartbeat 30s, онлайн/оффлайн статус в БД Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
33
backend/migrations/041_workstations.sql
Normal file
33
backend/migrations/041_workstations.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- Рабочие места (агенты Windows)
|
||||||
|
CREATE TABLE IF NOT EXISTS workstations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
hotel_id UUID NOT NULL REFERENCES hotels(id) ON DELETE CASCADE,
|
||||||
|
agent_id VARCHAR(64) UNIQUE, -- UUID генерируемый агентом
|
||||||
|
name VARCHAR(100) NOT NULL, -- «Ресепшн 1», «Бар»
|
||||||
|
hostname VARCHAR(255), -- имя Windows-компьютера
|
||||||
|
ip_address INET,
|
||||||
|
is_online BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
last_seen TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Коды сопряжения (6 цифр, TTL 10 минут)
|
||||||
|
CREATE TABLE IF NOT EXISTS agent_pair_codes (
|
||||||
|
code CHAR(6) PRIMARY KEY,
|
||||||
|
workstation_id UUID NOT NULL REFERENCES workstations(id) ON DELETE CASCADE,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
used BOOLEAN NOT NULL DEFAULT false
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Устройства привязанные к рабочему месту
|
||||||
|
CREATE TABLE IF NOT EXISTS workstation_devices (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
workstation_id UUID NOT NULL REFERENCES workstations(id) ON DELETE CASCADE,
|
||||||
|
type VARCHAR(20) NOT NULL CHECK (type IN ('kkt', 'printer')),
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
connection VARCHAR(20) NOT NULL CHECK (connection IN ('usb', 'network')),
|
||||||
|
network_host VARCHAR(255),
|
||||||
|
network_port INTEGER,
|
||||||
|
purpose VARCHAR(30) CHECK (purpose IN ('fiscal', 'kitchen', 'bar', 'receipt', 'other')),
|
||||||
|
config JSONB NOT NULL DEFAULT '{}'
|
||||||
|
);
|
||||||
133
backend/src/agent-ws.ts
Normal file
133
backend/src/agent-ws.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket сервер для агентов
|
||||||
|
* Агенты подключаются по wss://api.hotelsync.ru/ws/agent
|
||||||
|
* и остаются подключёнными для получения команд
|
||||||
|
*/
|
||||||
|
import { FastifyInstance } from 'fastify'
|
||||||
|
import { WebSocket } from 'ws'
|
||||||
|
import { db } from './db'
|
||||||
|
|
||||||
|
interface AgentSocket extends WebSocket {
|
||||||
|
agentId?: string
|
||||||
|
workstationId?: string
|
||||||
|
hotelSlug?: string
|
||||||
|
isAlive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// Карта: workstation_id → сокет
|
||||||
|
const agentSockets = new Map<string, AgentSocket>()
|
||||||
|
|
||||||
|
export function getAgentSocket(workstationId: string): AgentSocket | undefined {
|
||||||
|
return agentSockets.get(workstationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendCommand(
|
||||||
|
workstationId: string,
|
||||||
|
command: object,
|
||||||
|
): Promise<{ ok: boolean; error?: string }> {
|
||||||
|
const socket = agentSockets.get(workstationId)
|
||||||
|
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||||
|
return { ok: false, error: 'Агент не подключён' }
|
||||||
|
}
|
||||||
|
socket.send(JSON.stringify(command))
|
||||||
|
return { ok: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerAgentWs(fastify: FastifyInstance) {
|
||||||
|
// @ts-ignore — fastify.websocketServer добавляется плагином @fastify/websocket
|
||||||
|
const wss = fastify.websocketServer
|
||||||
|
|
||||||
|
// Heartbeat каждые 30 секунд
|
||||||
|
const heartbeat = setInterval(() => {
|
||||||
|
agentSockets.forEach((socket, wsId) => {
|
||||||
|
if (!socket.isAlive) {
|
||||||
|
console.log(`[agent-ws] Disconnect (timeout): ${wsId}`)
|
||||||
|
socket.terminate()
|
||||||
|
agentSockets.delete(wsId)
|
||||||
|
markOffline(wsId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socket.isAlive = false
|
||||||
|
socket.ping()
|
||||||
|
})
|
||||||
|
}, 30000)
|
||||||
|
|
||||||
|
wss.on('close', () => clearInterval(heartbeat))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markOffline(workstationId: string) {
|
||||||
|
await db.query(
|
||||||
|
'UPDATE workstations SET is_online = false WHERE id = $1',
|
||||||
|
[workstationId],
|
||||||
|
).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markOnline(workstationId: string, ip?: string) {
|
||||||
|
await db.query(
|
||||||
|
`UPDATE workstations SET is_online = true, last_seen = NOW(), ip_address = $2 WHERE id = $1`,
|
||||||
|
[workstationId, ip ?? null],
|
||||||
|
).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Регистрируем route для WebSocket агентов
|
||||||
|
export function setupAgentWsRoute(fastify: FastifyInstance) {
|
||||||
|
fastify.get('/ws/agent', { websocket: true }, async (socket: AgentSocket, req) => {
|
||||||
|
// Аутентификация по токену из заголовка
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization ?? ''
|
||||||
|
const token = authHeader.replace('Bearer ', '')
|
||||||
|
if (!token) { socket.close(1008, 'Unauthorized'); return }
|
||||||
|
|
||||||
|
const payload = fastify.jwt.verify(token) as {
|
||||||
|
sub: string; role: string; workstation_id: string; hotel_slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.role !== 'agent') { socket.close(1008, 'Forbidden'); return }
|
||||||
|
|
||||||
|
socket.agentId = payload.sub
|
||||||
|
socket.workstationId = payload.workstation_id
|
||||||
|
socket.hotelSlug = payload.hotel_slug
|
||||||
|
socket.isAlive = true
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
socket.close(1008, 'Invalid token')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsId = socket.workstationId!
|
||||||
|
console.log(`[agent-ws] Connected: ${wsId}`)
|
||||||
|
|
||||||
|
// Если уже было соединение — закрываем старое
|
||||||
|
const existing = agentSockets.get(wsId)
|
||||||
|
if (existing) existing.terminate()
|
||||||
|
|
||||||
|
agentSockets.set(wsId, socket)
|
||||||
|
await markOnline(wsId, req.ip)
|
||||||
|
|
||||||
|
socket.on('pong', () => { socket.isAlive = true })
|
||||||
|
|
||||||
|
socket.on('message', (raw) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(raw.toString())
|
||||||
|
// Обрабатываем ответы агента (результаты команд)
|
||||||
|
if (msg.type === 'result') {
|
||||||
|
fastify.log.info({ workstation: wsId, result: msg }, 'Agent command result')
|
||||||
|
// TODO: можно эмитить в EventEmitter для ожидающих промисов
|
||||||
|
}
|
||||||
|
if (msg.type === 'hello') {
|
||||||
|
console.log(`[agent-ws] Hello from ${msg.agent_id} (${wsId})`)
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('close', () => {
|
||||||
|
console.log(`[agent-ws] Disconnected: ${wsId}`)
|
||||||
|
agentSockets.delete(wsId)
|
||||||
|
markOffline(wsId)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('error', (err) => {
|
||||||
|
console.error(`[agent-ws] Error ${wsId}:`, err.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -33,6 +33,8 @@ import notificationsRoutes from './routes/notifications'
|
|||||||
import scheduleRoutes from './routes/schedule'
|
import scheduleRoutes from './routes/schedule'
|
||||||
import loyaltyRoutes from './routes/loyalty'
|
import loyaltyRoutes from './routes/loyalty'
|
||||||
import chatRoutes from './routes/chat'
|
import chatRoutes from './routes/chat'
|
||||||
|
import workstationRoutes from './routes/workstations'
|
||||||
|
import { setupAgentWsRoute } from './agent-ws'
|
||||||
import { startJobs } from './jobs'
|
import { startJobs } from './jobs'
|
||||||
|
|
||||||
export async function buildApp() {
|
export async function buildApp() {
|
||||||
@@ -111,6 +113,8 @@ export async function buildApp() {
|
|||||||
await fastify.register(scheduleRoutes)
|
await fastify.register(scheduleRoutes)
|
||||||
await fastify.register(loyaltyRoutes)
|
await fastify.register(loyaltyRoutes)
|
||||||
await fastify.register(chatRoutes)
|
await fastify.register(chatRoutes)
|
||||||
|
await fastify.register(workstationRoutes)
|
||||||
|
setupAgentWsRoute(fastify)
|
||||||
|
|
||||||
startJobs()
|
startJobs()
|
||||||
|
|
||||||
|
|||||||
262
backend/src/routes/workstations.ts
Normal file
262
backend/src/routes/workstations.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
/**
|
||||||
|
* Рабочие места и сопряжение агентов
|
||||||
|
* Routes:
|
||||||
|
* GET /api/hotels/:slug/workstations
|
||||||
|
* POST /api/hotels/:slug/workstations
|
||||||
|
* PATCH /api/hotels/:slug/workstations/:id
|
||||||
|
* DELETE /api/hotels/:slug/workstations/:id
|
||||||
|
* POST /api/hotels/:slug/workstations/:id/pair-code
|
||||||
|
* GET /api/hotels/:slug/workstations/:id/devices
|
||||||
|
* POST /api/hotels/:slug/workstations/:id/devices
|
||||||
|
* PATCH /api/hotels/:slug/workstations/:id/devices/:deviceId
|
||||||
|
* DELETE /api/hotels/:slug/workstations/:id/devices/:deviceId
|
||||||
|
*
|
||||||
|
* POST /api/agent/pair (публичный — без auth)
|
||||||
|
*/
|
||||||
|
import { FastifyPluginAsync } from 'fastify'
|
||||||
|
import { db } from '../db'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
|
type SlugParam = { Params: { slug: string } }
|
||||||
|
type WsParam = { Params: { slug: string; id: string } }
|
||||||
|
type DevParam = { Params: { slug: string; id: string; deviceId: string } }
|
||||||
|
|
||||||
|
const workstationRoutes: FastifyPluginAsync = async (fastify) => {
|
||||||
|
const getHotelId = async (slug: string): Promise<string | undefined> => {
|
||||||
|
const { rows } = await db.query('SELECT id FROM hotels WHERE slug = $1', [slug])
|
||||||
|
return rows[0]?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const canManage = (userSlug: string | null, role: string, slug: string) =>
|
||||||
|
role === 'super_admin' || role === 'hotel_admin' || userSlug === slug
|
||||||
|
|
||||||
|
// ── GET /api/hotels/:slug/workstations ──────────────────────────────────────
|
||||||
|
fastify.get<SlugParam>('/api/hotels/:slug/workstations', { onRequest: [fastify.authenticate] }, async (req, reply) => {
|
||||||
|
const { slug } = req.params
|
||||||
|
if (!canManage(req.user.hotelSlug, req.user.role, slug))
|
||||||
|
return reply.code(403).send({ error: 'Forbidden' })
|
||||||
|
|
||||||
|
const hotelId = await getHotelId(slug)
|
||||||
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||||
|
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`SELECT w.*,
|
||||||
|
json_agg(d ORDER BY d.type, d.name) FILTER (WHERE d.id IS NOT NULL) AS devices
|
||||||
|
FROM workstations w
|
||||||
|
LEFT JOIN workstation_devices d ON d.workstation_id = w.id
|
||||||
|
WHERE w.hotel_id = $1
|
||||||
|
GROUP BY w.id
|
||||||
|
ORDER BY w.created_at`,
|
||||||
|
[hotelId],
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── POST /api/hotels/:slug/workstations ─────────────────────────────────────
|
||||||
|
fastify.post<SlugParam & { Body: { name: string } }>(
|
||||||
|
'/api/hotels/:slug/workstations',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
const { slug } = req.params
|
||||||
|
if (!canManage(req.user.hotelSlug, req.user.role, slug))
|
||||||
|
return reply.code(403).send({ error: 'Forbidden' })
|
||||||
|
|
||||||
|
const hotelId = await getHotelId(slug)
|
||||||
|
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
|
||||||
|
|
||||||
|
const { name } = req.body
|
||||||
|
if (!name?.trim()) return reply.code(400).send({ error: 'name required' })
|
||||||
|
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`INSERT INTO workstations (hotel_id, name) VALUES ($1, $2) RETURNING *`,
|
||||||
|
[hotelId, name.trim()],
|
||||||
|
)
|
||||||
|
return reply.code(201).send(rows[0])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PATCH /api/hotels/:slug/workstations/:id ────────────────────────────────
|
||||||
|
fastify.patch<WsParam & { Body: { name?: string } }>(
|
||||||
|
'/api/hotels/:slug/workstations/:id',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
const { slug, id } = req.params
|
||||||
|
if (!canManage(req.user.hotelSlug, req.user.role, slug))
|
||||||
|
return reply.code(403).send({ error: 'Forbidden' })
|
||||||
|
|
||||||
|
const { name } = req.body
|
||||||
|
if (!name?.trim()) return reply.code(400).send({ error: 'name required' })
|
||||||
|
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`UPDATE workstations SET name = $1 WHERE id = $2 RETURNING *`,
|
||||||
|
[name.trim(), id],
|
||||||
|
)
|
||||||
|
if (!rows[0]) return reply.code(404).send({ error: 'Not found' })
|
||||||
|
return rows[0]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── DELETE /api/hotels/:slug/workstations/:id ───────────────────────────────
|
||||||
|
fastify.delete<WsParam>(
|
||||||
|
'/api/hotels/:slug/workstations/:id',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
const { slug, id } = req.params
|
||||||
|
if (!canManage(req.user.hotelSlug, req.user.role, slug))
|
||||||
|
return reply.code(403).send({ error: 'Forbidden' })
|
||||||
|
|
||||||
|
await db.query('DELETE FROM workstations WHERE id = $1', [id])
|
||||||
|
return reply.code(204).send()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── POST /api/hotels/:slug/workstations/:id/pair-code ───────────────────────
|
||||||
|
fastify.post<WsParam>(
|
||||||
|
'/api/hotels/:slug/workstations/:id/pair-code',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
const { slug, id } = req.params
|
||||||
|
if (!canManage(req.user.hotelSlug, req.user.role, slug))
|
||||||
|
return reply.code(403).send({ error: 'Forbidden' })
|
||||||
|
|
||||||
|
// Удаляем старые неиспользованные коды для этого рабочего места
|
||||||
|
await db.query(
|
||||||
|
`DELETE FROM agent_pair_codes WHERE workstation_id = $1`,
|
||||||
|
[id],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Генерируем 6-значный код
|
||||||
|
const code = String(crypto.randomInt(100000, 999999))
|
||||||
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1000) // 10 минут
|
||||||
|
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO agent_pair_codes (code, workstation_id, expires_at) VALUES ($1, $2, $3)`,
|
||||||
|
[code, id, expiresAt],
|
||||||
|
)
|
||||||
|
|
||||||
|
return { code, expires_at: expiresAt }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── POST /api/agent/pair (без auth — вызывается агентом) ────────────────────
|
||||||
|
fastify.post<{ Body: { code: string; agent_id: string; hostname?: string } }>(
|
||||||
|
'/api/agent/pair',
|
||||||
|
async (req, reply) => {
|
||||||
|
const { code, agent_id, hostname } = req.body
|
||||||
|
if (!code || !agent_id) return reply.code(400).send({ error: 'code and agent_id required' })
|
||||||
|
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`SELECT pc.*, w.hotel_id, w.name AS workstation_name, h.slug
|
||||||
|
FROM agent_pair_codes pc
|
||||||
|
JOIN workstations w ON w.id = pc.workstation_id
|
||||||
|
JOIN hotels h ON h.id = w.hotel_id
|
||||||
|
WHERE pc.code = $1
|
||||||
|
AND pc.used = false
|
||||||
|
AND pc.expires_at > NOW()`,
|
||||||
|
[code],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!rows[0]) {
|
||||||
|
return reply.code(400).send({ error: 'Код недействителен или истёк' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = rows[0]
|
||||||
|
|
||||||
|
// Помечаем код как использованный
|
||||||
|
await db.query('UPDATE agent_pair_codes SET used = true WHERE code = $1', [code])
|
||||||
|
|
||||||
|
// Привязываем agent_id к рабочему месту
|
||||||
|
await db.query(
|
||||||
|
`UPDATE workstations SET agent_id = $1, hostname = $2, last_seen = NOW(), is_online = false
|
||||||
|
WHERE id = $3`,
|
||||||
|
[agent_id, hostname ?? null, row.workstation_id],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Выдаём токен агенту (подписываем как обычный JWT с role=agent)
|
||||||
|
const token = fastify.jwt.sign(
|
||||||
|
{
|
||||||
|
sub: agent_id,
|
||||||
|
role: 'agent',
|
||||||
|
workstation_id: row.workstation_id,
|
||||||
|
hotel_slug: row.slug,
|
||||||
|
},
|
||||||
|
{ expiresIn: '365d' },
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
workstation_id: row.workstation_id,
|
||||||
|
workstation_name: row.workstation_name,
|
||||||
|
hotel_slug: row.slug,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── GET /api/hotels/:slug/workstations/:id/devices ──────────────────────────
|
||||||
|
fastify.get<WsParam>('/api/hotels/:slug/workstations/:id/devices', { onRequest: [fastify.authenticate] }, async (req, reply) => {
|
||||||
|
const { slug, id } = req.params
|
||||||
|
if (!canManage(req.user.hotelSlug, req.user.role, slug))
|
||||||
|
return reply.code(403).send({ error: 'Forbidden' })
|
||||||
|
|
||||||
|
const { rows } = await db.query(
|
||||||
|
'SELECT * FROM workstation_devices WHERE workstation_id = $1 ORDER BY type, name',
|
||||||
|
[id],
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── POST /api/hotels/:slug/workstations/:id/devices ─────────────────────────
|
||||||
|
fastify.post<WsParam & { Body: {
|
||||||
|
type: string; name: string; connection: string
|
||||||
|
network_host?: string; network_port?: number; purpose?: string; config?: object
|
||||||
|
} }>(
|
||||||
|
'/api/hotels/:slug/workstations/:id/devices',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const { type, name, connection, network_host, network_port, purpose, config } = req.body
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`INSERT INTO workstation_devices (workstation_id, type, name, connection, network_host, network_port, purpose, config)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
|
||||||
|
[id, type, name, connection, network_host ?? null, network_port ?? null, purpose ?? 'other', config ?? {}],
|
||||||
|
)
|
||||||
|
return reply.code(201).send(rows[0])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── PATCH /api/hotels/:slug/workstations/:id/devices/:deviceId ──────────────
|
||||||
|
fastify.patch<DevParam & { Body: object }>(
|
||||||
|
'/api/hotels/:slug/workstations/:id/devices/:deviceId',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
const { deviceId } = req.params
|
||||||
|
const body = req.body as Record<string, unknown>
|
||||||
|
const fields = ['name', 'connection', 'network_host', 'network_port', 'purpose', 'config']
|
||||||
|
const sets: string[] = []
|
||||||
|
const vals: unknown[] = []
|
||||||
|
fields.forEach(f => {
|
||||||
|
if (f in body) { sets.push(`${f} = $${vals.length + 1}`); vals.push(body[f]) }
|
||||||
|
})
|
||||||
|
if (!sets.length) return reply.code(400).send({ error: 'Nothing to update' })
|
||||||
|
vals.push(deviceId)
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`UPDATE workstation_devices SET ${sets.join(', ')} WHERE id = $${vals.length} RETURNING *`,
|
||||||
|
vals,
|
||||||
|
)
|
||||||
|
if (!rows[0]) return reply.code(404).send({ error: 'Not found' })
|
||||||
|
return rows[0]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── DELETE /api/hotels/:slug/workstations/:id/devices/:deviceId ─────────────
|
||||||
|
fastify.delete<DevParam>(
|
||||||
|
'/api/hotels/:slug/workstations/:id/devices/:deviceId',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
await db.query('DELETE FROM workstation_devices WHERE id = $1', [req.params.deviceId])
|
||||||
|
return reply.code(204).send()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default workstationRoutes
|
||||||
Reference in New Issue
Block a user