All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m11s
Chat (AIChat floating widget): conversationId was never captured from the API response, so every message created a new conversation with no context. Now creates the conversation upfront before streaming (same pattern as ChatContainer) so the ID persists across messages. Note history: was stored globally in UserAISettings, so enabling history on one note enabled it for ALL notes. Now each Note has its own historyEnabled boolean field. The "Enable history" action only affects the specific note. A migration adds the column with default false. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
234 lines
5.9 KiB
TypeScript
234 lines
5.9 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import prisma from '@/lib/prisma'
|
|
import { auth } from '@/auth'
|
|
import { parseNote } from '@/lib/utils'
|
|
import {
|
|
createNoteHistorySnapshot,
|
|
getNoteHistoryMode,
|
|
isNoteHistoryEnabledForUser,
|
|
shouldCaptureHistorySnapshot,
|
|
shouldCreateAutoSnapshot,
|
|
} from '@/lib/note-history'
|
|
|
|
// GET /api/notes/[id] - Get a single note
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Unauthorized' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
try {
|
|
const { id } = await params
|
|
const note = await prisma.note.findUnique({
|
|
where: { id }
|
|
})
|
|
|
|
if (!note) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Note not found' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
if (note.userId !== session.user.id) {
|
|
const share = await prisma.noteShare.findUnique({
|
|
where: {
|
|
noteId_userId: {
|
|
noteId: note.id,
|
|
userId: session.user.id
|
|
}
|
|
}
|
|
})
|
|
if (!share || share.status !== 'accepted') {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Forbidden' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: parseNote(note)
|
|
})
|
|
} catch (error) {
|
|
console.error('Error fetching note:', error)
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Failed to fetch note' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
// PUT /api/notes/[id] - Update a note
|
|
export async function PUT(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Unauthorized' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
try {
|
|
const { id } = await params
|
|
|
|
const existingNote = await prisma.note.findUnique({
|
|
where: { id }
|
|
})
|
|
|
|
if (!existingNote) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Note not found' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
if (existingNote.userId !== session.user.id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Forbidden' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
|
|
const body = await request.json()
|
|
// Whitelist allowed fields to prevent mass assignment
|
|
const allowedFields = ['title', 'content', 'color', 'isPinned', 'isArchived', 'type', 'isMarkdown', 'size', 'notebookId']
|
|
const updateData: Record<string, any> = {}
|
|
for (const key of allowedFields) {
|
|
if (key in body) {
|
|
updateData[key] = body[key]
|
|
}
|
|
}
|
|
|
|
if ('checkItems' in body) {
|
|
updateData.checkItems = body.checkItems ?? null
|
|
}
|
|
if ('labels' in body) {
|
|
updateData.labels = body.labels ?? null
|
|
}
|
|
|
|
// Only update if data actually changed
|
|
const hasChanges = Object.keys(updateData).some((key) => {
|
|
const newValue = updateData[key]
|
|
const oldValue = (existingNote as any)[key]
|
|
// Handle arrays/objects by comparing JSON
|
|
if (typeof newValue === 'object' && newValue !== null) {
|
|
return JSON.stringify(newValue) !== JSON.stringify(oldValue)
|
|
}
|
|
return newValue !== oldValue
|
|
})
|
|
|
|
// If no changes, return existing note without updating timestamp
|
|
if (!hasChanges) {
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: parseNote(existingNote),
|
|
})
|
|
}
|
|
|
|
const note = await prisma.note.update({
|
|
where: { id },
|
|
data: updateData,
|
|
})
|
|
|
|
try {
|
|
const historyEnabled = existingNote.historyEnabled === true
|
|
if (historyEnabled && shouldCaptureHistorySnapshot(updateData)) {
|
|
const mode = await getNoteHistoryMode(session.user.id)
|
|
if (mode === 'auto') {
|
|
const shouldAuto = await shouldCreateAutoSnapshot({
|
|
noteId: id,
|
|
userId: session.user.id,
|
|
updateData,
|
|
existingContent: existingNote.content ?? '',
|
|
existingTitle: existingNote.title ?? null,
|
|
})
|
|
if (shouldAuto) {
|
|
await createNoteHistorySnapshot({
|
|
noteId: id,
|
|
userId: session.user.id,
|
|
reason: 'api:update',
|
|
})
|
|
}
|
|
}
|
|
// manual mode: no auto-snapshot
|
|
}
|
|
} catch (snapshotError) {
|
|
console.error('[HISTORY] Failed to create snapshot from /api/notes/[id] PUT:', snapshotError)
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: parseNote(note)
|
|
})
|
|
} catch (error) {
|
|
console.error('Error updating note:', error)
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Failed to update note' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
|
|
// DELETE /api/notes/[id] - Delete a note
|
|
export async function DELETE(
|
|
request: NextRequest,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const session = await auth()
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Unauthorized' },
|
|
{ status: 401 }
|
|
)
|
|
}
|
|
|
|
try {
|
|
const { id } = await params
|
|
|
|
const existingNote = await prisma.note.findUnique({
|
|
where: { id }
|
|
})
|
|
|
|
if (!existingNote) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Note not found' },
|
|
{ status: 404 }
|
|
)
|
|
}
|
|
|
|
if (existingNote.userId !== session.user.id) {
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Forbidden' },
|
|
{ status: 403 }
|
|
)
|
|
}
|
|
|
|
await prisma.note.update({
|
|
where: { id },
|
|
data: { trashedAt: new Date() }
|
|
})
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: 'Note moved to trash'
|
|
})
|
|
} catch (error) {
|
|
console.error('Error deleting note:', error)
|
|
return NextResponse.json(
|
|
{ success: false, error: 'Failed to delete note' },
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|