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
|
|
|
|
|
|
|
|
const bookmarkSchema = z.object({
|
|
|
|
|
categoryId: z.number().int().nullable().optional(),
|
|
|
|
|
title: z.string().min(1).max(128),
|
|
|
|
|
url: z.string().url(),
|
|
|
|
|
icon: z.string().optional(),
|
|
|
|
|
favorite: z.boolean().optional(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const categorySchema = z.object({
|
|
|
|
|
name: z.string().min(1).max(64),
|
|
|
|
|
icon: z.string().optional(),
|
|
|
|
|
sortOrder: z.number().int().optional(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
export async function bookmarkRoutes(app: FastifyInstance) {
|
|
|
|
|
app.addHook('onRequest', app.authenticate)
|
|
|
|
|
|
|
|
|
|
app.get('/api/bookmarks/categories', async () => {
|
|
|
|
|
const categories = db.prepare('SELECT * FROM bookmark_categories ORDER BY sort_order').all()
|
|
|
|
|
return { categories }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.post('/api/bookmarks/categories', async (req, reply) => {
|
|
|
|
|
const parsed = categorySchema.safeParse(req.body)
|
|
|
|
|
if (!parsed.success) return reply.code(400).send({ error: 'Invalid input' })
|
|
|
|
|
const { name, icon, sortOrder } = parsed.data
|
|
|
|
|
const result = db
|
|
|
|
|
.prepare('INSERT INTO bookmark_categories (name, icon, sort_order) VALUES (?, ?, ?)')
|
|
|
|
|
.run(name, icon ?? null, sortOrder ?? 0)
|
|
|
|
|
return reply.code(201).send({ id: result.lastInsertRowid })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.get('/api/bookmarks', async () => {
|
|
|
|
|
const bookmarks = db.prepare('SELECT * FROM bookmarks ORDER BY created_at DESC').all()
|
|
|
|
|
return { bookmarks }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.post('/api/bookmarks', async (req, reply) => {
|
|
|
|
|
const parsed = bookmarkSchema.safeParse(req.body)
|
|
|
|
|
if (!parsed.success) return reply.code(400).send({ error: 'Invalid input' })
|
|
|
|
|
const { categoryId, title, url, icon, favorite } = parsed.data
|
|
|
|
|
const result = db
|
|
|
|
|
.prepare(
|
|
|
|
|
'INSERT INTO bookmarks (category_id, title, url, icon, favorite) VALUES (?, ?, ?, ?, ?)'
|
|
|
|
|
)
|
|
|
|
|
.run(categoryId ?? null, title, url, icon ?? null, favorite ? 1 : 0)
|
2026-06-18 19:56:10 +00:00
|
|
|
logEvent('bookmark_created', `Bookmark added: ${title}`)
|
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({ id: result.lastInsertRowid })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.put('/api/bookmarks/:id', async (req, reply) => {
|
|
|
|
|
const id = Number((req.params as { id: string }).id)
|
|
|
|
|
const parsed = bookmarkSchema.partial().safeParse(req.body)
|
|
|
|
|
if (!parsed.success) return reply.code(400).send({ error: 'Invalid input' })
|
|
|
|
|
const existing = db.prepare('SELECT * FROM bookmarks WHERE id = ?').get(id) as
|
|
|
|
|
| { category_id: number | null; title: string; url: string; icon: string | null; favorite: number }
|
|
|
|
|
| undefined
|
|
|
|
|
if (!existing) return reply.code(404).send({ error: 'Not found' })
|
|
|
|
|
const categoryId = parsed.data.categoryId ?? existing.category_id
|
|
|
|
|
const title = parsed.data.title ?? existing.title
|
|
|
|
|
const url = parsed.data.url ?? existing.url
|
|
|
|
|
const icon = parsed.data.icon ?? existing.icon
|
|
|
|
|
const favorite = parsed.data.favorite ?? !!existing.favorite
|
|
|
|
|
db.prepare(
|
|
|
|
|
'UPDATE bookmarks SET category_id = ?, title = ?, url = ?, icon = ?, favorite = ? WHERE id = ?'
|
|
|
|
|
).run(categoryId, title, url, icon, favorite ? 1 : 0, id)
|
|
|
|
|
return { ok: true }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
app.delete('/api/bookmarks/: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 title FROM bookmarks WHERE id = ?').get(id) as { title: string } | 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 bookmarks WHERE id = ?').run(id)
|
2026-06-18 19:56:10 +00:00
|
|
|
if (existing) logEvent('bookmark_deleted', `Bookmark removed: ${existing.title}`)
|
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()
|
|
|
|
|
})
|
|
|
|
|
}
|