From 17594124b03d92716d4afbe77ac11296799dcb78 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Sat, 20 Jun 2026 07:51:44 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20mod=C3=A9ration=20IA=20automatique=20?= =?UTF-8?q?=C3=A0=20la=20publication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - contentModerationService branché dans /api/notes/publish - blocked → 403, publication refusée, toast d'explication - flagged → publié mais admins notifiés pour révision - safe → publication normale - PublishDialog gère les 3 cas (succès normal, flagged, blocked) - i18n FR/EN --- memento-note/app/api/notes/publish/route.ts | 43 ++++++++++++++++++- .../components/note-editor/publish-dialog.tsx | 13 +++++- memento-note/locales/en.json | 1 + memento-note/locales/fr.json | 1 + 4 files changed, 55 insertions(+), 3 deletions(-) diff --git a/memento-note/app/api/notes/publish/route.ts b/memento-note/app/api/notes/publish/route.ts index 8ff0bc8..8968d1b 100644 --- a/memento-note/app/api/notes/publish/route.ts +++ b/memento-note/app/api/notes/publish/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { auth } from '@/auth' import prisma from '@/lib/prisma' +import { contentModerationService } from '@/lib/ai/services/content-moderation.service' function generateSlug(title: string): string { const base = title @@ -22,11 +23,44 @@ export async function POST(request: NextRequest) { const note = await prisma.note.findFirst({ where: { id: noteId, userId: session.user.id }, - select: { id: true, title: true, publicSlug: true }, + select: { id: true, title: true, publicSlug: true, content: true }, }) if (!note) return NextResponse.json({ error: 'Not found' }, { status: 404 }) if (action === 'publish') { + // --- AI Moderation --- + let moderation + try { + moderation = await contentModerationService.moderate(note.title || '', note.content || '') + } catch { + moderation = { verdict: 'safe' as const, categories: ['safe'], reason: 'Moderation indisponible' } + } + + if (moderation.verdict === 'blocked') { + return NextResponse.json({ + error: 'blocked', + reason: moderation.reason, + categories: moderation.categories, + }, { status: 403 }) + } + + // flagged → publish but notify admins + if (moderation.verdict === 'flagged') { + const admins = await prisma.user.findMany({ where: { role: 'ADMIN' }, select: { id: true } }) + for (const admin of admins) { + await prisma.notification.create({ + data: { + userId: admin.id, + type: 'content_flagged', + title: 'Contenu sensible publié', + message: `La note "${note.title}" a été publiée avec un contenu potentiellement sensible: ${moderation.reason}`, + actionUrl: '/admin/published', + relatedId: note.id, + }, + }).catch(() => {}) + } + } + let slug = note.publicSlug if (!slug) { slug = generateSlug(note.title || 'note') @@ -37,7 +71,12 @@ export async function POST(request: NextRequest) { where: { id: noteId }, data: { isPublic: true, publicSlug: slug, publishedAt: new Date() }, }) - return NextResponse.json({ success: true, slug }) + + return NextResponse.json({ + success: true, + slug, + moderation: moderation.verdict === 'flagged' ? 'flagged' : undefined, + }) } if (action === 'unpublish') { diff --git a/memento-note/components/note-editor/publish-dialog.tsx b/memento-note/components/note-editor/publish-dialog.tsx index 353e9d9..de390b8 100644 --- a/memento-note/components/note-editor/publish-dialog.tsx +++ b/memento-note/components/note-editor/publish-dialog.tsx @@ -41,7 +41,18 @@ export function PublishDialog({ open, onClose, noteId, noteTitle, isPublic: init if (res.ok && data.slug) { setIsPublic(true) setSlug(data.slug) - toast.success(t('richTextEditor.publishSuccess') || 'Note publiée !') + if (data.moderation === 'flagged') { + toast.success(t('richTextEditor.publishSuccess') || 'Note publiée !', { + description: '⚠️ Un modérateur examinera le contenu sous peu.', + }) + } else { + toast.success(t('richTextEditor.publishSuccess') || 'Note publiée !') + } + } else if (data.error === 'blocked') { + toast.error(t('richTextEditor.publishBlocked') || 'Publication refusée', { + description: data.reason || 'Le contenu ne respecte pas les règles de publication.', + duration: 6000, + }) } else { toast.error(data.error || 'Erreur') } diff --git a/memento-note/locales/en.json b/memento-note/locales/en.json index f18a50c..8d69a05 100644 --- a/memento-note/locales/en.json +++ b/memento-note/locales/en.json @@ -2574,6 +2574,7 @@ "settingsPublishedEmpty": "No published notes yet.", "unpublish": "Unpublish", "unpublishSuccess": "Note unpublished", + "publishBlocked": "Publication refused", "slashSubPage": "Sub-page", "slashSubPageDesc": "Create a linked note inside this note", "exercisesLoading": "Generating exercises...", diff --git a/memento-note/locales/fr.json b/memento-note/locales/fr.json index a02f75d..111c714 100644 --- a/memento-note/locales/fr.json +++ b/memento-note/locales/fr.json @@ -2578,6 +2578,7 @@ "settingsPublishedEmpty": "Aucune note publiée pour le moment.", "unpublish": "Dépublier", "unpublishSuccess": "Note dépubliée", + "publishBlocked": "Publication refusée", "slashSubPage": "Sous-page", "slashSubPageDesc": "Créer une note liée dans cette note", "exercisesLoading": "Génération des exercices...",