Add BMAD framework, authentication, and new features

This commit is contained in:
2026-01-08 21:23:23 +01:00
parent f07d28aefd
commit 15a95fb319
1298 changed files with 73308 additions and 154901 deletions

View File

@@ -0,0 +1,29 @@
'use server';
import { signIn } from '@/auth';
import { AuthError } from 'next-auth';
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
console.error('AuthError details:', error.type, error.message);
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return `Auth error: ${error.type}`;
}
}
// IMPORTANT: Next.js redirects throw a special error that must be rethrown
if (error instanceof Error && error.message === 'NEXT_REDIRECT') {
throw error;
}
console.error('Non-AuthError during signin:', error);
throw error;
}
}

View File

@@ -3,6 +3,7 @@
import { revalidatePath } from 'next/cache'
import prisma from '@/lib/prisma'
import { Note, CheckItem } from '@/lib/types'
import { auth } from '@/auth'
// Helper function to parse JSON strings from database
function parseNote(dbNote: any): Note {
@@ -11,14 +12,21 @@ function parseNote(dbNote: any): Note {
checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null,
labels: dbNote.labels ? JSON.parse(dbNote.labels) : null,
images: dbNote.images ? JSON.parse(dbNote.images) : null,
links: dbNote.links ? JSON.parse(dbNote.links) : null,
}
}
// Get all notes (non-archived by default)
export async function getNotes(includeArchived = false) {
const session = await auth();
if (!session?.user?.id) return [];
try {
const notes = await prisma.note.findMany({
where: includeArchived ? {} : { isArchived: false },
where: {
userId: session.user.id,
isArchived: includeArchived ? {} : { isArchived: false },
},
orderBy: [
{ isPinned: 'desc' },
{ order: 'asc' },
@@ -35,9 +43,15 @@ export async function getNotes(includeArchived = false) {
// Get archived notes only
export async function getArchivedNotes() {
const session = await auth();
if (!session?.user?.id) return [];
try {
const notes = await prisma.note.findMany({
where: { isArchived: true },
where: {
userId: session.user.id,
isArchived: true
},
orderBy: { updatedAt: 'desc' }
})
@@ -50,6 +64,9 @@ export async function getArchivedNotes() {
// Search notes
export async function searchNotes(query: string) {
const session = await auth();
if (!session?.user?.id) return [];
try {
if (!query.trim()) {
return await getNotes()
@@ -57,6 +74,7 @@ export async function searchNotes(query: string) {
const notes = await prisma.note.findMany({
where: {
userId: session.user.id,
isArchived: false,
OR: [
{ title: { contains: query } },
@@ -72,7 +90,7 @@ export async function searchNotes(query: string) {
})
// Enhanced ranking: prioritize title matches
const rankedNotes = notes.map(note => {
const rankedNotes = notes.map((note: any) => {
const parsedNote = parseNote(note)
let score = 0
@@ -101,8 +119,8 @@ export async function searchNotes(query: string) {
// Sort by score descending, then by existing order (pinned/updated)
return rankedNotes
.sort((a, b) => b.score - a.score)
.map(item => item.note)
.sort((a: any, b: any) => b.score - a.score)
.map((item: any) => item.note)
} catch (error) {
console.error('Error searching notes:', error)
return []
@@ -118,13 +136,18 @@ export async function createNote(data: {
checkItems?: CheckItem[]
labels?: string[]
images?: string[]
links?: any[]
isArchived?: boolean
reminder?: Date | null
isMarkdown?: boolean
}) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
const note = await prisma.note.create({
data: {
userId: session.user.id,
title: data.title || null,
content: data.content,
color: data.color || 'default',
@@ -132,6 +155,7 @@ export async function createNote(data: {
checkItems: data.checkItems ? JSON.stringify(data.checkItems) : null,
labels: data.labels ? JSON.stringify(data.labels) : null,
images: data.images ? JSON.stringify(data.images) : null,
links: data.links ? JSON.stringify(data.links) : null,
isArchived: data.isArchived || false,
reminder: data.reminder || null,
isMarkdown: data.isMarkdown || false,
@@ -157,9 +181,13 @@ export async function updateNote(id: string, data: {
checkItems?: CheckItem[] | null
labels?: string[] | null
images?: string[] | null
links?: any[] | null
reminder?: Date | null
isMarkdown?: boolean
}) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
// Stringify JSON fields if they exist
const updateData: any = { ...data }
@@ -172,10 +200,16 @@ export async function updateNote(id: string, data: {
if ('images' in data) {
updateData.images = data.images ? JSON.stringify(data.images) : null
}
if ('links' in data) {
updateData.links = data.links ? JSON.stringify(data.links) : null
}
updateData.updatedAt = new Date()
const note = await prisma.note.update({
where: { id },
where: {
id,
userId: session.user.id
},
data: updateData
})
@@ -189,9 +223,15 @@ export async function updateNote(id: string, data: {
// Delete a note
export async function deleteNote(id: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
try {
await prisma.note.delete({
where: { id }
where: {
id,
userId: session.user.id
}
})
revalidatePath('/')
@@ -230,7 +270,7 @@ export async function getAllLabels() {
})
const labelsSet = new Set<string>()
notes.forEach(note => {
notes.forEach((note: any) => {
const labels = note.labels ? JSON.parse(note.labels) : null
if (labels) {
labels.forEach((label: string) => labelsSet.add(label))
@@ -246,11 +286,14 @@ export async function getAllLabels() {
// Reorder notes (drag and drop)
export async function reorderNotes(draggedId: string, targetId: string) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
console.log('[REORDER-DEBUG] reorderNotes called:', { draggedId, targetId })
try {
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId } })
const targetNote = await prisma.note.findUnique({ where: { id: targetId } })
const draggedNote = await prisma.note.findUnique({ where: { id: draggedId, userId: session.user.id } })
const targetNote = await prisma.note.findUnique({ where: { id: targetId, userId: session.user.id } })
console.log('[REORDER-DEBUG] Notes found:', {
draggedNote: draggedNote ? { id: draggedNote.id, title: draggedNote.title, isPinned: draggedNote.isPinned, order: draggedNote.order } : null,
@@ -265,23 +308,24 @@ export async function reorderNotes(draggedId: string, targetId: string) {
// Get all notes in the same category (pinned or unpinned)
const allNotes = await prisma.note.findMany({
where: {
userId: session.user.id,
isPinned: draggedNote.isPinned,
isArchived: false
},
orderBy: { order: 'asc' }
})
console.log('[REORDER-DEBUG] All notes in category:', allNotes.map(n => ({ id: n.id, title: n.title, order: n.order })))
console.log('[REORDER-DEBUG] All notes in category:', allNotes.map((n: any) => ({ id: n.id, title: n.title, order: n.order })))
// Create new order array
const reorderedNotes = allNotes.filter(n => n.id !== draggedId)
const targetIndex = reorderedNotes.findIndex(n => n.id === targetId)
const reorderedNotes = allNotes.filter((n: any) => n.id !== draggedId)
const targetIndex = reorderedNotes.findIndex((n: any) => n.id === targetId)
reorderedNotes.splice(targetIndex, 0, draggedNote)
console.log('[REORDER-DEBUG] New order:', reorderedNotes.map((n, i) => ({ id: n.id, title: n.title, newOrder: i })))
console.log('[REORDER-DEBUG] New order:', reorderedNotes.map((n: any, i: number) => ({ id: n.id, title: n.title, newOrder: i })))
// Update all notes with new order
const updates = reorderedNotes.map((note, index) =>
const updates = reorderedNotes.map((note: any, index: number) =>
prisma.note.update({
where: { id: note.id },
data: { order: index }
@@ -299,3 +343,27 @@ export async function reorderNotes(draggedId: string, targetId: string) {
throw new Error('Failed to reorder notes')
}
}
// Update full order of notes
export async function updateFullOrder(ids: string[]) {
const session = await auth();
if (!session?.user?.id) throw new Error('Unauthorized');
const userId = session.user.id;
try {
const updates = ids.map((id: string, index: number) =>
prisma.note.update({
where: { id, userId },
data: { order: index }
})
)
await prisma.$transaction(updates)
revalidatePath('/')
return { success: true }
} catch (error) {
console.error('Error updating full order:', error)
throw new Error('Failed to update order')
}
}

View File

@@ -0,0 +1,54 @@
'use server';
import bcrypt from 'bcryptjs';
import prisma from '@/lib/prisma';
import { z } from 'zod';
import { redirect } from 'next/navigation';
const RegisterSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
name: z.string().min(2),
});
export async function register(prevState: string | undefined, formData: FormData) {
const validatedFields = RegisterSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
name: formData.get('name'),
});
if (!validatedFields.success) {
return 'Invalid fields. Failed to register.';
}
const { email, password, name } = validatedFields.data;
try {
const existingUser = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
if (existingUser) {
return 'User already exists.';
}
const hashedPassword = await bcrypt.hash(password, 10);
await prisma.user.create({
data: {
email: email.toLowerCase(),
password: hashedPassword,
name,
},
});
// Attempt to sign in immediately after registration
// We cannot import signIn here directly if it causes circular deps or issues,
// but usually it works. If not, redirecting to login is fine.
// Let's stick to redirecting to login but with a clear success message?
// Or better: lowercase the email to fix the potential bug.
} catch (error) {
console.error('Registration Error:', error);
return 'Database Error: Failed to create user.';
}
redirect('/login');
}

View File

@@ -0,0 +1,54 @@
'use server'
import * as cheerio from 'cheerio';
export interface LinkMetadata {
url: string;
title?: string;
description?: string;
imageUrl?: string;
siteName?: string;
}
export async function fetchLinkMetadata(url: string): Promise<LinkMetadata | null> {
try {
// Add protocol if missing
let targetUrl = url;
if (!url.startsWith('http://') && !url.startsWith('https://')) {
targetUrl = 'https://' + url;
}
const response = await fetch(targetUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (compatible; Memento/1.0; +http://localhost:3000)',
},
next: { revalidate: 3600 } // Cache for 1 hour
});
if (!response.ok) return null;
const html = await response.text();
const $ = cheerio.load(html);
const getMeta = (prop: string) =>
$(`meta[property="${prop}"]`).attr('content') ||
$(`meta[name="${prop}"]`).attr('content');
const title = getMeta('og:title') || $('title').text() || '';
const description = getMeta('og:description') || getMeta('description') || '';
const imageUrl = getMeta('og:image');
const siteName = getMeta('og:site_name');
return {
url: targetUrl,
title: title.substring(0, 100), // Truncate if too long
description: description.substring(0, 200),
imageUrl,
siteName
};
} catch (error) {
console.error('Error fetching link metadata:', error);
return null;
}
}