Skip to content

Commit c87f891

Browse files
authored
Merge pull request #591 from T-kesh/dev
feat: implement CRUD API endpoints for user stream bookmarks
2 parents f92440a + cbeacff commit c87f891

2 files changed

Lines changed: 157 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { sql } from "@vercel/postgres";
3+
import { verifySession } from "@/lib/auth/verify-session";
4+
5+
/**
6+
* DELETE /api/routes-f/stream/markers/[id] — remove a bookmark
7+
*/
8+
9+
export async function DELETE(
10+
req: NextRequest,
11+
{ params }: { params: { id: string } }
12+
) {
13+
const session = await verifySession(req);
14+
if (!session.ok) return session.response;
15+
16+
const { id } = params;
17+
18+
if (!id) {
19+
return NextResponse.json(
20+
{ error: "Marker ID is required" },
21+
{ status: 400 }
22+
);
23+
}
24+
25+
try {
26+
// Delete only if it belongs to the authenticated user
27+
const result = await sql`
28+
DELETE FROM stream_markers
29+
WHERE id = ${id} AND user_id = ${session.userId}
30+
`;
31+
32+
if (result.rowCount === 0) {
33+
return NextResponse.json(
34+
{ error: "Marker not found or unauthorized" },
35+
{ status: 404 }
36+
);
37+
}
38+
39+
return NextResponse.json({ message: "Marker deleted successfully" });
40+
} catch (error) {
41+
console.error("[DELETE Marker] Error:", error);
42+
return NextResponse.json(
43+
{ error: "Failed to delete marker" },
44+
{ status: 500 }
45+
);
46+
}
47+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { sql } from "@vercel/postgres";
3+
import { verifySession } from "@/lib/auth/verify-session";
4+
5+
/**
6+
* GET /api/routes-f/stream/markers?recording_id= — list own bookmarks for a recording
7+
* POST /api/routes-f/stream/markers — add a bookmark (recording_id, timestamp_seconds, note?)
8+
*/
9+
10+
async function ensureTableExists() {
11+
await sql`
12+
CREATE TABLE IF NOT EXISTS stream_markers (
13+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
14+
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
15+
recording_id VARCHAR(255) NOT NULL,
16+
timestamp_seconds INTEGER NOT NULL,
17+
note VARCHAR(100),
18+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
19+
);
20+
`;
21+
await sql`
22+
CREATE INDEX IF NOT EXISTS idx_stream_markers_user_recording ON stream_markers(user_id, recording_id);
23+
`;
24+
}
25+
26+
export async function GET(req: NextRequest) {
27+
const session = await verifySession(req);
28+
if (!session.ok) return session.response;
29+
30+
const { searchParams } = new URL(req.url);
31+
const recordingId = searchParams.get("recording_id");
32+
33+
if (!recordingId) {
34+
return NextResponse.json(
35+
{ error: "recording_id is required" },
36+
{ status: 400 }
37+
);
38+
}
39+
40+
try {
41+
await ensureTableExists();
42+
const { rows } = await sql`
43+
SELECT id, recording_id, timestamp_seconds, note, created_at
44+
FROM stream_markers
45+
WHERE user_id = ${session.userId} AND recording_id = ${recordingId}
46+
ORDER BY timestamp_seconds ASC
47+
`;
48+
49+
return NextResponse.json(rows);
50+
} catch (error) {
51+
console.error("[GET Markers] Error:", error);
52+
return NextResponse.json(
53+
{ error: "Failed to fetch markers" },
54+
{ status: 500 }
55+
);
56+
}
57+
}
58+
59+
export async function POST(req: NextRequest) {
60+
const session = await verifySession(req);
61+
if (!session.ok) return session.response;
62+
63+
try {
64+
const { recording_id, timestamp_seconds, note } = await req.json();
65+
66+
if (!recording_id || typeof timestamp_seconds !== "number") {
67+
return NextResponse.json(
68+
{ error: "recording_id and timestamp_seconds are required" },
69+
{ status: 400 }
70+
);
71+
}
72+
73+
if (note && note.length > 100) {
74+
return NextResponse.json(
75+
{ error: "Note must be 100 characters or less" },
76+
{ status: 400 }
77+
);
78+
}
79+
80+
await ensureTableExists();
81+
82+
// Check limit: Max 50 bookmarks per recording per user
83+
const { rows: countRows } = await sql`
84+
SELECT COUNT(*)::int as count
85+
FROM stream_markers
86+
WHERE user_id = ${session.userId} AND recording_id = ${recording_id}
87+
`;
88+
89+
if (countRows[0].count >= 50) {
90+
return NextResponse.json(
91+
{ error: "Maximum 50 bookmarks per recording reached" },
92+
{ status: 400 }
93+
);
94+
}
95+
96+
const { rows } = await sql`
97+
INSERT INTO stream_markers (user_id, recording_id, timestamp_seconds, note)
98+
VALUES (${session.userId}, ${recording_id}, ${timestamp_seconds}, ${note || null})
99+
RETURNING id, recording_id, timestamp_seconds, note, created_at
100+
`;
101+
102+
return NextResponse.json(rows[0], { status: 201 });
103+
} catch (error) {
104+
console.error("[POST Markers] Error:", error);
105+
return NextResponse.json(
106+
{ error: "Failed to add marker" },
107+
{ status: 500 }
108+
);
109+
}
110+
}

0 commit comments

Comments
 (0)