import type { FastifyInstance } from 'fastify' import { z } from 'zod' import { db, logEvent } from '../db/index.js' import { encryptSecret } from '../db/crypto.js' import { loadSecrets } from '../db/secrets.js' import { adapterRegistry } from '../integrations/registry.js' import type { IntegrationType, Resource } from '../integrations/types.js' const integrationTypes = [ 'proxmox', 'docker', 'netbird', 'cloudflare', 'aws', 'uptime_kuma', 'weather', 'ssh', 'remote_desktop', ] 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, } } 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 logEvent('integration_created', `${name} integration added`, type) 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) const existing = db.prepare('SELECT * FROM integrations WHERE id = ?').get(id) as IntegrationRow | undefined db.prepare('DELETE FROM integrations WHERE id = ?').run(id) if (existing) logEvent('integration_deleted', `${existing.name} integration removed`, existing.type) 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 ) logEvent('integration_tested', `${row.name} test ${result.ok ? 'succeeded' : 'failed'}`, row.type) return result }) app.get('/api/integrations/resources', async () => { const rows = db.prepare("SELECT * FROM integrations WHERE enabled = 1 AND status = 'connected'").all() as IntegrationRow[] const resources: (Resource & { integration: string })[] = [] for (const row of rows) { const adapter = adapterRegistry[row.type as IntegrationType] if (!adapter.listResources) continue const config = JSON.parse(row.config_json) const secrets = loadSecrets(row.id) try { const found = await adapter.listResources(config, secrets) for (const r of found) resources.push({ ...r, integration: row.name }) } catch { // adapter unreachable — skip, connection test already surfaces this } } return { resources } }) }