Scope all Drizzle queries by user_id from authenticated session

Every data API route now filters SELECT, INSERT, UPDATE, and DELETE
queries by the authenticated user's ID, ensuring full multi-tenant
data isolation. Candle queries are scoped via chart_id ownership.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-20 13:08:09 +01:00
parent 9901d0f3f1
commit 5f727d84c6
15 changed files with 75 additions and 60 deletions

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { annotationTypes } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
type RouteContext = {
@ -51,7 +51,7 @@ export async function PATCH(
const result = await db
.update(annotationTypes)
.set(updateData)
.where(eq(annotationTypes.id, idNum))
.where(and(eq(annotationTypes.id, idNum), eq(annotationTypes.user_id, user.id)))
.returning();
if (result.length === 0) {

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { annotationTypes, annotations } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
// GET - List all annotation types
@ -12,7 +12,7 @@ export async function GET() {
}
try {
const types = await db.select().from(annotationTypes);
const types = await db.select().from(annotationTypes).where(eq(annotationTypes.user_id, user.id));
return NextResponse.json(types);
} catch (error) {
console.error('Error fetching annotation types:', error);
@ -35,8 +35,8 @@ export async function POST(request: NextRequest) {
// Special case: seed default types
if (body.action === 'seed') {
const existing = await db.select().from(annotationTypes);
const existing = await db.select().from(annotationTypes).where(eq(annotationTypes.user_id, user.id));
if (existing.length > 0) {
return NextResponse.json({ message: 'Types already seeded' });
}
@ -49,6 +49,7 @@ export async function POST(request: NextRequest) {
category: 'marker',
icon: 'arrowUp',
is_active: true,
user_id: user.id,
},
{
name: 'break_down',
@ -57,6 +58,7 @@ export async function POST(request: NextRequest) {
category: 'marker',
icon: 'arrowDown',
is_active: true,
user_id: user.id,
},
{
name: 'line',
@ -65,6 +67,7 @@ export async function POST(request: NextRequest) {
category: 'line',
icon: 'line',
is_active: true,
user_id: user.id,
},
];
@ -91,6 +94,7 @@ export async function POST(request: NextRequest) {
category,
icon: icon || null,
is_active: true,
user_id: user.id,
})
.returning();
@ -130,11 +134,11 @@ export async function DELETE(request: NextRequest) {
);
}
// Check if the type exists
// Check if the type exists and belongs to user
const type = await db
.select()
.from(annotationTypes)
.where(eq(annotationTypes.id, parseInt(id)))
.where(and(eq(annotationTypes.id, parseInt(id)), eq(annotationTypes.user_id, user.id)))
.limit(1);
if (type.length === 0) {
@ -144,11 +148,11 @@ export async function DELETE(request: NextRequest) {
);
}
// Check if any annotations use this type
// Check if any of this user's annotations use this type
const existingAnnotations = await db
.select()
.from(annotations)
.where(eq(annotations.label_type, type[0].name))
.where(and(eq(annotations.label_type, type[0].name), eq(annotations.user_id, user.id)))
.limit(1);
if (existingAnnotations.length > 0) {
@ -158,7 +162,7 @@ export async function DELETE(request: NextRequest) {
);
}
await db.delete(annotationTypes).where(eq(annotationTypes.id, parseInt(id)));
await db.delete(annotationTypes).where(and(eq(annotationTypes.id, parseInt(id)), eq(annotationTypes.user_id, user.id)));
return NextResponse.json({ message: 'Annotation type deleted successfully' });
} catch (error) {

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { annotations } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
export async function PATCH(
@ -37,7 +37,7 @@ export async function PATCH(
const result = await db
.update(annotations)
.set({ geometry })
.where(eq(annotations.id, id))
.where(and(eq(annotations.id, id), eq(annotations.user_id, user.id)))
.returning();
if (result.length === 0) {
@ -79,7 +79,7 @@ export async function DELETE(
const result = await db
.delete(annotations)
.where(eq(annotations.id, id))
.where(and(eq(annotations.id, id), eq(annotations.user_id, user.id)))
.returning();
if (result.length === 0) {

View file

@ -17,7 +17,7 @@ export async function GET(request: NextRequest) {
// Fall back to most recent chart if no chartId provided
if (!chartId) {
const latest = await db.select({ id: charts.id }).from(charts).orderBy(desc(charts.created_at)).limit(1);
const latest = await db.select({ id: charts.id }).from(charts).where(eq(charts.user_id, user.id)).orderBy(desc(charts.created_at)).limit(1);
if (latest.length === 0) {
return NextResponse.json([]);
}
@ -35,7 +35,7 @@ export async function GET(request: NextRequest) {
const allAnnotations = await db
.select()
.from(annotations)
.where(eq(annotations.chart_id, chartIdNum));
.where(and(eq(annotations.chart_id, chartIdNum), eq(annotations.user_id, user.id)));
const normalized = allAnnotations.map((a) => ({
...a,
@ -71,8 +71,8 @@ export async function POST(request: NextRequest) {
);
}
// Verify chart exists
const chartExists = await db.select({ id: charts.id }).from(charts).where(eq(charts.id, chart_id)).limit(1);
// Verify chart exists and belongs to user
const chartExists = await db.select({ id: charts.id }).from(charts).where(and(eq(charts.id, chart_id), eq(charts.user_id, user.id))).limit(1);
if (chartExists.length === 0) {
return NextResponse.json(
{ error: 'Chart not found' },
@ -89,6 +89,7 @@ export async function POST(request: NextRequest) {
.insert(annotations)
.values({
chart_id,
user_id: user.id,
timestamp: timestampDate,
label_type,
geometry: geometry || null,
@ -139,7 +140,7 @@ export async function DELETE(request: NextRequest) {
{ status: 400 }
);
}
result = await db.delete(annotations).where(eq(annotations.chart_id, chartIdNum)).returning();
result = await db.delete(annotations).where(and(eq(annotations.chart_id, chartIdNum), eq(annotations.user_id, user.id))).returning();
} else if (type) {
const types = type.split(',').map((t) => t.trim());
if (chartId) {
@ -152,12 +153,12 @@ export async function DELETE(request: NextRequest) {
}
result = await db
.delete(annotations)
.where(and(inArray(annotations.label_type, types), eq(annotations.chart_id, chartIdNum)))
.where(and(inArray(annotations.label_type, types), eq(annotations.chart_id, chartIdNum), eq(annotations.user_id, user.id)))
.returning();
} else {
result = await db
.delete(annotations)
.where(inArray(annotations.label_type, types))
.where(and(inArray(annotations.label_type, types), eq(annotations.user_id, user.id)))
.returning();
}
} else {

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { candles, charts } from '@/lib/db/schema';
import { asc, desc, eq } from 'drizzle-orm';
import { asc, desc, eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
export async function GET(request: NextRequest) {
@ -16,7 +16,7 @@ export async function GET(request: NextRequest) {
// Fall back to most recent chart if no chartId provided
if (!chartId) {
const latest = await db.select({ id: charts.id }).from(charts).orderBy(desc(charts.created_at)).limit(1);
const latest = await db.select({ id: charts.id }).from(charts).where(eq(charts.user_id, user.id)).orderBy(desc(charts.created_at)).limit(1);
if (latest.length === 0) {
return NextResponse.json([]);
}
@ -31,6 +31,12 @@ export async function GET(request: NextRequest) {
);
}
// Verify chart belongs to user
const chartOwner = await db.select({ id: charts.id }).from(charts).where(and(eq(charts.id, chartIdNum), eq(charts.user_id, user.id))).limit(1);
if (chartOwner.length === 0) {
return NextResponse.json({ error: 'Chart not found' }, { status: 404 });
}
const allCandles = await db
.select({
time: candles.time,

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { charts, candles, annotations, spanAnnotations } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
export async function GET(
@ -20,7 +20,7 @@ export async function GET(
return NextResponse.json({ error: 'Invalid chart ID' }, { status: 400 });
}
const result = await db.select().from(charts).where(eq(charts.id, chartId)).limit(1);
const result = await db.select().from(charts).where(and(eq(charts.id, chartId), eq(charts.user_id, user.id))).limit(1);
if (result.length === 0) {
return NextResponse.json({ error: 'Chart not found' }, { status: 404 });
@ -45,7 +45,7 @@ export async function DELETE(
return NextResponse.json({ error: 'Invalid chart ID' }, { status: 400 });
}
const existing = await db.select().from(charts).where(eq(charts.id, chartId)).limit(1);
const existing = await db.select().from(charts).where(and(eq(charts.id, chartId), eq(charts.user_id, user.id))).limit(1);
if (existing.length === 0) {
return NextResponse.json({ error: 'Chart not found' }, { status: 404 });
}

View file

@ -1,7 +1,7 @@
import { NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { charts } from '@/lib/db/schema';
import { desc } from 'drizzle-orm';
import { desc, eq } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
export async function GET() {
@ -10,6 +10,6 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const allCharts = await db.select().from(charts).orderBy(desc(charts.created_at));
const allCharts = await db.select().from(charts).where(eq(charts.user_id, user.id)).orderBy(desc(charts.created_at));
return NextResponse.json(allCharts);
}

View file

@ -23,7 +23,7 @@ export async function GET(request: NextRequest) {
// Fall back to most recent chart if no chartId provided
if (!chartId) {
const latest = await db.select({ id: charts.id }).from(charts).orderBy(desc(charts.created_at)).limit(1);
const latest = await db.select({ id: charts.id }).from(charts).where(eq(charts.user_id, user.id)).orderBy(desc(charts.created_at)).limit(1);
if (latest.length === 0) {
return new NextResponse('timestamp,label_type,price\n', {
headers: {
@ -46,7 +46,7 @@ export async function GET(request: NextRequest) {
const allAnnotations = await db
.select()
.from(annotations)
.where(eq(annotations.chart_id, chartIdNum));
.where(and(eq(annotations.chart_id, chartIdNum), eq(annotations.user_id, user.id)));
// Build CSV content
const csvRows = ['timestamp,label_type,price'];

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { spanAnnotations } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
// PATCH - Update span annotation
@ -29,11 +29,11 @@ export async function PATCH(
const { label, confidence, outcome, notes, sub_spans } = body;
// Check if the span exists
// Check if the span exists and belongs to user
const existing = await db
.select()
.from(spanAnnotations)
.where(eq(spanAnnotations.id, idNum))
.where(and(eq(spanAnnotations.id, idNum), eq(spanAnnotations.user_id, user.id)))
.limit(1);
if (existing.length === 0) {
@ -54,7 +54,7 @@ export async function PATCH(
const result = await db
.update(spanAnnotations)
.set(updates)
.where(eq(spanAnnotations.id, idNum))
.where(and(eq(spanAnnotations.id, idNum), eq(spanAnnotations.user_id, user.id)))
.returning();
const s = result[0];
@ -94,11 +94,11 @@ export async function DELETE(
);
}
// Check if the span exists
// Check if the span exists and belongs to user
const existing = await db
.select()
.from(spanAnnotations)
.where(eq(spanAnnotations.id, idNum))
.where(and(eq(spanAnnotations.id, idNum), eq(spanAnnotations.user_id, user.id)))
.limit(1);
if (existing.length === 0) {
@ -108,7 +108,7 @@ export async function DELETE(
);
}
await db.delete(spanAnnotations).where(eq(spanAnnotations.id, idNum));
await db.delete(spanAnnotations).where(and(eq(spanAnnotations.id, idNum), eq(spanAnnotations.user_id, user.id)));
return NextResponse.json({ message: 'Span annotation deleted successfully' });
} catch (error) {

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { spanAnnotations, charts } from '@/lib/db/schema';
import { eq, desc } from 'drizzle-orm';
import { eq, desc, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
function sanitizeCsvCell(value: string): string {
@ -53,6 +53,7 @@ export async function GET(request: NextRequest) {
const latest = await db
.select({ id: charts.id })
.from(charts)
.where(eq(charts.user_id, user.id))
.orderBy(desc(charts.created_at))
.limit(1);
@ -78,7 +79,7 @@ export async function GET(request: NextRequest) {
const spans = await db
.select()
.from(spanAnnotations)
.where(eq(spanAnnotations.chart_id, chartIdNum))
.where(and(eq(spanAnnotations.chart_id, chartIdNum), eq(spanAnnotations.user_id, user.id)))
.orderBy(spanAnnotations.start_time);
if (format === 'json') {

View file

@ -33,7 +33,7 @@ export async function GET(request: NextRequest) {
const spans = await db
.select()
.from(spanAnnotations)
.where(eq(spanAnnotations.chart_id, chartIdNum))
.where(and(eq(spanAnnotations.chart_id, chartIdNum), eq(spanAnnotations.user_id, user.id)))
.orderBy(desc(spanAnnotations.start_time));
const normalized = spans.map((s) => ({
@ -91,6 +91,7 @@ export async function POST(request: NextRequest) {
.insert(spanAnnotations)
.values({
chart_id,
user_id: user.id,
start_time: new Date(actualStartTime * 1000),
end_time: new Date(actualEndTime * 1000),
label,
@ -148,8 +149,8 @@ export async function DELETE(request: NextRequest) {
);
}
// Build filter conditions
const conditions = [eq(spanAnnotations.chart_id, chartIdInt)];
// Build filter conditions (always scoped to user)
const conditions = [eq(spanAnnotations.chart_id, chartIdInt), eq(spanAnnotations.user_id, user.id)];
if (source) conditions.push(eq(spanAnnotations.source, source));
if (label) conditions.push(eq(spanAnnotations.label, label));

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { spanLabelTypes, spanAnnotations } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
// PATCH - Update span label type
@ -29,11 +29,11 @@ export async function PATCH(
const { name, display_name, color, hotkey, is_active, sort_order } = body;
// Check if the type exists
// Check if the type exists and belongs to user
const existing = await db
.select()
.from(spanLabelTypes)
.where(eq(spanLabelTypes.id, idNum))
.where(and(eq(spanLabelTypes.id, idNum), eq(spanLabelTypes.user_id, user.id)))
.limit(1);
if (existing.length === 0) {
@ -55,7 +55,7 @@ export async function PATCH(
const result = await db
.update(spanLabelTypes)
.set(updates)
.where(eq(spanLabelTypes.id, idNum))
.where(and(eq(spanLabelTypes.id, idNum), eq(spanLabelTypes.user_id, user.id)))
.returning();
return NextResponse.json(result[0]);
@ -97,11 +97,11 @@ export async function DELETE(
);
}
// Check if the type exists
// Check if the type exists and belongs to user
const type = await db
.select()
.from(spanLabelTypes)
.where(eq(spanLabelTypes.id, idNum))
.where(and(eq(spanLabelTypes.id, idNum), eq(spanLabelTypes.user_id, user.id)))
.limit(1);
if (type.length === 0) {
@ -111,11 +111,11 @@ export async function DELETE(
);
}
// Check if any span annotations use this label
// Check if any of this user's span annotations use this label
const existingSpans = await db
.select()
.from(spanAnnotations)
.where(eq(spanAnnotations.label, type[0].name))
.where(and(eq(spanAnnotations.label, type[0].name), eq(spanAnnotations.user_id, user.id)))
.limit(1);
if (existingSpans.length > 0) {
@ -125,7 +125,7 @@ export async function DELETE(
);
}
await db.delete(spanLabelTypes).where(eq(spanLabelTypes.id, idNum));
await db.delete(spanLabelTypes).where(and(eq(spanLabelTypes.id, idNum), eq(spanLabelTypes.user_id, user.id)));
return NextResponse.json({ message: 'Span label type deleted successfully' });
} catch (error) {

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { spanLabelTypes } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
// GET - List all span label types (active only, sorted by sort_order)
@ -15,7 +15,7 @@ export async function GET() {
const types = await db
.select()
.from(spanLabelTypes)
.where(eq(spanLabelTypes.is_active, true))
.where(and(eq(spanLabelTypes.is_active, true), eq(spanLabelTypes.user_id, user.id)))
.orderBy(spanLabelTypes.sort_order);
return NextResponse.json(types);
@ -55,6 +55,7 @@ export async function POST(request: NextRequest) {
hotkey: hotkey || null,
is_active: true,
sort_order: sort_order ?? 0,
user_id: user.id,
})
.returning();

View file

@ -2,17 +2,17 @@ import { NextRequest, NextResponse } from 'next/server';
import Papa from 'papaparse';
import { db } from '@/lib/db';
import { candles, charts } from '@/lib/db/schema';
import { eq, like } from 'drizzle-orm';
import { eq, like, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth';
async function getUniqueChartName(baseName: string): Promise<string> {
// Check if the base name is already taken
const existing = await db.select().from(charts).where(eq(charts.name, baseName)).limit(1);
async function getUniqueChartName(baseName: string, userId: string): Promise<string> {
// Check if the base name is already taken for this user
const existing = await db.select().from(charts).where(and(eq(charts.user_id, userId), eq(charts.name, baseName))).limit(1);
if (existing.length === 0) return baseName;
// Find existing charts with this base name pattern (e.g., "btc-daily-2", "btc-daily-3")
const pattern = `${baseName}-%`;
const suffixed = await db.select({ name: charts.name }).from(charts).where(like(charts.name, pattern));
const suffixed = await db.select({ name: charts.name }).from(charts).where(and(eq(charts.user_id, userId), like(charts.name, pattern)));
let maxSuffix = 1;
for (const row of suffixed) {
@ -124,11 +124,12 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
}
// Get unique chart name (handle duplicates)
const chartName = await getUniqueChartName(baseName);
const chartName = await getUniqueChartName(baseName, user.id);
// Create the chart
const [newChart] = await db.insert(charts).values({
name: chartName,
user_id: user.id,
}).returning();
// Parse and prepare candle data