dev_arc_aws/backend/src/routes/integrations.ts
Claude 3b920fcfb2
Replace mock data on Glance and Infrastructure with real backend data
Adds an events table + logEvent helper for a genuine activity log, and
a /api/integrations/resources aggregate endpoint backed by a new optional
listResources adapter method (implemented for Docker via its containers API).
StatusCards, MiddleRow, BottomRow, and Infrastructure now render real
integration/resource/event data instead of hardcoded numbers, with empty
states where no data source exists yet (AWS cost, historical trends).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:56:10 +00:00

163 lines
6.1 KiB
TypeScript

import type { FastifyInstance } from 'fastify'
import { z } from 'zod'
import { db, logEvent } from '../db/index.js'
import { encryptSecret, decryptSecret } from '../db/crypto.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',
] 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
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 }
})
}