diff --git a/src/hooks.server.ts b/src/hooks.server.ts index dba289a..f2ee656 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -23,6 +23,47 @@ function accessNotConfigured(): Response { } export const handle: Handle = async ({ event, resolve }) => { + if (event.url.pathname.startsWith('/api/v1/')) { + const authHeader = event.request.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return new Response(JSON.stringify({ error: 'Unauthorized: Missing or invalid token' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + + const token = authHeader.slice('Bearer '.length).trim(); + const db = event.platform?.env.DB; + if (!db) { + return new Response(JSON.stringify({ error: 'Database not available' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + + try { + const result = await db + .prepare("SELECT value FROM settings WHERE key = 'agent.api_token'") + .first<{ value: string }>(); + + const validToken = result?.value; + if (!validToken || token !== validToken) { + return new Response(JSON.stringify({ error: 'Unauthorized: Invalid token' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }); + } + } catch (err) { + console.error('API token verification failed', err); + return new Response(JSON.stringify({ error: 'Internal Server Error' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }); + } + + return resolve(event); + } + if (!event.url.pathname.startsWith('/admin')) { return resolve(event); } diff --git a/src/routes/api/v1/posts/+server.ts b/src/routes/api/v1/posts/+server.ts new file mode 100644 index 0000000..a14fca6 --- /dev/null +++ b/src/routes/api/v1/posts/+server.ts @@ -0,0 +1,94 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ platform, url }) => { + const db = platform?.env.DB; + if (!db) return json({ error: 'Database not connected' }, { status: 500 }); + + const limit = parseInt(url.searchParams.get('limit') || '10', 10); + const offset = parseInt(url.searchParams.get('offset') || '0', 10); + + try { + const { results } = await db + .prepare( + ` + SELECT p.id, p.slug, p.title, p.published_at, p.updated_at, c.slug as category_slug + FROM posts p + LEFT JOIN categories c ON p.category_id = c.id + ORDER BY p.published_at DESC + LIMIT ? OFFSET ? + ` + ) + .bind(limit, offset) + .all(); + + const { results: countResult } = await db.prepare('SELECT COUNT(*) as total FROM posts').all(); + const total = (countResult[0] as { total: number })?.total || 0; + + return json({ + data: results, + meta: { + total, + limit, + offset + } + }); + } catch (err: unknown) { + console.error('Failed to get posts', err); + return json({ error: 'Failed to retrieve posts' }, { status: 500 }); + } +}; + +export const POST: RequestHandler = async ({ request, platform }) => { + const db = platform?.env.DB; + if (!db) return json({ error: 'Database not connected' }, { status: 500 }); + + try { + const data = (await request.json()) as Record; + const { slug, title, content, category_slug } = data as { + slug?: string; + title?: string; + content?: string; + category_slug?: string; + }; + + if (!slug || !title || !content) { + return json({ error: 'slug, title, and content are required' }, { status: 400 }); + } + + // Get default admin user id (assuming id=1 for simplicity as a bot) + const authorId = 1; + + let categoryId = null; + if (category_slug) { + const catResult = await db + .prepare('SELECT id FROM categories WHERE slug = ?') + .bind(category_slug) + .first<{ id: number }>(); + if (catResult) categoryId = catResult.id; + } + + const result = await db + .prepare( + ` + INSERT INTO posts (slug, title, content, author_id, category_id, published_at, updated_at) + VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ` + ) + .bind(slug, title, content, authorId, categoryId) + .run(); + + if (result.success) { + return json({ success: true, slug, title }); + } else { + return json({ error: 'Failed to insert post' }, { status: 500 }); + } + } catch (err: unknown) { + console.error('Failed to create post', err); + // Duplicate slug error handling + if (err instanceof Error && err.message.includes('UNIQUE constraint failed')) { + return json({ error: 'Slug already exists' }, { status: 409 }); + } + return json({ error: 'Invalid request data or server error' }, { status: 500 }); + } +}; diff --git a/src/routes/api/v1/posts/[slug]/+server.ts b/src/routes/api/v1/posts/[slug]/+server.ts new file mode 100644 index 0000000..6b2cbff --- /dev/null +++ b/src/routes/api/v1/posts/[slug]/+server.ts @@ -0,0 +1,104 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ params, platform }) => { + const db = platform?.env.DB; + if (!db) return json({ error: 'Database not connected' }, { status: 500 }); + + const { slug } = params; + + try { + const post = await db + .prepare( + ` + SELECT p.id, p.slug, p.title, p.content, p.published_at, p.updated_at, c.slug as category_slug + FROM posts p + LEFT JOIN categories c ON p.category_id = c.id + WHERE p.slug = ? + ` + ) + .bind(slug) + .first(); + + if (!post) { + return json({ error: 'Post not found' }, { status: 404 }); + } + + return json({ data: post }); + } catch (err: unknown) { + console.error('Failed to get post', err); + return json({ error: 'Failed to retrieve post' }, { status: 500 }); + } +}; + +export const PUT: RequestHandler = async ({ params, request, platform }) => { + const db = platform?.env.DB; + if (!db) return json({ error: 'Database not connected' }, { status: 500 }); + + const { slug } = params; + + try { + const data = (await request.json()) as Record; + const { title, content, category_slug } = data as { + title?: string; + content?: string; + category_slug?: string; + }; + + if (!title && !content && !category_slug) { + return json({ error: 'No fields to update' }, { status: 400 }); + } + + // First, check if post exists + const existingPost = await db + .prepare('SELECT id FROM posts WHERE slug = ?') + .bind(slug) + .first<{ id: number }>(); + if (!existingPost) { + return json({ error: 'Post not found' }, { status: 404 }); + } + + const updates: string[] = []; + const bindings: unknown[] = []; + + if (title) { + updates.push('title = ?'); + bindings.push(title); + } + if (content) { + updates.push('content = ?'); + bindings.push(content); + } + if (category_slug !== undefined) { + let categoryId = null; + if (category_slug) { + const catResult = await db + .prepare('SELECT id FROM categories WHERE slug = ?') + .bind(category_slug) + .first<{ id: number }>(); + if (catResult) categoryId = catResult.id; + } + updates.push('category_id = ?'); + bindings.push(categoryId); + } + + updates.push('updated_at = CURRENT_TIMESTAMP'); + bindings.push(slug); // for WHERE clause + + const query = `UPDATE posts SET ${updates.join(', ')} WHERE slug = ?`; + + const result = await db + .prepare(query) + .bind(...bindings) + .run(); + + if (result.success) { + return json({ success: true, slug }); + } else { + return json({ error: 'Failed to update post' }, { status: 500 }); + } + } catch (err: unknown) { + console.error('Failed to update post', err); + return json({ error: 'Invalid request data or server error' }, { status: 500 }); + } +};