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

@ -38,7 +38,7 @@
## 7. Update Existing API Routes ## 7. Update Existing API Routes
- [x] 7.1 `[sonnet]` Add `getAuthUser()` check to all data API routes: `/api/upload`, `/api/candles`, `/api/charts`, `/api/annotations`, `/api/annotation-types`, `/api/span-annotations`, `/api/span-label-types`, `/api/export` - [x] 7.1 `[sonnet]` Add `getAuthUser()` check to all data API routes: `/api/upload`, `/api/candles`, `/api/charts`, `/api/annotations`, `/api/annotation-types`, `/api/span-annotations`, `/api/span-label-types`, `/api/export`
- [ ] 7.2 `[opus]` Update all Drizzle queries to filter by `user_id` from authenticated session (SELECT, INSERT, DELETE) - [x] 7.2 `[opus]` Update all Drizzle queries to filter by `user_id` from authenticated session (SELECT, INSERT, DELETE)
- [ ] 7.3 `[sonnet]` Add `getAuthUser()` check to all proxy API routes: `/api/predict`, `/api/predict/batch`, `/api/model/info`, `/api/model/load`, `/api/patterns/detect`, `/api/patterns/available`, `/api/training/start`, `/api/training/runs` - [ ] 7.3 `[sonnet]` Add `getAuthUser()` check to all proxy API routes: `/api/predict`, `/api/predict/batch`, `/api/model/info`, `/api/model/load`, `/api/patterns/detect`, `/api/patterns/available`, `/api/training/start`, `/api/training/runs`
- [ ] 7.4 `[haiku]` Add `X-User-ID` header to all fetch calls from proxy routes to the FastAPI ML service - [ ] 7.4 `[haiku]` Add `X-User-ID` header to all fetch calls from proxy routes to the FastAPI ML service

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { candles, charts } from '@/lib/db/schema'; 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'; import { getAuthUser } from '@/lib/auth';
export async function GET(request: NextRequest) { 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 // Fall back to most recent chart if no chartId provided
if (!chartId) { 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) { if (latest.length === 0) {
return NextResponse.json([]); 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 const allCandles = await db
.select({ .select({
time: candles.time, time: candles.time,

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { charts, candles, annotations, spanAnnotations } from '@/lib/db/schema'; 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'; import { getAuthUser } from '@/lib/auth';
export async function GET( export async function GET(
@ -20,7 +20,7 @@ export async function GET(
return NextResponse.json({ error: 'Invalid chart ID' }, { status: 400 }); 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) { if (result.length === 0) {
return NextResponse.json({ error: 'Chart not found' }, { status: 404 }); 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 }); 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) { if (existing.length === 0) {
return NextResponse.json({ error: 'Chart not found' }, { status: 404 }); return NextResponse.json({ error: 'Chart not found' }, { status: 404 });
} }

View file

@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { charts } from '@/lib/db/schema'; import { charts } from '@/lib/db/schema';
import { desc } from 'drizzle-orm'; import { desc, eq } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth'; import { getAuthUser } from '@/lib/auth';
export async function GET() { export async function GET() {
@ -10,6 +10,6 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 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); 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 // Fall back to most recent chart if no chartId provided
if (!chartId) { 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) { if (latest.length === 0) {
return new NextResponse('timestamp,label_type,price\n', { return new NextResponse('timestamp,label_type,price\n', {
headers: { headers: {
@ -46,7 +46,7 @@ export async function GET(request: NextRequest) {
const allAnnotations = await db const allAnnotations = await db
.select() .select()
.from(annotations) .from(annotations)
.where(eq(annotations.chart_id, chartIdNum)); .where(and(eq(annotations.chart_id, chartIdNum), eq(annotations.user_id, user.id)));
// Build CSV content // Build CSV content
const csvRows = ['timestamp,label_type,price']; const csvRows = ['timestamp,label_type,price'];

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { spanLabelTypes, spanAnnotations } from '@/lib/db/schema'; import { spanLabelTypes, spanAnnotations } from '@/lib/db/schema';
import { eq } from 'drizzle-orm'; import { eq, and } from 'drizzle-orm';
import { getAuthUser } from '@/lib/auth'; import { getAuthUser } from '@/lib/auth';
// PATCH - Update span label type // PATCH - Update span label type
@ -29,11 +29,11 @@ export async function PATCH(
const { name, display_name, color, hotkey, is_active, sort_order } = body; 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 const existing = await db
.select() .select()
.from(spanLabelTypes) .from(spanLabelTypes)
.where(eq(spanLabelTypes.id, idNum)) .where(and(eq(spanLabelTypes.id, idNum), eq(spanLabelTypes.user_id, user.id)))
.limit(1); .limit(1);
if (existing.length === 0) { if (existing.length === 0) {
@ -55,7 +55,7 @@ export async function PATCH(
const result = await db const result = await db
.update(spanLabelTypes) .update(spanLabelTypes)
.set(updates) .set(updates)
.where(eq(spanLabelTypes.id, idNum)) .where(and(eq(spanLabelTypes.id, idNum), eq(spanLabelTypes.user_id, user.id)))
.returning(); .returning();
return NextResponse.json(result[0]); 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 const type = await db
.select() .select()
.from(spanLabelTypes) .from(spanLabelTypes)
.where(eq(spanLabelTypes.id, idNum)) .where(and(eq(spanLabelTypes.id, idNum), eq(spanLabelTypes.user_id, user.id)))
.limit(1); .limit(1);
if (type.length === 0) { 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 const existingSpans = await db
.select() .select()
.from(spanAnnotations) .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); .limit(1);
if (existingSpans.length > 0) { 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' }); return NextResponse.json({ message: 'Span label type deleted successfully' });
} catch (error) { } catch (error) {

View file

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

View file

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