import { NextRequest, NextResponse } from 'next/server' import { getPayload } from 'payload' import configPromise from '@payload-config' import type { Media } from '@payload-types' import { resolveCoverImage } from '@/lib/agent-media' import { resolveAttachments, deleteFileDocs } from '@/lib/agent-files' export const dynamic = 'force-dynamic' /** Agent update endpoint: PUT /api/agent/posts/:slug */ export async function PUT(req: NextRequest, ctx: { params: Promise<{ slug: string }> }) { const key = process.env.AGENT_API_KEY if (!key || req.headers.get('x-agent-key') !== key) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const payload = await getPayload({ config: configPromise }) const { slug } = await ctx.params const body = (await req.json().catch(() => null)) as Record | null if (!body) return NextResponse.json({ error: 'invalid JSON body' }, { status: 400 }) const existing = await payload.find({ collection: 'posts', where: { slug: { equals: slug } }, limit: 1, }) const post = existing.docs[0] if (!post) return NextResponse.json({ error: 'not found' }, { status: 404 }) // Only writable fields; status is admin-only (except the explicit publish flag) const allowed = ['title', 'slug', 'dek', 'teaser', 'content', 'category', 'access', 'readingMinutes', 'cover'] as const const data: Record = {} for (const field of allowed) { if (field in body) data[field] = body[field] } // Explicit image payload (inline base64 or remote URL) → stored as a media document. let coverUrl: string | null = null if ('coverImage' in body && body.coverImage != null) { try { const cover = await resolveCoverImage(payload, body.coverImage) data.cover = cover.id coverUrl = cover.url } catch (err) { return NextResponse.json({ error: (err as Error).message }, { status: 400 }) } } // Per-post work files (inline base64) → stored as gated `files` documents. // Like coverImage, `attachments` is an input transform, not a raw field. // Sending it REPLACES the post's file set; omit it to keep existing files. let attachments: Awaited> = [] if ('attachments' in body && body.attachments != null) { try { attachments = await resolveAttachments(payload, body.attachments) data.attachments = attachments.map(a => a.id) } catch (err) { return NextResponse.json({ error: (err as Error).message }, { status: 400 }) } } // `publish` is an explicit opt-in flag; unpublishing stays admin-only. if (body.publish === true) { data.status = 'published' if (!post.publishedAt) data.publishedAt = new Date().toISOString() } let doc try { doc = await payload.update({ collection: 'posts', id: post.id, data, depth: 0 }) } catch (err) { // The post write failed — don't orphan the file docs created above // (the post keeps its previous attachments, which are untouched). if (attachments.length) await deleteFileDocs(payload, attachments.map(a => a.id)) return NextResponse.json({ error: (err as Error).message }, { status: 400 }) } if (!coverUrl && typeof data.cover === 'number') { const coverDoc: Media = await payload.findByID({ collection: 'media', id: data.cover }) coverUrl = coverDoc.url ?? null } return NextResponse.json({ id: doc.id, title: doc.title, slug: doc.slug, status: doc.status, access: doc.access, category: doc.category, cover: { id: data.cover ?? post.cover, url: coverUrl }, attachments: attachments.map(a => ({ id: a.id, filename: a.filename, access: a.access, url: `/api/files/${a.id}` })), }) }