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:
Frederic Cabassut
2026-03-09 01:10:37 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 875306c99a
commit c9e041bffe
9 changed files with 110 additions and 38 deletions
+12
View File
@@ -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;
}
+2
View File
@@ -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
View File
@@ -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();
}
+9 -5
View File
@@ -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',
+26 -10
View File
@@ -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');
+6 -3
View File
@@ -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();
}
+7 -4
View File
@@ -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);
+5 -2
View File
@@ -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);
};