dev_arc_aws/backend/src/routes/integrations.ts

142 lines
4.9 KiB
TypeScript
Raw Normal View History

import type { FastifyInstance } from 'fastify'
import { z } from 'zod'
import { db } from '../db/index.js'
import { encryptSecret, decryptSecret } from '../db/crypto.js'
import { adapterRegistry } from '../integrations/registry.js'
import type { IntegrationType } from '../integrations/types.js'
const integrationTypes = [
'proxmox',
'docker',
'netbird',
'cloudflare',
'aws',
'uptime_kuma',
'weather',
] as const
const createSchema = z.object({
type: z.enum(integrationTypes),
name: z.string().min(1).max(128),
config: z.record(z.string(), z.string()).default({}),
secrets: z.record(z.string(), z.string()).default({}),
})
interface IntegrationRow {
id: number
type: string
name: string
enabled: number
status: string
config_json: string
last_checked_at: string | null
created_at: string
}
function serialize(row: IntegrationRow) {
return {
id: row.id,
type: row.type,
name: row.name,
enabled: !!row.enabled,
status: row.status,
config: JSON.parse(row.config_json),
lastCheckedAt: row.last_checked_at,
createdAt: row.created_at,
}
}
function loadSecrets(integrationId: number): Record<string, string> {
const rows = db
.prepare('SELECT key, value_encrypted FROM secrets WHERE integration_id = ?')
.all(integrationId) as { key: string; value_encrypted: string }[]
const out: Record<string, string> = {}
for (const row of rows) out[row.key] = decryptSecret(row.value_encrypted)
return out
}
export async function integrationRoutes(app: FastifyInstance) {
app.addHook('onRequest', app.authenticate)
app.get('/api/integrations', async () => {
const rows = db.prepare('SELECT * FROM integrations ORDER BY created_at').all() as IntegrationRow[]
return { integrations: rows.map(serialize) }
})
app.post('/api/integrations', 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 { type, name, config, secrets } = parsed.data
const result = db
.prepare('INSERT INTO integrations (type, name, config_json) VALUES (?, ?, ?)')
.run(type, name, JSON.stringify(config))
const integrationId = Number(result.lastInsertRowid)
const insertSecret = db.prepare(
'INSERT INTO secrets (integration_id, key, value_encrypted) VALUES (?, ?, ?)'
)
for (const [key, value] of Object.entries(secrets)) {
insertSecret.run(integrationId, key, encryptSecret(value))
}
const row = db.prepare('SELECT * FROM integrations WHERE id = ?').get(integrationId) as IntegrationRow
return reply.code(201).send({ integration: serialize(row) })
})
app.put('/api/integrations/:id', async (req, reply) => {
const id = Number((req.params as { id: string }).id)
const parsed = createSchema.partial().safeParse(req.body)
if (!parsed.success) {
return reply.code(400).send({ error: parsed.error.issues[0]?.message ?? 'Invalid input' })
}
const existing = db.prepare('SELECT * FROM integrations WHERE id = ?').get(id) as
| IntegrationRow
| undefined
if (!existing) return reply.code(404).send({ error: 'Not found' })
const name = parsed.data.name ?? existing.name
const config = parsed.data.config ?? JSON.parse(existing.config_json)
db.prepare('UPDATE integrations SET name = ?, config_json = ? WHERE id = ?').run(
name,
JSON.stringify(config),
id
)
if (parsed.data.secrets) {
const upsert = db.prepare(
`INSERT INTO secrets (integration_id, key, value_encrypted) VALUES (?, ?, ?)
ON CONFLICT(integration_id, key) DO UPDATE SET value_encrypted = excluded.value_encrypted`
)
for (const [key, value] of Object.entries(parsed.data.secrets)) {
upsert.run(id, key, encryptSecret(value))
}
}
const row = db.prepare('SELECT * FROM integrations WHERE id = ?').get(id) as IntegrationRow
return { integration: serialize(row) }
})
app.delete('/api/integrations/:id', async (req, reply) => {
const id = Number((req.params as { id: string }).id)
db.prepare('DELETE FROM integrations WHERE id = ?').run(id)
return reply.code(204).send()
})
app.post('/api/integrations/:id/test', async (req, reply) => {
const id = Number((req.params as { id: string }).id)
const row = db.prepare('SELECT * FROM integrations WHERE id = ?').get(id) as
| IntegrationRow
| undefined
if (!row) return reply.code(404).send({ error: 'Not found' })
const adapter = adapterRegistry[row.type as IntegrationType]
const config = JSON.parse(row.config_json)
const secrets = loadSecrets(id)
const result = await adapter.testConnection(config, secrets)
const status = result.ok ? 'connected' : 'error'
db.prepare("UPDATE integrations SET status = ?, last_checked_at = datetime('now') WHERE id = ?").run(
status,
id
)
return result
})
}