Skip to content

Commit

Permalink
Improve security for sandbox video stream by introducing token
Browse files Browse the repository at this point in the history
  • Loading branch information
mlejva committed Nov 24, 2024
1 parent a6b3121 commit c3d5bda
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 72 deletions.
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"autoprefixer": "^10.4.7",
"class-variance-authority": "^0.7.0",
"clsx": "^1.2.1",
"e2b": "^0.16.2",
"e2b": "^1.0.5",
"fast-glob": "^3.3.0",
"fast-xml-parser": "^4.3.3",
"flexsearch": "^0.7.31",
Expand Down
42 changes: 42 additions & 0 deletions apps/web/src/app/api/stream/sandbox/[sandboxId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { verifySandbox } from '@/lib/utils'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!

const supabase = createClient(supabaseUrl, supabaseServiceKey)

export async function GET(request: Request, { params }: { params: { sandboxId: string } }) {
const apiKey = request.headers.get('X-API-Key')
const sandboxId = params.sandboxId

if (!sandboxId) {
return NextResponse.json({ error: 'Missing sandbox ID' }, { status: 400 })
}

if (!apiKey) {
return NextResponse.json({ error: 'Missing E2B API Key' }, { status: 400 })
}

if (!(await verifySandbox(apiKey, sandboxId))) {
return NextResponse.json({ error: 'Invalid E2B API Key' }, { status: 401 })
}

const { data: stream, error } = await supabase
.from('sandbox_streams')
.select('token')
.eq('sandbox_id', sandboxId)
.single()

if (error) {
return NextResponse.json({ error: `Failed to retrieve stream - ${error.message}` }, { status: 500 })
}

if (!stream) {
return NextResponse.json({ error: `Stream not found for sandbox ${sandboxId}` }, { status: 404 })
}

return NextResponse.json({ token: stream.token }, { status: 200 })
}

43 changes: 40 additions & 3 deletions apps/web/src/app/api/stream/sandbox/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Mux from '@mux/mux-node'
import { NextResponse } from 'next/server'

import { createClient } from '@supabase/supabase-js'
import { verifySandbox } from '@/lib/utils'

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!
Expand All @@ -13,13 +13,40 @@ const mux = new Mux({
tokenSecret: process.env.MUX_TOKEN_SECRET
})


// Create a new live stream and return the stream key
export async function POST(request: Request) {
const apiKey = request.headers.get('X-API-Key')
const { sandboxId } = await request.json()

if (!sandboxId) {
return NextResponse.json({ error: 'Missing sandboxId' }, { status: 400 })
}

if (!apiKey) {
return NextResponse.json({ error: 'Missing E2B API Key' }, { status: 400 })
}

if (!(await verifySandbox(apiKey, sandboxId))) {
return NextResponse.json({ error: 'Invalid E2B API Key' }, { status: 401 })
}

// Check if a stream already exists for the sandbox
const { data: existingStream, error: existingStreamError } = await supabase
.from('sandbox_streams')
.select('token')
.eq('sandbox_id', sandboxId)
.single()

if (existingStreamError && existingStreamError.code !== 'PGRST116') {
return NextResponse.json({ error: `Failed to check existing stream - ${existingStreamError.message}` }, { status: 500 })
}

if (existingStream) {
return NextResponse.json({ error: `Stream for the sandbox '${sandboxId}' already exists. There can be only one stream per sandbox.` }, { status: 400 })
}

// The stream doesn't exist yet, so create a new live stream
const liveStream = await mux.video.liveStreams.create({
latency_mode: 'low',
reconnect_window: 60,
Expand All @@ -31,9 +58,19 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Failed to create live stream' }, { status: 500 })
}

await supabase
const { data, error }: { data: { token: string } | null, error: any } = await supabase
.from('sandbox_streams')
.insert([{ sandbox_id: sandboxId, playback_id: liveStream.playback_ids[0].id }])
.select('token')
.single()

if (error) {
return NextResponse.json({ error: `Failed to insert and retrieve token - ${error.message}` }, { status: 500 })
}

if (!data) {
return NextResponse.json({ error: 'Failed to insert and retrieve token - no data' }, { status: 500 })
}

return NextResponse.json({ streamKey: liveStream.stream_key }, { status: 201 })
return NextResponse.json({ streamKey: liveStream.stream_key, token: data.token }, { status: 201 })
}
9 changes: 0 additions & 9 deletions apps/web/src/app/stream/sandbox/[sandboxId]/layout.tsx

This file was deleted.

21 changes: 17 additions & 4 deletions apps/web/src/app/stream/sandbox/[sandboxId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ interface SandboxStream {
playbackId: string
}

async function fetchStream(sandboxId: string): Promise<SandboxStream | null> {
async function fetchStream(sandboxId: string, token: string): Promise<SandboxStream | null> {
const { data, error } = await supabase
.from('sandbox_streams')
.select('playback_id')
.eq('sandbox_id', sandboxId)
.eq('token', token)
.single()

if (error || !data) {
Expand All @@ -26,16 +27,28 @@ async function fetchStream(sandboxId: string): Promise<SandboxStream | null> {
return { sandboxId, playbackId: data.playback_id }
}

export default async function StreamPage({ params }: { params: { sandboxId: string } }) {
const stream = await fetchStream(params.sandboxId)
export default async function StreamPage({
params,
searchParams // Add searchParams to props
}: {
params: { sandboxId: string }
searchParams: { token?: string } // Add type for searchParams
}) {
const token = searchParams.token

if (!token) {
return <div>Missing token</div>
}

const stream = await fetchStream(params.sandboxId, token)

if (!stream) {
return <div>Stream not found</div>
}

return (
<Suspense fallback={<div className="h-full w-full flex items-center justify-center">Loading stream...</div>}>
<div className="flex justify-center">
<div className="flex justify-center max-h-[768px]">
<MuxPlayer
autoPlay
muted
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Sandbox } from 'e2b'
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

// Verify that the sandbox exists and is associated with the API key
export async function verifySandbox(apiKey: string, sandboxId: string) {
const sandboxes = await Sandbox.list({ apiKey })
return sandboxes.some((sandbox) => sandbox.sandboxId === sandboxId)
}
70 changes: 15 additions & 55 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c3d5bda

Please sign in to comment.