forked from mirror/crumbs-original
refactor(api): scope all queries by authenticated user
Add getUserId/requireAdmin helpers in api-utils.ts. All notes, tags, search, sync, and attachment routes now filter by userId to ensure complete data isolation between users. Sync server accepts userId parameter for push/pull operations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Fred Cabassut
co-authored by
Claude Opus 4.6
parent
875306c99a
commit
c9e041bffe
@@ -0,0 +1,12 @@
|
||||
import { error, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export function getUserId(event: RequestEvent): number {
|
||||
if (!event.locals.user) throw error(401, 'Unauthorized');
|
||||
return event.locals.user.id;
|
||||
}
|
||||
|
||||
export function requireAdmin(event: RequestEvent) {
|
||||
const user = event.locals.user;
|
||||
if (!user || user.role !== 'admin') throw error(403, 'Forbidden');
|
||||
return user;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ if (!existsSync(ATTACHMENTS_DIR)) {
|
||||
export async function saveAttachment(
|
||||
noteId: string,
|
||||
file: File,
|
||||
userId: number,
|
||||
thumbnail?: File | Blob | null
|
||||
): Promise<typeof attachments.$inferSelect> {
|
||||
const id = uuidv4();
|
||||
@@ -40,6 +41,7 @@ export async function saveAttachment(
|
||||
.insert(attachments)
|
||||
.values({
|
||||
id,
|
||||
userId,
|
||||
noteId,
|
||||
filename: file.name,
|
||||
mimeType: file.type,
|
||||
|
||||
+11
-9
@@ -1,6 +1,6 @@
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { notes, syncLog } from '$lib/server/db/schema.js';
|
||||
import { eq, gt } from 'drizzle-orm';
|
||||
import { eq, gt, and } from 'drizzle-orm';
|
||||
import { extractTags } from '$lib/utils/tags.js';
|
||||
import { syncNoteTags } from '$lib/server/tags.js';
|
||||
import type { SyncQueueItem } from './idb.js';
|
||||
@@ -8,7 +8,7 @@ import type { SyncQueueItem } from './idb.js';
|
||||
/**
|
||||
* Process incoming sync changes from client.
|
||||
*/
|
||||
export async function processSyncPush(changes: SyncQueueItem[]): Promise<void> {
|
||||
export async function processSyncPush(changes: SyncQueueItem[], userId: number): Promise<void> {
|
||||
const noteIdsToSyncTags: string[] = [];
|
||||
|
||||
db.transaction((tx) => {
|
||||
@@ -20,7 +20,7 @@ export async function processSyncPush(changes: SyncQueueItem[]): Promise<void> {
|
||||
const existing = tx
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(eq(notes.id, change.noteId))
|
||||
.where(and(eq(notes.id, change.noteId), eq(notes.userId, userId)))
|
||||
.get();
|
||||
|
||||
if (existing) {
|
||||
@@ -31,13 +31,14 @@ export async function processSyncPush(changes: SyncQueueItem[]): Promise<void> {
|
||||
updatedAt: new Date(change.timestamp),
|
||||
version: existing.version + 1
|
||||
})
|
||||
.where(eq(notes.id, change.noteId))
|
||||
.where(and(eq(notes.id, change.noteId), eq(notes.userId, userId)))
|
||||
.run();
|
||||
}
|
||||
} else if (change.operation === 'create' && change.data) {
|
||||
tx.insert(notes)
|
||||
.values({
|
||||
id: change.noteId,
|
||||
userId,
|
||||
title: change.data.title || '',
|
||||
content: change.data.content || '',
|
||||
color: change.data.color || 'default',
|
||||
@@ -59,13 +60,14 @@ export async function processSyncPush(changes: SyncQueueItem[]): Promise<void> {
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
tx.delete(notes).where(eq(notes.id, change.noteId)).run();
|
||||
tx.delete(notes).where(and(eq(notes.id, change.noteId), eq(notes.userId, userId))).run();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tx.insert(syncLog)
|
||||
.values({
|
||||
userId,
|
||||
noteId: change.noteId,
|
||||
operation: change.operation,
|
||||
timestamp: new Date(change.timestamp),
|
||||
@@ -76,7 +78,7 @@ export async function processSyncPush(changes: SyncQueueItem[]): Promise<void> {
|
||||
});
|
||||
|
||||
for (const noteId of noteIdsToSyncTags) {
|
||||
const note = db.select().from(notes).where(eq(notes.id, noteId)).get();
|
||||
const note = db.select().from(notes).where(and(eq(notes.id, noteId), eq(notes.userId, userId))).get();
|
||||
if (note) {
|
||||
const content = `${note.title} ${note.content}`;
|
||||
syncNoteTags(noteId, extractTags(content));
|
||||
@@ -85,12 +87,12 @@ export async function processSyncPush(changes: SyncQueueItem[]): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notes updated since a given timestamp.
|
||||
* Get all notes updated since a given timestamp for a specific user.
|
||||
*/
|
||||
export async function getChangesSince(sinceTimestamp: number) {
|
||||
export async function getChangesSince(sinceTimestamp: number, userId: number) {
|
||||
return db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(gt(notes.updatedAt, new Date(sinceTimestamp)))
|
||||
.where(and(eq(notes.userId, userId), gt(notes.updatedAt, new Date(sinceTimestamp))))
|
||||
.all();
|
||||
}
|
||||
|
||||
@@ -7,20 +7,22 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { extractTags } from '$lib/utils/tags.js';
|
||||
import { fetchTagsForNotes, syncNoteTags } from '$lib/server/tags.js';
|
||||
import { fetchAttachmentsForNotes } from '$lib/server/attachments.js';
|
||||
import { getUserId } from '$lib/server/api-utils.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
export const GET: RequestHandler = async ({ url, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const filter = url.searchParams.get('filter') || 'all';
|
||||
|
||||
let conditions;
|
||||
switch (filter) {
|
||||
case 'archived':
|
||||
conditions = and(eq(notes.archived, true), eq(notes.trashed, false));
|
||||
conditions = and(eq(notes.userId, userId), eq(notes.archived, true), eq(notes.trashed, false));
|
||||
break;
|
||||
case 'trashed':
|
||||
conditions = eq(notes.trashed, true);
|
||||
conditions = and(eq(notes.userId, userId), eq(notes.trashed, true));
|
||||
break;
|
||||
default:
|
||||
conditions = and(eq(notes.archived, false), eq(notes.trashed, false));
|
||||
conditions = and(eq(notes.userId, userId), eq(notes.archived, false), eq(notes.trashed, false));
|
||||
}
|
||||
|
||||
const result = db
|
||||
@@ -42,13 +44,15 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
return json(notesWithTags);
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
export const POST: RequestHandler = async ({ request, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const body = await request.json();
|
||||
const now = new Date();
|
||||
const id = body.id || uuidv4();
|
||||
|
||||
const newNote = {
|
||||
id,
|
||||
userId,
|
||||
title: body.title || '',
|
||||
content: body.content || '',
|
||||
color: body.color || 'default',
|
||||
|
||||
@@ -2,13 +2,19 @@ import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { notes } from '$lib/server/db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { extractTags } from '$lib/utils/tags.js';
|
||||
import { fetchTagsForNotes, syncNoteTags } from '$lib/server/tags.js';
|
||||
import { fetchAttachmentsForNotes } from '$lib/server/attachments.js';
|
||||
import { getUserId } from '$lib/server/api-utils.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const note = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
export const GET: RequestHandler = async ({ params, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const note = db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(and(eq(notes.id, params.id), eq(notes.userId, userId)))
|
||||
.get();
|
||||
|
||||
if (!note) {
|
||||
throw error(404, 'Note not found');
|
||||
@@ -24,9 +30,14 @@ export const GET: RequestHandler = async ({ params }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
export const PATCH: RequestHandler = async ({ params, request, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const body = await request.json();
|
||||
const existing = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
const existing = db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(and(eq(notes.id, params.id), eq(notes.userId, userId)))
|
||||
.get();
|
||||
|
||||
if (!existing) {
|
||||
throw error(404, 'Note not found');
|
||||
@@ -50,9 +61,9 @@ export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
if (body.checklistMode !== undefined) updates.checklistMode = body.checklistMode;
|
||||
if (body.sortOrder !== undefined) updates.sortOrder = body.sortOrder;
|
||||
|
||||
db.update(notes).set(updates).where(eq(notes.id, params.id)).run();
|
||||
db.update(notes).set(updates).where(and(eq(notes.id, params.id), eq(notes.userId, userId))).run();
|
||||
|
||||
const updated = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
const updated = db.select().from(notes).where(and(eq(notes.id, params.id), eq(notes.userId, userId))).get();
|
||||
|
||||
if (body.title !== undefined || body.content !== undefined) {
|
||||
const content = `${updated!.title} ${updated!.content}`;
|
||||
@@ -70,13 +81,18 @@ export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params }) => {
|
||||
const existing = db.select().from(notes).where(eq(notes.id, params.id)).get();
|
||||
export const DELETE: RequestHandler = async ({ params, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const existing = db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(and(eq(notes.id, params.id), eq(notes.userId, userId)))
|
||||
.get();
|
||||
|
||||
if (!existing) {
|
||||
throw error(404, 'Note not found');
|
||||
}
|
||||
|
||||
db.delete(notes).where(eq(notes.id, params.id)).run();
|
||||
db.delete(notes).where(and(eq(notes.id, params.id), eq(notes.userId, userId))).run();
|
||||
return json({ success: true });
|
||||
};
|
||||
|
||||
@@ -2,8 +2,26 @@ import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { saveAttachment, getAttachmentsByNote, getAttachment, deleteAttachment, updateAttachment } from '$lib/server/attachments.js';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { notes } from '$lib/server/db/schema.js';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
import { getUserId } from '$lib/server/api-utils.js';
|
||||
|
||||
/** Verify the note belongs to the authenticated user */
|
||||
async function verifyNoteOwnership(noteId: string, userId: number) {
|
||||
const note = await db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(and(eq(notes.id, noteId), eq(notes.userId, userId)))
|
||||
.get();
|
||||
if (!note) throw error(404, 'Note not found');
|
||||
return note;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
await verifyNoteOwnership(params.id, userId);
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url }) => {
|
||||
const attachmentId = url.searchParams.get('attachmentId');
|
||||
|
||||
if (attachmentId) {
|
||||
@@ -28,7 +46,10 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
return json(list);
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ params, request }) => {
|
||||
export const POST: RequestHandler = async ({ params, request, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
await verifyNoteOwnership(params.id, userId);
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file') as File;
|
||||
|
||||
@@ -46,11 +67,14 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
||||
}
|
||||
|
||||
const thumbnail = formData.get('thumbnail') as File | null;
|
||||
const attachment = await saveAttachment(params.id, file, thumbnail);
|
||||
const attachment = await saveAttachment(params.id, file, userId, thumbnail);
|
||||
return json(attachment, { status: 201 });
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async ({ url, request }) => {
|
||||
export const PATCH: RequestHandler = async ({ params, url, request, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
await verifyNoteOwnership(params.id, userId);
|
||||
|
||||
const attachmentId = url.searchParams.get('attachmentId');
|
||||
if (!attachmentId) throw error(400, 'attachmentId required');
|
||||
|
||||
@@ -63,7 +87,10 @@ export const PATCH: RequestHandler = async ({ url, request }) => {
|
||||
return json(updated);
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ url }) => {
|
||||
export const DELETE: RequestHandler = async ({ params, url, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
await verifyNoteOwnership(params.id, userId);
|
||||
|
||||
const attachmentId = url.searchParams.get('attachmentId');
|
||||
if (!attachmentId) throw error(400, 'attachmentId required');
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import { db } from '$lib/server/db/index.js';
|
||||
import { notes, noteTags, tags } from '$lib/server/db/schema.js';
|
||||
import { and, eq, like, or, inArray } from 'drizzle-orm';
|
||||
import { fetchTagsForNotes } from '$lib/server/tags.js';
|
||||
import { getUserId } from '$lib/server/api-utils.js';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
export const GET: RequestHandler = async ({ url, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const query = url.searchParams.get('q')?.trim();
|
||||
|
||||
if (!query) {
|
||||
@@ -19,6 +21,7 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
.from(notes)
|
||||
.where(
|
||||
and(
|
||||
eq(notes.userId, userId),
|
||||
eq(notes.trashed, false),
|
||||
or(like(notes.title, pattern), like(notes.content, pattern))
|
||||
)
|
||||
@@ -30,7 +33,7 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
.select({ noteId: noteTags.noteId })
|
||||
.from(noteTags)
|
||||
.innerJoin(tags, eq(noteTags.tagId, tags.id))
|
||||
.where(like(tags.name, pattern))
|
||||
.where(and(like(tags.name, pattern), eq(tags.userId, userId)))
|
||||
.all();
|
||||
|
||||
const resultIds = new Set(results.map((n) => n.id));
|
||||
@@ -43,7 +46,7 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
extraNotes = db
|
||||
.select()
|
||||
.from(notes)
|
||||
.where(and(inArray(notes.id, extraNoteIds), eq(notes.trashed, false)))
|
||||
.where(and(eq(notes.userId, userId), inArray(notes.id, extraNoteIds), eq(notes.trashed, false)))
|
||||
.all();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,24 @@ import type { RequestHandler } from './$types.js';
|
||||
import { processSyncPush, getChangesSince } from '$lib/sync/server.js';
|
||||
import { fetchTagsForNotes } from '$lib/server/tags.js';
|
||||
import { fetchAttachmentsForNotes } from '$lib/server/attachments.js';
|
||||
import { getUserId } from '$lib/server/api-utils.js';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
export const POST: RequestHandler = async ({ request, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const { changes } = await request.json();
|
||||
|
||||
if (!Array.isArray(changes)) {
|
||||
return json({ error: 'Invalid changes' }, { status: 400 });
|
||||
}
|
||||
|
||||
await processSyncPush(changes);
|
||||
await processSyncPush(changes, userId);
|
||||
return json({ success: true });
|
||||
};
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
export const GET: RequestHandler = async ({ url, ...event }) => {
|
||||
const userId = getUserId(event);
|
||||
const since = parseInt(url.searchParams.get('since') || '0', 10);
|
||||
const changes = await getChangesSince(since);
|
||||
const changes = await getChangesSince(since, userId);
|
||||
|
||||
const noteIds = changes.map((n) => n.id);
|
||||
const tagMap = fetchTagsForNotes(noteIds);
|
||||
|
||||
@@ -2,8 +2,11 @@ import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { tags } from '$lib/server/db/schema.js';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getUserId } from '$lib/server/api-utils.js';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const allTags = await db.select().from(tags);
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const userId = getUserId(event);
|
||||
const allTags = await db.select().from(tags).where(eq(tags.userId, userId));
|
||||
return json(allTags);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user