Skip to content

Commit ef2b8b2

Browse files
committed
follow nextjs route gen strat
1 parent 6a6c9c3 commit ef2b8b2

4 files changed

Lines changed: 108 additions & 177 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { getSession } from '@/lib/auth'
4+
import { toError } from '@/lib/core/utils/helpers'
5+
import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants'
6+
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
7+
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
8+
import type { SandboxTaskId } from '@/sandbox-tasks/registry'
9+
10+
/**
11+
* Config for a document preview route handler.
12+
*
13+
* All three document preview endpoints (PDF / PPTX / DOCX) share the same
14+
* shape: auth → workspace membership → JSON body parse → `code` validation →
15+
* size guard → `runSandboxTask(taskId, ...)` → binary response. The only
16+
* differences between them are the sandbox task, the response MIME type, and
17+
* the logger/label used for the 500 path.
18+
*/
19+
export interface DocumentPreviewRouteConfig {
20+
/** Sandbox task registered in `apps/sim/sandbox-tasks/registry.ts`. */
21+
taskId: SandboxTaskId
22+
/** Content-Type of the binary returned on success. */
23+
contentType: string
24+
/** Short label used for the logger name + 500 log message. */
25+
label: 'PDF' | 'PPTX' | 'DOCX'
26+
}
27+
28+
/**
29+
* Build a Next.js POST handler for one of the document preview endpoints.
30+
*
31+
* Everything security-relevant (session, workspace membership, JSON shape,
32+
* empty/oversized code) is enforced before we ever reach the isolated-vm
33+
* sandbox, and `runSandboxTask` is always invoked with the session owner key
34+
* + `req.signal` so pool fairness and client-disconnect cancellation behave
35+
* identically across all three formats.
36+
*/
37+
export function createDocumentPreviewRoute(config: DocumentPreviewRouteConfig) {
38+
const logger = createLogger(`${config.label}PreviewAPI`)
39+
40+
return async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
41+
const { id: workspaceId } = await params
42+
43+
try {
44+
const session = await getSession()
45+
if (!session?.user?.id) {
46+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
47+
}
48+
49+
const membership = await verifyWorkspaceMembership(session.user.id, workspaceId)
50+
if (!membership) {
51+
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
52+
}
53+
54+
let body: unknown
55+
try {
56+
body = await req.json()
57+
} catch {
58+
return NextResponse.json({ error: 'Invalid or missing JSON body' }, { status: 400 })
59+
}
60+
const { code } = body as { code?: string }
61+
62+
if (typeof code !== 'string' || code.trim().length === 0) {
63+
return NextResponse.json({ error: 'code is required' }, { status: 400 })
64+
}
65+
66+
if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) {
67+
return NextResponse.json({ error: 'code exceeds maximum size' }, { status: 413 })
68+
}
69+
70+
const buffer = await runSandboxTask(
71+
config.taskId,
72+
{ code, workspaceId },
73+
{ ownerKey: `user:${session.user.id}`, signal: req.signal }
74+
)
75+
76+
return new NextResponse(new Uint8Array(buffer), {
77+
status: 200,
78+
headers: {
79+
'Content-Type': config.contentType,
80+
'Content-Length': String(buffer.length),
81+
'Cache-Control': 'private, no-store',
82+
},
83+
})
84+
} catch (err) {
85+
const message = toError(err).message
86+
logger.error(`${config.label} preview generation failed`, { error: message, workspaceId })
87+
return NextResponse.json({ error: message }, { status: 500 })
88+
}
89+
}
90+
}
Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,14 @@
1-
import { createLogger } from '@sim/logger'
2-
import { type NextRequest, NextResponse } from 'next/server'
3-
import { getSession } from '@/lib/auth'
4-
import { toError } from '@/lib/core/utils/helpers'
5-
import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants'
6-
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
7-
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
1+
import { createDocumentPreviewRoute } from '@/app/api/workspaces/[id]/_preview/create-preview-route'
82

93
export const dynamic = 'force-dynamic'
104
export const runtime = 'nodejs'
115

12-
const logger = createLogger('DocxPreviewAPI')
13-
146
/**
157
* POST /api/workspaces/[id]/docx/preview
168
* Compile docx source code and return the binary DOCX for streaming preview.
179
*/
18-
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
19-
const { id: workspaceId } = await params
20-
21-
try {
22-
const session = await getSession()
23-
if (!session?.user?.id) {
24-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
25-
}
26-
27-
const membership = await verifyWorkspaceMembership(session.user.id, workspaceId)
28-
if (!membership) {
29-
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
30-
}
31-
32-
let body: unknown
33-
try {
34-
body = await req.json()
35-
} catch {
36-
return NextResponse.json({ error: 'Invalid or missing JSON body' }, { status: 400 })
37-
}
38-
const { code } = body as { code?: string }
39-
40-
if (typeof code !== 'string' || code.trim().length === 0) {
41-
return NextResponse.json({ error: 'code is required' }, { status: 400 })
42-
}
43-
44-
if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) {
45-
return NextResponse.json({ error: 'code exceeds maximum size' }, { status: 413 })
46-
}
47-
48-
const buffer = await runSandboxTask(
49-
'docx-generate',
50-
{ code, workspaceId },
51-
{ ownerKey: `user:${session.user.id}`, signal: req.signal }
52-
)
53-
54-
return new NextResponse(new Uint8Array(buffer), {
55-
status: 200,
56-
headers: {
57-
'Content-Type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
58-
'Content-Length': String(buffer.length),
59-
'Cache-Control': 'private, no-store',
60-
},
61-
})
62-
} catch (err) {
63-
const message = toError(err).message
64-
logger.error('DOCX preview generation failed', { error: message, workspaceId })
65-
return NextResponse.json({ error: message }, { status: 500 })
66-
}
67-
}
10+
export const POST = createDocumentPreviewRoute({
11+
taskId: 'docx-generate',
12+
contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
13+
label: 'DOCX',
14+
})
Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,14 @@
1-
import { createLogger } from '@sim/logger'
2-
import { type NextRequest, NextResponse } from 'next/server'
3-
import { getSession } from '@/lib/auth'
4-
import { toError } from '@/lib/core/utils/helpers'
5-
import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants'
6-
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
7-
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
1+
import { createDocumentPreviewRoute } from '@/app/api/workspaces/[id]/_preview/create-preview-route'
82

