Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 72 additions & 47 deletions app/api/classes/[classId]/live-session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type RouteContext = {

type LiveSessionRequestBody = {
action?: "end"
liveSessionId?: string
roomName?: string
}

Expand Down Expand Up @@ -80,12 +81,14 @@ async function endLiveSession({
roomName,
supabase,
userId,
liveSessionId,
}: {
canManage: boolean
classId: string
roomName: string
supabase: SupabaseClient
userId: string
liveSessionId?: string
}) {
const now = new Date().toISOString()
let query = supabase
Expand All @@ -97,15 +100,30 @@ async function endLiveSession({
})
.eq("class_id", classId)
.eq("room_name", roomName)
.eq("status", "live")
.in("status", ["pending", "live"])

if (liveSessionId) {
query = query.eq("live_session_id", liveSessionId)
}

if (!canManage) {
query = query.eq("started_by_user_id", userId)
}

const { error } = await query
const { data, error } = await query.select("id").maybeSingle()

return error
if (error) {
return { message: error.message, status: 500 as const }
}

if (!data) {
return {
message: "No matching live session is active.",
status: 409 as const,
}
}

return null
}

export async function POST(request: Request, context: RouteContext) {
Expand All @@ -123,13 +141,17 @@ export async function POST(request: Request, context: RouteContext) {
const error = await endLiveSession({
canManage: false,
classId,
liveSessionId: body.liveSessionId,
roomName: routeRoomName,
supabase,
userId: user.id,
})

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json(
{ error: error.message },
{ status: error.status },
)
}

return NextResponse.json({ ok: true })
Expand All @@ -153,58 +175,44 @@ export async function POST(request: Request, context: RouteContext) {
)
}

const now = new Date().toISOString()
const staleBefore = Date.now() - LIVE_SESSION_STALE_MS
const { data: existingSession, error: existingError } = await supabase
.from("class_live_sessions")
.select("id, status, last_seen_at")
.eq("class_id", result.classData.id)
.maybeSingle()

if (existingError) {
return NextResponse.json({ error: existingError.message }, { status: 500 })
}

const hasFreshLiveSession =
existingSession?.status === "live" &&
Date.parse(existingSession.last_seen_at) > staleBefore

if (hasFreshLiveSession) {
const { error } = await supabase
.from("class_live_sessions")
.update({
room_name: roomName,
started_by_user_id: user.id,
last_seen_at: now,
ended_at: null,
})
.eq("id", existingSession.id)

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}

return NextResponse.json({ ok: true })
if (!body?.liveSessionId) {
return NextResponse.json(
{ error: "A liveSessionId is required to mark a session live." },
{ status: 400 },
)
}

const { error } = await supabase.from("class_live_sessions").upsert(
{
organization_id: result.classData.organization_id,
class_id: result.classData.id,
const now = new Date().toISOString()
const staleBefore = new Date(Date.now() - LIVE_SESSION_STALE_MS).toISOString()
const { data: liveSession, error } = await supabase
.from("class_live_sessions")
.update({
room_name: roomName,
started_by_user_id: user.id,
status: "live",
started_at: now,
last_seen_at: now,
ended_at: null,
},
{ onConflict: "class_id" },
)
})
.eq("class_id", result.classData.id)
.eq("room_name", roomName)
.eq("live_session_id", body.liveSessionId)
.in("status", ["pending", "live"])
.is("ended_at", null)
.gt("last_seen_at", staleBefore)
.select("live_session_id")
.maybeSingle()

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}

if (!liveSession) {
return NextResponse.json(
{ error: "No matching claimed live session is active." },
{ status: 409 },
)
}

if (result.classData.teacher_user_id === user.id) {
const cooldownBucket = Math.floor(Date.now() / (10 * 60 * 1000))

Expand All @@ -227,7 +235,10 @@ export async function POST(request: Request, context: RouteContext) {
}).catch(() => null)
}

return NextResponse.json({ ok: true })
return NextResponse.json({
ok: true,
liveSessionId: liveSession.live_session_id,
})
}

export async function PATCH(request: Request, context: RouteContext) {
Expand All @@ -248,18 +259,31 @@ export async function PATCH(request: Request, context: RouteContext) {

const body = await readLiveSessionBody(request)
const roomName = body?.roomName || `class-${result.classData.id}`
const { error } = await supabase
let query = supabase
.from("class_live_sessions")
.update({ last_seen_at: new Date().toISOString() })
.eq("class_id", result.classData.id)
.eq("room_name", roomName)
.eq("status", "live")
.is("ended_at", null)

if (body?.liveSessionId) {
query = query.eq("live_session_id", body.liveSessionId)
}

const { data, error } = await query.select("id").maybeSingle()

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}

if (!data) {
return NextResponse.json(
{ error: "No matching live session is active." },
{ status: 409 },
)
}

return NextResponse.json({ ok: true })
}

Expand All @@ -282,13 +306,14 @@ export async function DELETE(request: Request, context: RouteContext) {
const error = await endLiveSession({
canManage: result.canManage,
classId: result.classData.id,
liveSessionId: body?.liveSessionId,
roomName,
supabase,
userId: user.id,
})

if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json({ error: error.message }, { status: error.status })
}

return NextResponse.json({ ok: true })
Expand Down
34 changes: 33 additions & 1 deletion app/api/livekit/token/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const LIVE_SESSION_STALE_MS = 5 * 60 * 1000

interface TokenRequestBody {
classId?: string
liveSessionId?: string
user?: {
id?: string
name?: string
Expand Down Expand Up @@ -111,14 +112,15 @@ export async function POST(request: Request) {

const classId = classData.id
const roomName = `class-${classId}`
let liveSessionId = body.liveSessionId

if (!canManage) {
const staleBefore = new Date(
Date.now() - LIVE_SESSION_STALE_MS,
).toISOString()
const { data: liveSession, error: liveSessionError } = await supabase
.from("class_live_sessions")
.select("id")
.select("id, live_session_id")
.eq("class_id", classId)
.eq("room_name", roomName)
.eq("status", "live")
Expand All @@ -139,12 +141,41 @@ export async function POST(request: Request) {
{ status: 409 },
)
}

if (liveSessionId && liveSession.live_session_id !== liveSessionId) {
return NextResponse.json(
{ error: "This live session is no longer active." },
{ status: 409 },
)
}

liveSessionId = liveSession.live_session_id
} else {
const staleBefore = new Date(
Date.now() - LIVE_SESSION_STALE_MS,
).toISOString()
const { data: claimedLiveSession, error: claimError } = await supabase
.rpc("claim_class_live_session", {
target_org_id: classData.organization_id,
target_class_id: classId,
target_room_name: roomName,
stale_before: staleBefore,
})
.single()

if (claimError) {
return NextResponse.json({ error: claimError.message }, { status: 500 })
}

const liveSession = claimedLiveSession as { live_session_id: string }
liveSessionId = liveSession.live_session_id
}

const metadata = JSON.stringify({
avatar: body.user.avatar ?? body.user.name.slice(0, 2).toUpperCase(),
role: body.user.role ?? "student",
classId,
liveSessionId,
})

const token = new AccessToken(apiKey, apiSecret, {
Expand All @@ -164,6 +195,7 @@ export async function POST(request: Request) {
return NextResponse.json({
serverUrl,
roomName,
liveSessionId,
participantToken: await token.toJwt(),
})
}
Loading
Loading