Add safe database migration workflow and note history infrastructure.
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 5s
This introduces guarded migrations with automatic backups, fixes note creation after DB reset, and wires snapshot/restore history across notes surfaces.
This commit is contained in:
28
Momento-main/momento/memento-note/app/(main)/page.tsx
Normal file
28
Momento-main/momento/memento-note/app/(main)/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getAllNotes } from '@/app/actions/notes'
|
||||
import { getAISettings } from '@/app/actions/ai-settings'
|
||||
import { HomeClient } from '@/components/home-client'
|
||||
|
||||
export default async function HomePage() {
|
||||
const [allNotes, settings] = await Promise.all([
|
||||
getAllNotes(),
|
||||
getAISettings(),
|
||||
])
|
||||
|
||||
const notesViewMode =
|
||||
settings?.notesViewMode === 'masonry'
|
||||
? 'masonry' as const
|
||||
: settings?.notesViewMode === 'tabs' || settings?.notesViewMode === 'list'
|
||||
? 'tabs' as const
|
||||
: 'masonry' as const
|
||||
|
||||
return (
|
||||
<HomeClient
|
||||
initialNotes={allNotes}
|
||||
initialSettings={{
|
||||
showRecentNotes: settings?.showRecentNotes !== false,
|
||||
notesViewMode,
|
||||
noteHistory: settings?.noteHistory === true,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
280
Momento-main/momento/memento-note/app/actions/ai-settings.ts
Normal file
280
Momento-main/momento/memento-note/app/actions/ai-settings.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
'use server'
|
||||
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { revalidatePath, updateTag } from 'next/cache'
|
||||
|
||||
export type UserAISettingsData = {
|
||||
titleSuggestions?: boolean
|
||||
semanticSearch?: boolean
|
||||
paragraphRefactor?: boolean
|
||||
memoryEcho?: boolean
|
||||
memoryEchoFrequency?: 'daily' | 'weekly' | 'custom'
|
||||
aiProvider?: 'auto' | 'openai' | 'ollama'
|
||||
preferredLanguage?: 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl'
|
||||
demoMode?: boolean
|
||||
showRecentNotes?: boolean
|
||||
notesViewMode?: 'masonry' | 'tabs' | 'list'
|
||||
emailNotifications?: boolean
|
||||
desktopNotifications?: boolean
|
||||
anonymousAnalytics?: boolean
|
||||
fontSize?: 'small' | 'medium' | 'large'
|
||||
languageDetection?: boolean
|
||||
autoLabeling?: boolean
|
||||
noteHistory?: boolean
|
||||
}
|
||||
|
||||
/** Only fields that exist on `UserAISettings` in Prisma (excludes e.g. `theme`, which lives on `User`). */
|
||||
const USER_AI_SETTINGS_PRISMA_KEYS = [
|
||||
'titleSuggestions',
|
||||
'semanticSearch',
|
||||
'paragraphRefactor',
|
||||
'memoryEcho',
|
||||
'memoryEchoFrequency',
|
||||
'aiProvider',
|
||||
'preferredLanguage',
|
||||
'fontSize',
|
||||
'demoMode',
|
||||
'showRecentNotes',
|
||||
'notesViewMode',
|
||||
'emailNotifications',
|
||||
'desktopNotifications',
|
||||
'anonymousAnalytics',
|
||||
'languageDetection',
|
||||
'autoLabeling',
|
||||
'noteHistory',
|
||||
] as const
|
||||
|
||||
type UserAISettingsPrismaKey = (typeof USER_AI_SETTINGS_PRISMA_KEYS)[number]
|
||||
|
||||
function pickUserAISettingsForDb(input: UserAISettingsData): Partial<Record<UserAISettingsPrismaKey, unknown>> {
|
||||
const out: Partial<Record<UserAISettingsPrismaKey, unknown>> = {}
|
||||
for (const key of USER_AI_SETTINGS_PRISMA_KEYS) {
|
||||
const v = input[key]
|
||||
if (v !== undefined) {
|
||||
out[key] = v
|
||||
}
|
||||
}
|
||||
if (out.notesViewMode === 'list') {
|
||||
out.notesViewMode = 'tabs'
|
||||
}
|
||||
if (
|
||||
out.notesViewMode != null &&
|
||||
out.notesViewMode !== 'masonry' &&
|
||||
out.notesViewMode !== 'tabs'
|
||||
) {
|
||||
delete out.notesViewMode
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Update AI settings for the current user
|
||||
*/
|
||||
export async function updateAISettings(settings: UserAISettingsData) {
|
||||
|
||||
const session = await auth()
|
||||
|
||||
|
||||
if (!session?.user?.id) {
|
||||
console.error('[updateAISettings] Unauthorized: No session or user ID')
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
try {
|
||||
const data = pickUserAISettingsForDb(settings)
|
||||
if (Object.keys(data).length === 0) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// Valeurs scalaires uniquement (pickUserAISettingsForDb) — cast pour éviter UpdateOperations vs create.
|
||||
const payload = data as Record<string, string | boolean | undefined>
|
||||
|
||||
// Upsert settings (create if not exists, update if exists)
|
||||
await prisma.userAISettings.upsert({
|
||||
where: { userId: session.user.id },
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
...payload,
|
||||
},
|
||||
update: payload,
|
||||
})
|
||||
|
||||
revalidatePath('/settings/ai', 'page')
|
||||
revalidatePath('/settings/appearance', 'page')
|
||||
revalidatePath('/', 'layout')
|
||||
updateTag('ai-settings')
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error updating AI settings:', error)
|
||||
const raw = error instanceof Error ? error.message : String(error)
|
||||
const isSchema =
|
||||
/no such column|notesViewMode|Unknown column|does not exist/i.test(raw) ||
|
||||
(typeof raw === 'string' && raw.includes('UserAISettings') && raw.includes('column'))
|
||||
if (isSchema) {
|
||||
throw new Error(
|
||||
'Schéma base de données obsolète : colonne notesViewMode manquante. Dans le dossier memento-note, exécutez : npx prisma db push (ou appliquez les migrations Prisma).'
|
||||
)
|
||||
}
|
||||
throw new Error('Failed to update AI settings')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI settings for the current user (Cached)
|
||||
*/
|
||||
import { unstable_cache } from 'next/cache'
|
||||
|
||||
// Internal cached function to fetch settings from DB
|
||||
const getCachedAISettings = unstable_cache(
|
||||
async (userId: string) => {
|
||||
try {
|
||||
const settings = await prisma.userAISettings.findUnique({
|
||||
where: { userId }
|
||||
})
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily' as const,
|
||||
aiProvider: 'auto' as const,
|
||||
preferredLanguage: 'auto' as const,
|
||||
demoMode: false,
|
||||
showRecentNotes: false,
|
||||
notesViewMode: 'masonry' as const,
|
||||
emailNotifications: false,
|
||||
desktopNotifications: false,
|
||||
anonymousAnalytics: false,
|
||||
theme: 'light' as const,
|
||||
fontSize: 'medium' as const,
|
||||
languageDetection: true,
|
||||
autoLabeling: true,
|
||||
noteHistory: false,
|
||||
}
|
||||
}
|
||||
|
||||
const raw = settings.notesViewMode
|
||||
const viewMode =
|
||||
raw === 'masonry'
|
||||
? ('masonry' as const)
|
||||
: raw === 'list' || raw === 'tabs'
|
||||
? ('tabs' as const)
|
||||
: ('masonry' as const)
|
||||
|
||||
return {
|
||||
titleSuggestions: settings.titleSuggestions,
|
||||
semanticSearch: settings.semanticSearch,
|
||||
paragraphRefactor: settings.paragraphRefactor,
|
||||
memoryEcho: settings.memoryEcho,
|
||||
memoryEchoFrequency: (settings.memoryEchoFrequency || 'daily') as 'daily' | 'weekly' | 'custom',
|
||||
aiProvider: (settings.aiProvider || 'auto') as 'auto' | 'openai' | 'ollama',
|
||||
preferredLanguage: (settings.preferredLanguage || 'auto') as 'auto' | 'en' | 'fr' | 'es' | 'de' | 'fa' | 'it' | 'pt' | 'ru' | 'zh' | 'ja' | 'ko' | 'ar' | 'hi' | 'nl' | 'pl',
|
||||
demoMode: settings.demoMode,
|
||||
showRecentNotes: settings.showRecentNotes,
|
||||
notesViewMode: viewMode,
|
||||
emailNotifications: settings.emailNotifications,
|
||||
desktopNotifications: settings.desktopNotifications,
|
||||
anonymousAnalytics: settings.anonymousAnalytics,
|
||||
// theme: 'light' as const, // REMOVED: Should not be handled here or hardcoded
|
||||
fontSize: (settings.fontSize || 'medium') as 'small' | 'medium' | 'large',
|
||||
languageDetection: settings.languageDetection ?? true,
|
||||
autoLabeling: settings.autoLabeling ?? true,
|
||||
noteHistory: settings.noteHistory ?? false,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting AI settings:', error)
|
||||
// Return defaults on error
|
||||
return {
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily' as const,
|
||||
aiProvider: 'auto' as const,
|
||||
preferredLanguage: 'auto' as const,
|
||||
demoMode: false,
|
||||
showRecentNotes: false,
|
||||
notesViewMode: 'masonry' as const,
|
||||
emailNotifications: false,
|
||||
desktopNotifications: false,
|
||||
anonymousAnalytics: false,
|
||||
theme: 'light' as const,
|
||||
fontSize: 'medium' as const,
|
||||
languageDetection: true,
|
||||
autoLabeling: true,
|
||||
noteHistory: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
['user-ai-settings'],
|
||||
{ tags: ['ai-settings'] }
|
||||
)
|
||||
|
||||
export async function getAISettings(userId?: string) {
|
||||
let id = userId
|
||||
|
||||
if (!id) {
|
||||
const session = await auth()
|
||||
id = session?.user?.id
|
||||
}
|
||||
|
||||
// Return defaults for non-logged-in users
|
||||
if (!id) {
|
||||
return {
|
||||
titleSuggestions: true,
|
||||
semanticSearch: true,
|
||||
paragraphRefactor: true,
|
||||
memoryEcho: true,
|
||||
memoryEchoFrequency: 'daily' as const,
|
||||
aiProvider: 'auto' as const,
|
||||
preferredLanguage: 'auto' as const,
|
||||
demoMode: false,
|
||||
showRecentNotes: false,
|
||||
notesViewMode: 'masonry' as const,
|
||||
emailNotifications: false,
|
||||
desktopNotifications: false,
|
||||
anonymousAnalytics: false,
|
||||
theme: 'light' as const,
|
||||
fontSize: 'medium' as const,
|
||||
languageDetection: true,
|
||||
autoLabeling: true,
|
||||
noteHistory: false,
|
||||
}
|
||||
}
|
||||
|
||||
return getCachedAISettings(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's preferred AI provider
|
||||
*/
|
||||
export async function getUserAIPreference(): Promise<'auto' | 'openai' | 'ollama'> {
|
||||
const settings = await getAISettings()
|
||||
return settings.aiProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific AI feature is enabled for the user
|
||||
*/
|
||||
export async function isAIFeatureEnabled(feature: keyof UserAISettingsData): Promise<boolean> {
|
||||
const settings = await getAISettings()
|
||||
|
||||
switch (feature) {
|
||||
case 'titleSuggestions':
|
||||
return settings.titleSuggestions
|
||||
case 'semanticSearch':
|
||||
return settings.semanticSearch
|
||||
case 'paragraphRefactor':
|
||||
return settings.paragraphRefactor
|
||||
case 'memoryEcho':
|
||||
return settings.memoryEcho
|
||||
case 'noteHistory':
|
||||
return settings.noteHistory
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
1952
Momento-main/momento/memento-note/app/actions/notes.ts
Normal file
1952
Momento-main/momento/memento-note/app/actions/notes.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
'use server'
|
||||
|
||||
import { auth } from '@/auth'
|
||||
import { titleSuggestionService } from '@/lib/ai/services/title-suggestion.service'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { createNoteHistorySnapshot, isNoteHistoryEnabledForUser } from '@/lib/note-history'
|
||||
|
||||
export interface GenerateTitlesResponse {
|
||||
suggestions: Array<{
|
||||
title: string
|
||||
confidence: number
|
||||
reasoning?: string
|
||||
}>
|
||||
noteId: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate title suggestions for a note
|
||||
* Triggered when note reaches 50+ words without a title
|
||||
*/
|
||||
export async function generateTitleSuggestions(noteId: string): Promise<GenerateTitlesResponse> {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch note content
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id: noteId },
|
||||
select: { id: true, content: true, userId: true }
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
throw new Error('Note not found')
|
||||
}
|
||||
|
||||
if (note.userId !== session.user.id) {
|
||||
throw new Error('Forbidden')
|
||||
}
|
||||
|
||||
if (!note.content || note.content.trim().length === 0) {
|
||||
throw new Error('Note content is empty')
|
||||
}
|
||||
|
||||
// Generate suggestions
|
||||
const suggestions = await titleSuggestionService.generateSuggestions(note.content)
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
noteId
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating title suggestions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply selected title to note
|
||||
*/
|
||||
export async function applyTitleSuggestion(
|
||||
noteId: string,
|
||||
selectedTitle: string
|
||||
): Promise<void> {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
try {
|
||||
// Update note with selected title
|
||||
await prisma.note.update({
|
||||
where: {
|
||||
id: noteId,
|
||||
userId: session.user.id
|
||||
},
|
||||
data: {
|
||||
title: selectedTitle,
|
||||
autoGenerated: true,
|
||||
lastAiAnalysis: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const historyEnabled = await isNoteHistoryEnabledForUser(session.user.id)
|
||||
if (historyEnabled) {
|
||||
await createNoteHistorySnapshot({
|
||||
noteId,
|
||||
userId: session.user.id,
|
||||
reason: 'title-suggestion',
|
||||
})
|
||||
}
|
||||
} catch (snapshotError) {
|
||||
console.error('[HISTORY] Failed to create snapshot after title suggestion:', snapshotError)
|
||||
}
|
||||
|
||||
revalidatePath('/')
|
||||
revalidatePath(`/note/${noteId}`)
|
||||
} catch (error) {
|
||||
console.error('Error applying title suggestion:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record user feedback on title suggestions
|
||||
* (Phase 3 - for improving future suggestions)
|
||||
*/
|
||||
export async function recordTitleFeedback(
|
||||
noteId: string,
|
||||
selectedTitle: string,
|
||||
allSuggestions: Array<{ title: string; confidence: number }>
|
||||
): Promise<void> {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
throw new Error('Unauthorized')
|
||||
}
|
||||
|
||||
try {
|
||||
// Save to AiFeedback table for learning
|
||||
await prisma.aiFeedback.create({
|
||||
data: {
|
||||
noteId,
|
||||
userId: session.user.id,
|
||||
feedbackType: 'thumbs_up', // User chose one of our suggestions
|
||||
feature: 'title_suggestion',
|
||||
originalContent: JSON.stringify(allSuggestions),
|
||||
correctedContent: selectedTitle,
|
||||
metadata: JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
provider: 'auto' // Will be dynamic based on user settings
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error recording title feedback:', error)
|
||||
// Don't throw - feedback is optional
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { auth } from '@/auth'
|
||||
import { getNoteHistory, restoreNoteVersion } from '@/app/actions/notes'
|
||||
|
||||
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 limitRaw = request.nextUrl.searchParams.get('limit')
|
||||
const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 30
|
||||
const entries = await getNoteHistory(id, Number.isNaN(limit) ? 30 : limit)
|
||||
return NextResponse.json({ success: true, data: entries })
|
||||
} catch (error) {
|
||||
console.error('Error fetching note history:', error)
|
||||
return NextResponse.json({ success: false, error: 'Failed to fetch history' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
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 body = await request.json()
|
||||
const historyEntryId = body?.historyEntryId
|
||||
|
||||
if (!historyEntryId || typeof historyEntryId !== 'string') {
|
||||
return NextResponse.json({ success: false, error: 'historyEntryId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const restoredNote = await restoreNoteVersion(id, historyEntryId)
|
||||
return NextResponse.json({ success: true, data: restoredNote })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to restore history version'
|
||||
console.error('Error restoring note history:', error)
|
||||
return NextResponse.json({ success: false, error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import { reconcileLabelsAfterNoteMove } from '@/app/actions/notes'
|
||||
import { createNoteHistorySnapshot, isNoteHistoryEnabledForUser } from '@/lib/note-history'
|
||||
|
||||
// POST /api/notes/[id]/move - Move a note to a notebook (or to Inbox)
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const { notebookId } = body
|
||||
|
||||
// Get the note
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
notebookId: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!note) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Note not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if (note.userId !== session.user.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Forbidden' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
// If notebookId is provided, verify it exists and belongs to the user
|
||||
if (notebookId !== null && notebookId !== '') {
|
||||
const notebook = await prisma.notebook.findUnique({
|
||||
where: { id: notebookId },
|
||||
select: { userId: true }
|
||||
})
|
||||
|
||||
if (!notebook || notebook.userId !== session.user.id) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Notebook not found or unauthorized' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the note's notebook
|
||||
// notebookId = null or "" means move to Inbox (Notes générales)
|
||||
const targetNotebookId = notebookId && notebookId !== '' ? notebookId : null
|
||||
|
||||
const updatedNote = await prisma.note.update({
|
||||
where: { id },
|
||||
data: {
|
||||
notebookId: targetNotebookId
|
||||
},
|
||||
include: {
|
||||
notebook: {
|
||||
select: { id: true, name: true }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await reconcileLabelsAfterNoteMove(id, targetNotebookId)
|
||||
|
||||
try {
|
||||
const historyEnabled = await isNoteHistoryEnabledForUser(session.user.id)
|
||||
if (historyEnabled) {
|
||||
await createNoteHistorySnapshot({
|
||||
noteId: id,
|
||||
userId: session.user.id,
|
||||
reason: 'move-notebook',
|
||||
})
|
||||
}
|
||||
} catch (snapshotError) {
|
||||
console.error('[HISTORY] Failed to create snapshot after notebook move:', snapshotError)
|
||||
}
|
||||
|
||||
// No revalidatePath('/') here — the client-side triggerRefresh() in
|
||||
// notebooks-context.tsx handles the refresh. Avoiding server-side
|
||||
// revalidation prevents a double-refresh (server + client).
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: updatedNote,
|
||||
message: notebookId && notebookId !== ''
|
||||
? `Note moved to "${updatedNote.notebook?.name || 'notebook'}"`
|
||||
: 'Note moved to Inbox'
|
||||
})
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to move note' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
218
Momento-main/momento/memento-note/app/api/notes/[id]/route.ts
Normal file
218
Momento-main/momento/memento-note/app/api/notes/[id]/route.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { auth } from '@/auth'
|
||||
import { parseNote } from '@/lib/utils'
|
||||
import {
|
||||
createNoteHistorySnapshot,
|
||||
isNoteHistoryEnabledForUser,
|
||||
shouldCaptureHistorySnapshot,
|
||||
} 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 = await isNoteHistoryEnabledForUser(session.user.id)
|
||||
if (historyEnabled && shouldCaptureHistorySnapshot(updateData)) {
|
||||
await createNoteHistorySnapshot({
|
||||
noteId: id,
|
||||
userId: session.user.id,
|
||||
reason: 'api:update',
|
||||
})
|
||||
}
|
||||
} 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user