All files / web/src/app/api/admin/blog-images/capture-snapshot route.ts

0% Statements 0/130
0% Branches 0/1
0% Functions 0/1
0% Lines 0/130

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131                                                                                                                                                                                                                                                                     
import fs from 'fs'
import path from 'path'
import { type NextRequest, NextResponse } from 'next/server'
import { withAuth } from '@/lib/auth/withAuth'

const heroHtmlDirectory = path.join(process.cwd(), 'content', 'blog', 'hero-html')
const embedHtmlDirectory = path.join(process.cwd(), 'content', 'blog', 'embed-html')

/**
 * POST /api/admin/blog-images/capture-snapshot
 *
 * Fetches an internal URL (e.g. /api/worksheets/preview), extracts content
 * from the response, and saves it as a hero-html or embed-html file for a blog post.
 *
 * When `embedId` is provided, saves to content/blog/embed-html/{slug}/{embedId}.html
 * Otherwise saves to content/blog/hero-html/{slug}.html
 */
export const POST = withAuth(
  async (request: NextRequest) => {
    let body: {
      slug: string
      url: string
      method?: string
      body?: unknown
      extractPath?: string
      embedId?: string
    }
    try {
      body = await request.json()
    } catch {
      return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
    }

    const { slug, url, method = 'GET', body: fetchBody, extractPath, embedId } = body

    if (!slug || !url) {
      return NextResponse.json({ error: 'slug and url are required' }, { status: 400 })
    }

    // Resolve url against the app's own origin
    const host = request.headers.get('host') || 'localhost:3000'
    const protocol = request.headers.get('x-forwarded-proto') || 'http'
    const origin = `${protocol}://${host}`
    const resolvedUrl = new URL(url, origin).toString()

    // Forward cookies for authentication
    const cookie = request.headers.get('cookie')

    try {
      const fetchOptions: RequestInit = {
        method: method.toUpperCase(),
        headers: {
          'Content-Type': 'application/json',
          ...(cookie ? { cookie } : {}),
        },
      }

      if (method.toUpperCase() === 'POST' && fetchBody !== undefined) {
        fetchOptions.body = JSON.stringify(fetchBody)
      }

      const res = await fetch(resolvedUrl, fetchOptions)

      if (!res.ok) {
        const text = await res.text().catch(() => 'Unknown error')
        return NextResponse.json(
          { error: `Upstream request failed (${res.status}): ${text.slice(0, 200)}` },
          { status: 502 }
        )
      }

      let content: string

      const contentType = res.headers.get('content-type') || ''
      if (contentType.includes('application/json') && extractPath) {
        const json = await res.json()
        // Drill into the object using dot-notation path (e.g. "pages.0")
        let current: unknown = json
        for (const segment of extractPath.split('.')) {
          if (current === null || current === undefined) break
          if (typeof current === 'object') {
            current = (current as Record<string, unknown>)[segment]
          } else {
            current = undefined
          }
        }
        if (typeof current !== 'string') {
          return NextResponse.json(
            {
              error: `extractPath "${extractPath}" did not resolve to a string (got ${typeof current})`,
            },
            { status: 422 }
          )
        }
        content = current
      } else {
        content = await res.text()
      }

      // Determine save path: embed-html/{slug}/{embedId}.html or hero-html/{slug}.html
      let targetDir: string
      let filePath: string
      if (embedId) {
        targetDir = path.join(embedHtmlDirectory, slug)
        filePath = path.join(targetDir, `${embedId}.html`)
      } else {
        targetDir = heroHtmlDirectory
        filePath = path.join(targetDir, `${slug}.html`)
      }

      // Ensure directory exists
      if (!fs.existsSync(targetDir)) {
        fs.mkdirSync(targetDir, { recursive: true })
      }

      fs.writeFileSync(filePath, content, 'utf8')

      return NextResponse.json({
        success: true,
        sizeBytes: Buffer.byteLength(content, 'utf8'),
      })
    } catch (err) {
      return NextResponse.json(
        { error: err instanceof Error ? err.message : 'Snapshot capture failed' },
        { status: 500 }
      )
    }
  },
  { role: 'admin' }
)