94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
|
|
import type { FastifyInstance } from 'fastify'
|
||
|
|
import { z } from 'zod'
|
||
|
|
import { db, logEvent } from '../db/index.js'
|
||
|
|
import {
|
||
|
|
getStatus,
|
||
|
|
getTunnelRow,
|
||
|
|
startTunnel,
|
||
|
|
stopTunnel,
|
||
|
|
deleteTunnelRuntime,
|
||
|
|
type TunnelRow,
|
||
|
|
} from '../tunnels/manager.js'
|
||
|
|
|
||
|
|
const createSchema = z.object({
|
||
|
|
name: z.string().min(1).max(128),
|
||
|
|
integrationId: z.number().int(),
|
||
|
|
mode: z.enum(['local', 'remote', 'dynamic']),
|
||
|
|
sourcePort: z.number().int().min(1).max(65535),
|
||
|
|
endpointHost: z.string().default(''),
|
||
|
|
endpointPort: z.number().int().min(0).max(65535).default(0),
|
||
|
|
autoStart: z.boolean().default(false),
|
||
|
|
maxRetries: z.number().int().min(0).max(20).default(3),
|
||
|
|
retryIntervalMs: z.number().int().min(500).max(60000).default(5000),
|
||
|
|
})
|
||
|
|
|
||
|
|
function serialize(row: TunnelRow) {
|
||
|
|
return {
|
||
|
|
id: row.id,
|
||
|
|
name: row.name,
|
||
|
|
integrationId: row.integration_id,
|
||
|
|
mode: row.mode,
|
||
|
|
sourcePort: row.source_port,
|
||
|
|
endpointHost: row.endpoint_host,
|
||
|
|
endpointPort: row.endpoint_port,
|
||
|
|
autoStart: !!row.auto_start,
|
||
|
|
maxRetries: row.max_retries,
|
||
|
|
retryIntervalMs: row.retry_interval_ms,
|
||
|
|
createdAt: row.created_at,
|
||
|
|
...getStatus(row.id),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function tunnelRoutes(app: FastifyInstance) {
|
||
|
|
app.addHook('onRequest', app.authenticate)
|
||
|
|
|
||
|
|
app.get('/api/tunnels', async () => {
|
||
|
|
const rows = db.prepare('SELECT * FROM tunnels ORDER BY created_at').all() as TunnelRow[]
|
||
|
|
return { tunnels: rows.map(serialize) }
|
||
|
|
})
|
||
|
|
|
||
|
|
app.post('/api/tunnels', async (req, reply) => {
|
||
|
|
const parsed = createSchema.safeParse(req.body)
|
||
|
|
if (!parsed.success) {
|
||
|
|
return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
|
||
|
|
}
|
||
|
|
const d = parsed.data
|
||
|
|
const result = db
|
||
|
|
.prepare(
|
||
|
|
`INSERT INTO tunnels (name, integration_id, mode, source_port, endpoint_host, endpoint_port, auto_start, max_retries, retry_interval_ms)
|
||
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||
|
|
)
|
||
|
|
.run(d.name, d.integrationId, d.mode, d.sourcePort, d.endpointHost, d.endpointPort, d.autoStart ? 1 : 0, d.maxRetries, d.retryIntervalMs)
|
||
|
|
const id = Number(result.lastInsertRowid)
|
||
|
|
const row = getTunnelRow(id)!
|
||
|
|
logEvent('tunnel_created', `${d.name} tunnel added`, d.mode)
|
||
|
|
return reply.code(201).send({ tunnel: serialize(row) })
|
||
|
|
})
|
||
|
|
|
||
|
|
app.delete('/api/tunnels/:id', async (req, reply) => {
|
||
|
|
const id = Number((req.params as { id: string }).id)
|
||
|
|
const row = getTunnelRow(id)
|
||
|
|
if (!row) return reply.code(404).send({ error: 'Tunnel not found' })
|
||
|
|
deleteTunnelRuntime(id)
|
||
|
|
db.prepare('DELETE FROM tunnels WHERE id = ?').run(id)
|
||
|
|
logEvent('tunnel_deleted', `${row.name} tunnel removed`, row.mode)
|
||
|
|
return { ok: true }
|
||
|
|
})
|
||
|
|
|
||
|
|
app.post('/api/tunnels/:id/connect', async (req, reply) => {
|
||
|
|
const id = Number((req.params as { id: string }).id)
|
||
|
|
const row = getTunnelRow(id)
|
||
|
|
if (!row) return reply.code(404).send({ error: 'Tunnel not found' })
|
||
|
|
startTunnel(id)
|
||
|
|
return { ok: true, ...getStatus(id) }
|
||
|
|
})
|
||
|
|
|
||
|
|
app.post('/api/tunnels/:id/disconnect', async (req, reply) => {
|
||
|
|
const id = Number((req.params as { id: string }).id)
|
||
|
|
const row = getTunnelRow(id)
|
||
|
|
if (!row) return reply.code(404).send({ error: 'Tunnel not found' })
|
||
|
|
stopTunnel(id)
|
||
|
|
return { ok: true, ...getStatus(id) }
|
||
|
|
})
|
||
|
|
}
|