Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
import type { FastifyInstance } from 'fastify'
|
|
|
|
|
import { z } from 'zod'
|
2026-06-18 19:56:10 +00:00
|
|
|
import { db, logEvent } from '../db/index.js'
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
import { encryptSecret, decryptSecret } from '../db/crypto.js'
|
|
|
|
|
import { adapterRegistry } from '../integrations/registry.js'
|
2026-06-18 19:56:10 +00:00
|
|
|
import type { IntegrationType, Resource } from '../integrations/types.js'
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
|
|
|
|
|
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
|
2026-06-18 19:56:10 +00:00
|
|
|
logEvent('integration_created', `${name} integration added`, type)
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
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)
|
2026-06-18 19:56:10 +00:00
|
|
|
const existing = db.prepare('SELECT * FROM integrations WHERE id = ?').get(id) as IntegrationRow | undefined
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
db.prepare('DELETE FROM integrations WHERE id = ?').run(id)
|
2026-06-18 19:56:10 +00:00
|
|
|
if (existing) logEvent('integration_deleted', `${existing.name} integration removed`, existing.type)
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
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
|
|
|
|
|
)
|
2026-06-18 19:56:10 +00:00
|
|
|
logEvent('integration_tested', `${row.name} test ${result.ok ? 'succeeded' : 'failed'}`, row.type)
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
return result
|
|
|
|
|
})
|
2026-06-18 19:56:10 +00:00
|
|
|
|
|
|
|
|
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 }
|
|
|
|
|
})
|
Add backend skeleton: Fastify + SQLite API with auth and integrations
- Single-user JWT auth with a first-run /api/setup endpoint, gated by
GET /api/system/setup-status, to back an upcoming enrollment page
- SQLite schema for users, integrations, secrets (AES-256-GCM encrypted),
bookmarks, and bookmark categories
- Integration adapter registry with real health-check adapters for
Uptime Kuma and Docker, stubs for the rest, wired to
POST /api/integrations/:id/test
- CRUD routes for integrations and bookmarks
- backend/ as its own Docker service in docker-compose.yml, Vite dev
proxy for /api, .env.example for required secrets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BbJV5nm8KPVH1oNJYKpnoF
2026-06-18 19:04:48 +00:00
|
|
|
}
|