All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 7s
- Add brainstorm feature with collaborative canvas, AI idea generation, live cursors, playback, and export - Add PDF upload/extraction/ingestion pipeline with pgvector document search (RAG) - Add document Q&A overlay with streaming chat and PDF preview - Add note attachments UI with status polling, grid layout, and auto-scroll - Add task extraction AI tool and agent executor improvements - Fix NoteEmbedding missing updatedAt column, re-index 66 notes with 1536-dim embeddings - Fix brainstorm 'Create Note' button: add success toast and redirect to created note - Fix memory echo notification infinite polling - Fix chat route to always include document_search tool - Add brainstorm i18n keys across all 14 locales - Add socket server for real-time brainstorm collaboration - Add hierarchical notebook selector and organize notebook dialog improvements - Add sidebar brainstorm section with session management - Update prisma schema with brainstorm tables, attachments, and document chunks
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
import prisma from '@/lib/prisma'
|
|
|
|
export async function verifyParticipant(
|
|
sessionId: string,
|
|
userId: string,
|
|
requiredRole?: 'host' | 'editor' | 'viewer'
|
|
): Promise<{ isParticipant: boolean; role: string }> {
|
|
const participant = await prisma.brainstormParticipant.findFirst({
|
|
where: { sessionId, userId },
|
|
})
|
|
|
|
if (!participant) {
|
|
return { isParticipant: false, role: 'none' }
|
|
}
|
|
|
|
await prisma.brainstormParticipant.update({
|
|
where: { id: participant.id },
|
|
data: { lastSeenAt: new Date() },
|
|
})
|
|
|
|
if (requiredRole === 'host' && participant.role !== 'host') {
|
|
return { isParticipant: false, role: participant.role }
|
|
}
|
|
if (requiredRole === 'editor' && participant.role === 'viewer') {
|
|
return { isParticipant: false, role: participant.role }
|
|
}
|
|
|
|
return { isParticipant: true, role: participant.role }
|
|
}
|
|
|
|
export async function logActivity(
|
|
sessionId: string,
|
|
action: string,
|
|
userId?: string | null,
|
|
details?: Record<string, any>
|
|
) {
|
|
await prisma.brainstormActivity.create({
|
|
data: {
|
|
sessionId,
|
|
userId: userId || null,
|
|
action,
|
|
details: details ? JSON.stringify(details) : null,
|
|
},
|
|
})
|
|
}
|
|
|
|
// [UPDATE - SÉCURITÉ] Résoudre l'userId et le périmètre de notes autorisé pour les appels IA.
|
|
// - Hôte : accès complet à ses notes (publicNoteIds = null)
|
|
// - Invité : restreint aux contextNoteIds publics de la session (publicNoteIds = string[] | [])
|
|
export async function resolveAiContextUserId(
|
|
sessionId: string,
|
|
requestingUserId: string
|
|
): Promise<{ aiUserId: string; isGuest: boolean; publicNoteIds: string[] | null }> {
|
|
const session = await prisma.brainstormSession.findUnique({
|
|
where: { id: sessionId },
|
|
select: {
|
|
userId: true,
|
|
contextNoteIds: true,
|
|
},
|
|
})
|
|
|
|
if (!session) throw new Error('Session not found')
|
|
|
|
const isHost = session.userId === requestingUserId
|
|
if (isHost) {
|
|
return { aiUserId: requestingUserId, isGuest: false, publicNoteIds: null }
|
|
}
|
|
|
|
// Invité : on restreint aux contextNoteIds déclarés publics par l'hôte
|
|
const publicNoteIds: string[] = session.contextNoteIds
|
|
? (JSON.parse(session.contextNoteIds) as string[])
|
|
: []
|
|
|
|
return {
|
|
aiUserId: session.userId,
|
|
isGuest: true,
|
|
publicNoteIds: publicNoteIds.length > 0 ? publicNoteIds : [],
|
|
}
|
|
}
|
|
|
|
// [UPDATE - SÉCURITÉ] Sanitize les notes injectées dans un prompt IA pour un invité.
|
|
// Tronque le contenu et masque les entités nommées (Prénom Nom) avec [Person].
|
|
export function sanitizeNotesForGuest(
|
|
notes: { id: string; title: string | null; summary: string }[]
|
|
): { id: string; title: string; summary: string }[] {
|
|
const namedEntityRe = /\b[A-ZÀ-Ü][a-zà-ü]+ [A-ZÀ-Ü][a-zà-ü]+\b/g
|
|
return notes.map(n => ({
|
|
id: n.id,
|
|
title: (n.title || 'Note').replace(namedEntityRe, '[Person]'),
|
|
summary: n.summary.slice(0, 80).replace(namedEntityRe, '[Person]') + '…',
|
|
}))
|
|
}
|
|
|
|
export async function captureSnapshot(
|
|
sessionId: string,
|
|
label: string,
|
|
activityId?: string
|
|
): Promise<void> {
|
|
const ideas = await prisma.brainstormIdea.findMany({
|
|
where: { sessionId, status: 'active' },
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
waveNumber: true,
|
|
positionX: true,
|
|
positionY: true,
|
|
parentIdeaId: true,
|
|
noveltyScore: true,
|
|
createdByType: true,
|
|
status: true,
|
|
},
|
|
orderBy: [{ waveNumber: 'asc' }, { createdAt: 'asc' }],
|
|
})
|
|
|
|
const maxStep = await prisma.brainstormSnapshot.findFirst({
|
|
where: { sessionId },
|
|
orderBy: { step: 'desc' },
|
|
select: { step: true },
|
|
})
|
|
|
|
await prisma.brainstormSnapshot.create({
|
|
data: {
|
|
sessionId,
|
|
activityId: activityId || null,
|
|
step: (maxStep?.step || 0) + 1,
|
|
label,
|
|
ideaGraph: JSON.stringify(ideas),
|
|
},
|
|
})
|
|
}
|