Files
hotelsync/backend/src/routes/workstations.ts

264 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Рабочие места и сопряжение агентов
* 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)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const token = (fastify.jwt.sign as any)(
{
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