import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import prisma from '@/lib/prisma' import { syncNoteLabels } from '@/app/actions/notes' import { createNotification } from '@/app/actions/notifications' import { buildClipSourceFooter, clipFooterLocaleTag } from '@/lib/clip/extract-article' import { resolveClipLocale, wrapClipArticleHtml, applyRtlToHtmlBlocks } from '@/lib/clip/rtl-content' import { embeddingService } from '@/lib/ai/services/embedding.service' import { upsertNoteEmbedding } from '@/lib/embeddings' import DOMPurify from 'isomorphic-dompurify' function isBlockedUrl(url: string): boolean { try { const parsed = new URL(url) const hostname = parsed.hostname.toLowerCase() if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return true return ['localhost', '127.0.0.1', '0.0.0.0', '::1'].includes(hostname) } catch { return true } } export async function POST(request: NextRequest) { try { const session = await auth() if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const body = await request.json() const url = typeof body.url === 'string' ? body.url.trim() : '' const title = typeof body.title === 'string' ? body.title.trim().slice(0, 300) : null const rawContent = typeof body.content === 'string' ? body.content : '' const summary = typeof body.summary === 'string' ? body.summary.trim() : '' const notebookId = typeof body.notebookId === 'string' ? body.notebookId : null const tags = Array.isArray(body.tags) ? body.tags.filter((t: unknown): t is string => typeof t === 'string').slice(0, 5) : [] if (!url || isBlockedUrl(url)) { return NextResponse.json({ error: 'Invalid URL' }, { status: 400 }) } if (!rawContent.trim()) { return NextResponse.json({ error: 'Content required' }, { status: 400 }) } await prisma.user.upsert({ where: { id: session.user.id }, update: { ...(session.user.email ? { email: session.user.email } : {}), ...(session.user.name !== undefined ? { name: session.user.name } : {}), }, create: { id: session.user.id, email: session.user.email || `user-${session.user.id}@local.momento`, name: session.user.name || null, }, }) const domain = new URL(url).hostname.replace(/^www\./, '') const locale = resolveClipLocale(url, title || '', summary, rawContent.replace(/<[^>]+>/g, ' ')) const sanitizedContent = DOMPurify.sanitize(rawContent) const rtlBlocks = applyRtlToHtmlBlocks(sanitizedContent, locale) const summaryBlock = summary ? `
${DOMPurify.sanitize(summary)}
` : '' const footer = buildClipSourceFooter(domain, new Date(), clipFooterLocaleTag(locale.lang)) const bodyHtml = rtlBlocks.includes('clip-article--rtl') ? rtlBlocks : wrapClipArticleHtml(rtlBlocks, locale) const fullContent = `${summaryBlock}${bodyHtml}${footer}` const note = await prisma.note.create({ data: { userId: session.user.id, title: title || domain, content: fullContent, type: 'richtext', notebookId, sourceUrl: url, autoGenerated: true, ...(locale.lang ? { language: locale.lang } : {}), }, }) void (async () => { try { const { embedding } = await embeddingService.generateNoteEmbedding( title || domain, fullContent, ) if (embedding?.length) { await upsertNoteEmbedding(note.id, embedding) } } catch (error) { console.error('[clip/save] embedding generation failed:', error) } })() if (tags.length > 0) { await syncNoteLabels(note.id, tags, notebookId, session.user.id) } const noteUrl = `/home?openNote=${encodeURIComponent(note.id)}` await createNotification({ userId: session.user.id, type: 'clip', title: title || domain, message: summary || undefined, actionUrl: noteUrl, relatedId: note.id, }) return NextResponse.json({ noteId: note.id, noteUrl }) } catch (error) { console.error('[POST /api/clip/save]', error) return NextResponse.json({ error: 'Save failed' }, { status: 500 }) } }