diff --git a/openspec/changes/user-accounts/tasks.md b/openspec/changes/user-accounts/tasks.md index 126be59..de7466d 100644 --- a/openspec/changes/user-accounts/tasks.md +++ b/openspec/changes/user-accounts/tasks.md @@ -37,7 +37,7 @@ ## 7. Update Existing API Routes -- [ ] 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) - [ ] 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 diff --git a/src/app/api/annotation-types/[id]/route.ts b/src/app/api/annotation-types/[id]/route.ts index 73fbb1e..f40524c 100644 --- a/src/app/api/annotation-types/[id]/route.ts +++ b/src/app/api/annotation-types/[id]/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { annotationTypes } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; type RouteContext = { params: Promise<{ id: string }>; @@ -12,6 +13,11 @@ export async function PATCH( request: NextRequest, context: RouteContext ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { id } = await context.params; const idNum = parseInt(id, 10); diff --git a/src/app/api/annotation-types/route.ts b/src/app/api/annotation-types/route.ts index 7fa0c7d..396391e 100644 --- a/src/app/api/annotation-types/route.ts +++ b/src/app/api/annotation-types/route.ts @@ -2,9 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { annotationTypes, annotations } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; // GET - List all annotation types export async function GET() { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const types = await db.select().from(annotationTypes); return NextResponse.json(types); @@ -19,6 +25,11 @@ export async function GET() { // POST - Create new annotation type or seed defaults export async function POST(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const body = await request.json(); @@ -103,6 +114,11 @@ export async function POST(request: NextRequest) { // DELETE - Delete annotation type (only if no annotations use it) export async function DELETE(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; const id = searchParams.get('id'); diff --git a/src/app/api/annotations/[id]/route.ts b/src/app/api/annotations/[id]/route.ts index f2274c7..5db144b 100644 --- a/src/app/api/annotations/[id]/route.ts +++ b/src/app/api/annotations/[id]/route.ts @@ -2,11 +2,17 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { annotations } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { id: idParam } = await params; const id = parseInt(idParam, 10); @@ -55,6 +61,11 @@ export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { id: idParam } = await params; const id = parseInt(idParam, 10); diff --git a/src/app/api/annotations/route.ts b/src/app/api/annotations/route.ts index e50abcb..514abc7 100644 --- a/src/app/api/annotations/route.ts +++ b/src/app/api/annotations/route.ts @@ -2,9 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { annotations, charts } from '@/lib/db/schema'; import { eq, inArray, and, desc } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; // GET annotations scoped by chartId export async function GET(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; let chartId = searchParams.get('chartId'); @@ -48,6 +54,11 @@ export async function GET(request: NextRequest) { // POST create new annotation (requires chart_id) export async function POST(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const body = await request.json(); const { timestamp, label_type, chart_id, geometry, color } = body; @@ -101,6 +112,11 @@ export async function POST(request: NextRequest) { // DELETE annotations with bulk operations (scoped by chartId) export async function DELETE(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; const type = searchParams.get('type'); diff --git a/src/app/api/candles/route.ts b/src/app/api/candles/route.ts index ee2d3b6..34be8b0 100644 --- a/src/app/api/candles/route.ts +++ b/src/app/api/candles/route.ts @@ -2,8 +2,14 @@ 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 { getAuthUser } from '@/lib/auth'; export async function GET(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; let chartId = searchParams.get('chartId'); diff --git a/src/app/api/charts/[id]/route.ts b/src/app/api/charts/[id]/route.ts index 6be9952..8e24982 100644 --- a/src/app/api/charts/[id]/route.ts +++ b/src/app/api/charts/[id]/route.ts @@ -2,11 +2,17 @@ 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 { getAuthUser } from '@/lib/auth'; export async function GET( _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const { id } = await params; const chartId = parseInt(id, 10); @@ -27,6 +33,11 @@ export async function DELETE( _request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const { id } = await params; const chartId = parseInt(id, 10); diff --git a/src/app/api/charts/route.ts b/src/app/api/charts/route.ts index 50b1990..65bed5d 100644 --- a/src/app/api/charts/route.ts +++ b/src/app/api/charts/route.ts @@ -2,8 +2,14 @@ import { NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { charts } from '@/lib/db/schema'; import { desc } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; export async function GET() { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + const allCharts = await db.select().from(charts).orderBy(desc(charts.created_at)); return NextResponse.json(allCharts); } diff --git a/src/app/api/export/route.ts b/src/app/api/export/route.ts index d39ba09..d00387c 100644 --- a/src/app/api/export/route.ts +++ b/src/app/api/export/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { annotations, candles, charts } from '@/lib/db/schema'; import { eq, and, desc } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; function sanitizeCsvCell(value: string): string { if (/^[=+@\-]/.test(value)) { @@ -11,6 +12,11 @@ function sanitizeCsvCell(value: string): string { } export async function GET(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; let chartId = searchParams.get('chartId'); diff --git a/src/app/api/export/spans/route.ts b/src/app/api/export/spans/route.ts index a008814..6d66c39 100644 --- a/src/app/api/export/spans/route.ts +++ b/src/app/api/export/spans/route.ts @@ -2,12 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { spanAnnotations, candles } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; function toUnix(d: Date): number { return Math.floor(d.getTime() / 1000); } export async function GET(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const searchParams = request.nextUrl.searchParams; const chartId = searchParams.get('chartId'); diff --git a/src/app/api/span-annotations/[id]/route.ts b/src/app/api/span-annotations/[id]/route.ts index 8ce9baa..e77201a 100644 --- a/src/app/api/span-annotations/[id]/route.ts +++ b/src/app/api/span-annotations/[id]/route.ts @@ -2,12 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { spanAnnotations } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; // PATCH - Update span annotation export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { id } = await params; const idNum = parseInt(id, 10); @@ -72,6 +78,11 @@ export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { id } = await params; const idNum = parseInt(id, 10); diff --git a/src/app/api/span-annotations/export/route.ts b/src/app/api/span-annotations/export/route.ts index a3e964e..2b6b5ae 100644 --- a/src/app/api/span-annotations/export/route.ts +++ b/src/app/api/span-annotations/export/route.ts @@ -2,6 +2,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 { getAuthUser } from '@/lib/auth'; function sanitizeCsvCell(value: string): string { if (/^[=+@\-]/.test(value)) { @@ -37,6 +38,11 @@ function sanitizeCsvCell(value: string): string { * } */ export async function GET(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; let chartId = searchParams.get('chartId'); diff --git a/src/app/api/span-annotations/route.ts b/src/app/api/span-annotations/route.ts index b1ec261..2faf191 100644 --- a/src/app/api/span-annotations/route.ts +++ b/src/app/api/span-annotations/route.ts @@ -2,9 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { spanAnnotations } from '@/lib/db/schema'; import { eq, desc, and } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; // GET - List all span annotations for a chart export async function GET(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; const chartId = searchParams.get('chartId'); @@ -49,6 +55,11 @@ export async function GET(request: NextRequest) { // POST - Create new span annotation export async function POST(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const body = await request.json(); const { @@ -111,6 +122,11 @@ export async function POST(request: NextRequest) { // DELETE - Bulk delete span annotations by chartId + optional source/label filters export async function DELETE(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { searchParams } = request.nextUrl; const chartId = searchParams.get('chartId'); diff --git a/src/app/api/span-label-types/[id]/route.ts b/src/app/api/span-label-types/[id]/route.ts index 09b0893..2f1bd78 100644 --- a/src/app/api/span-label-types/[id]/route.ts +++ b/src/app/api/span-label-types/[id]/route.ts @@ -2,12 +2,18 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { spanLabelTypes, spanAnnotations } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; // PATCH - Update span label type export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { id } = await params; const idNum = parseInt(id, 10); @@ -75,6 +81,11 @@ export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const { id } = await params; const idNum = parseInt(id, 10); diff --git a/src/app/api/span-label-types/route.ts b/src/app/api/span-label-types/route.ts index f6563b6..96b9483 100644 --- a/src/app/api/span-label-types/route.ts +++ b/src/app/api/span-label-types/route.ts @@ -2,9 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/lib/db'; import { spanLabelTypes } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; // GET - List all span label types (active only, sorted by sort_order) export async function GET() { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const types = await db .select() @@ -24,6 +30,11 @@ export async function GET() { // POST - Create new span label type export async function POST(request: NextRequest) { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const body = await request.json(); const { name, display_name, color, hotkey, sort_order } = body; diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 10d57a9..4990fde 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -3,6 +3,7 @@ import Papa from 'papaparse'; import { db } from '@/lib/db'; import { candles, charts } from '@/lib/db/schema'; import { eq, like } from 'drizzle-orm'; +import { getAuthUser } from '@/lib/auth'; async function getUniqueChartName(baseName: string): Promise { // Check if the base name is already taken @@ -25,6 +26,11 @@ async function getUniqueChartName(baseName: string): Promise { } export async function POST(request: NextRequest): Promise { + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + try { const formData = await request.formData(); const file = formData.get('file') as File;