93
export const dynamic = 'force-dynamic'
104
export const runtime = 'nodejs'
115

12-
const logger = createLogger('PdfPreviewAPI')
13-
146
/**
157
* POST /api/workspaces/[id]/pdf/preview
168
* Compile PDF-Lib source code and return the binary PDF for streaming preview.
179
*/
18-
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
19-
const { id: workspaceId } = await params
20-
21-
try {
22-
const session = await getSession()
23-
if (!session?.user?.id) {
24-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
25-
}
26-
27-
const membership = await verifyWorkspaceMembership(session.user.id, workspaceId)
28-
if (!membership) {
29-
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
30-
}
31-
32-
let body: unknown
33-
try {
34-
body = await req.json()
35-
} catch {
36-
return NextResponse.json({ error: 'Invalid or missing JSON body' }, { status: 400 })
37-
}
38-
const { code } = body as { code?: string }
39-
40-
if (typeof code !== 'string' || code.trim().length === 0) {
41-
return NextResponse.json({ error: 'code is required' }, { status: 400 })
42-
}
43-
44-
if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) {
45-
return NextResponse.json({ error: 'code exceeds maximum size' }, { status: 413 })
46-
}
47-
48-
const buffer = await runSandboxTask(
49-
'pdf-generate',
50-
{ code, workspaceId },
51-
{ ownerKey: `user:${session.user.id}`, signal: req.signal }
52-
)
53-
54-
return new NextResponse(new Uint8Array(buffer), {
55-
status: 200,
56-
headers: {
57-
'Content-Type': 'application/pdf',
58-
'Content-Length': String(buffer.length),
59-
'Cache-Control': 'private, no-store',
60-
},
61-
})
62-
} catch (err) {
63-
const message = toError(err).message
64-
logger.error('PDF preview generation failed', { error: message, workspaceId })
65-
return NextResponse.json({ error: message }, { status: 500 })
66-
}
67-
}
10+
export const POST = createDocumentPreviewRoute({
11+
taskId: 'pdf-generate',
12+
contentType: 'application/pdf',
13+
label: 'PDF',
14+
})
Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,14 @@
1-
import { createLogger } from '@sim/logger'
2-
import { type NextRequest, NextResponse } from 'next/server'
3-
import { getSession } from '@/lib/auth'
4-
import { toError } from '@/lib/core/utils/helpers'
5-
import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants'
6-
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
7-
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
1+
import { createDocumentPreviewRoute } from '@/app/api/workspaces/[id]/_preview/create-preview-route'
82

93
export const dynamic = 'force-dynamic'
104
export const runtime = 'nodejs'
115

12-
const logger = createLogger('PptxPreviewAPI')
13-
146
/**
157
* POST /api/workspaces/[id]/pptx/preview
168
* Compile PptxGenJS source code and return the binary PPTX for streaming preview.
179
*/
18-
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
19-
const { id: workspaceId } = await params
20-
21-
try {
22-
const session = await getSession()
23-
if (!session?.user?.id) {
24-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
25-
}
26-
27-
const membership = await verifyWorkspaceMembership(session.user.id, workspaceId)
28-
if (!membership) {
29-
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
30-
}
31-
32-
let body: unknown
33-
try {
34-
body = await req.json()
35-
} catch {
36-
return NextResponse.json({ error: 'Invalid or missing JSON body' }, { status: 400 })
37-
}
38-
const { code } = body as { code?: string }
39-
40-
if (typeof code !== 'string' || code.trim().length === 0) {
41-
return NextResponse.json({ error: 'code is required' }, { status: 400 })
42-
}
43-
44-
if (Buffer.byteLength(code, 'utf-8') > MAX_DOCUMENT_PREVIEW_CODE_BYTES) {
45-
return NextResponse.json({ error: 'code exceeds maximum size' }, { status: 413 })
46-
}
47-
48-
const buffer = await runSandboxTask(
49-
'pptx-generate',
50-
{ code, workspaceId },
51-
{ ownerKey: `user:${session.user.id}`, signal: req.signal }
52-
)
53-
54-
return new NextResponse(new Uint8Array(buffer), {
55-
status: 200,
56-
headers: {
57-
'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
58-
'Content-Length': String(buffer.length),
59-
'Cache-Control': 'private, no-store',
60-
},
61-
})
62-
} catch (err) {
63-
const message = toError(err).message
64-
logger.error('PPTX preview generation failed', { error: message, workspaceId })
65-
return NextResponse.json({ error: message }, { status: 500 })
66-
}
67-
}
10+
export const POST = createDocumentPreviewRoute({
11+
taskId: 'pptx-generate',
12+
contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
13+
label: 'PPTX',
14+
})

0 commit comments

Comments
 (0)