Add getAuthUser() auth guard to all data API routes (task 7.1)

Add 401 Unauthorized check at the top of every handler in:
- /api/upload (POST)
- /api/candles (GET)
- /api/charts (GET) and /api/charts/[id] (GET, DELETE)
- /api/annotations (GET, POST, DELETE) and /api/annotations/[id] (PATCH, DELETE)
- /api/annotation-types (GET, POST, DELETE) and /api/annotation-types/[id] (PATCH)
- /api/span-annotations (GET, POST, DELETE), /[id] (PATCH, DELETE), /export (GET)
- /api/span-label-types (GET, POST) and /[id] (PATCH, DELETE)
- /api/export (GET) and /api/export/spans (GET)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-20 10:26:09 +01:00
parent aa2c5fdb69
commit 9901d0f3f1
16 changed files with 146 additions and 1 deletions

View file

@ -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

View file

@ -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);

View file

@ -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');

View file

@ -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);

View file

@ -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');

View file

@ -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');

View file

@ -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);

View file

@ -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);
}

View file

@ -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');

View file

@ -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');

View file

@ -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);

View file

@ -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');

View file

@ -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');

View file

@ -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);

View file

@ -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;

View file

@ -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<string> {
// Check if the base name is already taken
@ -25,6 +26,11 @@ async function getUniqueChartName(baseName: string): Promise<string> {
}
export async function POST(request: NextRequest): Promise<NextResponse> {
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;