feat: publication IA (magazine/brief/essay) + fixes critique
Publication IA: - 4 templates (magazine, brief, essay, simple) avec CSS riche - Rewrite IA (article/exercises/tutorial/reference/mixed) - Modération avec timeout 12s + fallback safe - Quotas publish_enhance par tier (basic=2, pro=15, business=100) - Détection contenu stale (hash) - Migration DB publishedContent/publishedTemplate/publishedSourceHash Fixes: - cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast - _isShared ajouté au type Note (champ virtuel serveur) - callout colors PDF export: extraction fonction pure testable - admin/published: guard note.userId null - Cmd+S fonctionne en mode dialog (pas seulement fullPage) i18n: - 23 clés publish* traduites dans les 15 locales - Extension Web Clipper: 13 locales mise à jour Tests: - callout-colors.test.ts (6 tests) - note-visible-in-view.test.ts (5 tests) - entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs - 199/199 tests passent Tracker: user-stories.md sync avec sprint-status.yaml
This commit is contained in:
116
memento-note/app/api/notebooks/[id]/publish/route.ts
Normal file
116
memento-note/app/api/notebooks/[id]/publish/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
const { id: notebookId } = await params
|
||||
const site = await prisma.notebookSite.findUnique({
|
||||
where: { notebookId },
|
||||
select: { slug: true, isPublic: true, publishedAt: true, template: true, selectedNoteIds: true },
|
||||
})
|
||||
return NextResponse.json({ site: site || null })
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
function toSlug(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.slice(0, 40)
|
||||
}
|
||||
|
||||
function generateNotebookSlug(name: string): string {
|
||||
const base = toSlug(name) || 'carnet'
|
||||
const suffix = Math.random().toString(36).slice(2, 7)
|
||||
return `${base}-${suffix}`
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: notebookId } = await params
|
||||
const { selectedNoteIds, template, description } = await request.json()
|
||||
|
||||
if (!selectedNoteIds || !Array.isArray(selectedNoteIds) || selectedNoteIds.length === 0) {
|
||||
return NextResponse.json({ error: 'selectedNoteIds is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: { id: notebookId, userId: session.user.id, trashedAt: null },
|
||||
select: { id: true, name: true, site: { select: { slug: true } } },
|
||||
})
|
||||
if (!notebook) return NextResponse.json({ error: 'Notebook not found' }, { status: 404 })
|
||||
|
||||
// Reuse existing slug or generate a new one
|
||||
const slug = notebook.site?.slug || generateNotebookSlug(notebook.name)
|
||||
|
||||
const site = await prisma.notebookSite.upsert({
|
||||
where: { notebookId },
|
||||
create: {
|
||||
notebookId,
|
||||
slug,
|
||||
isPublic: true,
|
||||
template: template || 'magazine',
|
||||
selectedNoteIds: JSON.stringify(selectedNoteIds),
|
||||
description: description || null,
|
||||
},
|
||||
update: {
|
||||
isPublic: true,
|
||||
template: template || 'magazine',
|
||||
selectedNoteIds: JSON.stringify(selectedNoteIds),
|
||||
description: description || null,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
|
||||
return NextResponse.json({ slug: site.slug, url: `/c/${site.slug}` })
|
||||
} catch (error: any) {
|
||||
console.error('[notebooks/publish] Error:', error)
|
||||
return NextResponse.json({ error: error.message || 'Failed to publish' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: notebookId } = await params
|
||||
|
||||
const notebook = await prisma.notebook.findFirst({
|
||||
where: { id: notebookId, userId: session.user.id },
|
||||
select: { id: true },
|
||||
})
|
||||
if (!notebook) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
||||
|
||||
await prisma.notebookSite.deleteMany({ where: { notebookId } })
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user