@@ -8,21 +8,61 @@ import {
88 isUsingCloudStorage ,
99 type StorageContext ,
1010} from '@/lib/uploads'
11+ import {
12+ signUploadToken ,
13+ type UploadTokenPayload ,
14+ verifyUploadToken ,
15+ } from '@/lib/uploads/core/upload-token'
16+ import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1117
1218const logger = createLogger ( 'MultipartUploadAPI' )
1319
20+ const ALLOWED_UPLOAD_CONTEXTS = new Set < StorageContext > ( [
21+ 'knowledge-base' ,
22+ 'chat' ,
23+ 'copilot' ,
24+ 'mothership' ,
25+ 'execution' ,
26+ 'workspace' ,
27+ 'profile-pictures' ,
28+ 'og-images' ,
29+ 'logs' ,
30+ 'workspace-logos' ,
31+ ] )
32+
1433interface InitiateMultipartRequest {
1534 fileName : string
1635 contentType : string
1736 fileSize : number
37+ workspaceId : string
1838 context ?: StorageContext
1939}
2040
21- interface GetPartUrlsRequest {
22- uploadId : string
23- key : string
41+ interface TokenBoundRequest {
42+ uploadToken : string
43+ }
44+
45+ interface GetPartUrlsRequest extends TokenBoundRequest {
2446 partNumbers : number [ ]
25- context ?: StorageContext
47+ }
48+
49+ interface CompleteSingleRequest extends TokenBoundRequest {
50+ parts : unknown
51+ }
52+
53+ interface CompleteBatchRequest {
54+ uploads : Array < TokenBoundRequest & { parts : unknown } >
55+ }
56+
57+ const verifyTokenForUser = ( token : string | undefined , userId : string ) => {
58+ if ( ! token || typeof token !== 'string' ) {
59+ return null
60+ }
61+ const result = verifyUploadToken ( token )
62+ if ( ! result . valid || result . payload . userId !== userId ) {
63+ return null
64+ }
65+ return result . payload
2666}
2767
2868export const POST = withRouteHandler ( async ( request : NextRequest ) => {
@@ -31,6 +71,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3171 if ( ! session ?. user ?. id ) {
3272 return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } )
3373 }
74+ const userId = session . user . id
3475
3576 const action = request . nextUrl . searchParams . get ( 'action' )
3677
@@ -45,32 +86,34 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4586
4687 switch ( action ) {
4788 case 'initiate' : {
48- const data : InitiateMultipartRequest = await request . json ( )
49- const { fileName, contentType, fileSize, context = 'knowledge-base' } = data
89+ const data = ( await request . json ( ) ) as InitiateMultipartRequest
90+ const { fileName, contentType, fileSize, workspaceId , context = 'knowledge-base' } = data
5091
51- const config = getStorageConfig ( context )
92+ if ( ! workspaceId || typeof workspaceId !== 'string' ) {
93+ return NextResponse . json ( { error : 'workspaceId is required' } , { status : 400 } )
94+ }
5295
53- if ( storageProvider === 's3' ) {
54- const { initiateS3MultipartUpload } = await import ( '@/lib/uploads/providers/s3/client' )
96+ if ( ! ALLOWED_UPLOAD_CONTEXTS . has ( context ) ) {
97+ return NextResponse . json ( { error : 'Invalid storage context' } , { status : 400 } )
98+ }
5599
56- const result = await initiateS3MultipartUpload ( {
57- fileName,
58- contentType,
59- fileSize,
60- } )
100+ const permission = await getUserEntityPermissions ( userId , 'workspace' , workspaceId )
101+ if ( permission !== 'write' && permission !== 'admin' ) {
102+ return NextResponse . json ( { error : 'Forbidden' } , { status : 403 } )
103+ }
61104
62- logger . info (
63- `Initiated S3 multipart upload for ${ fileName } (context: ${ context } ): ${ result . uploadId } `
64- )
105+ const config = getStorageConfig ( context )
65106
66- return NextResponse . json ( {
67- uploadId : result . uploadId ,
68- key : result . key ,
69- } )
70- }
71- if ( storageProvider === 'blob' ) {
72- const { initiateMultipartUpload } = await import ( '@/lib/uploads/providers/blob/client' )
107+ let uploadId : string
108+ let key : string
73109
110+ if ( storageProvider === 's3' ) {
111+ const { initiateS3MultipartUpload } = await import ( '@/lib/uploads/providers/s3/client' )
112+ const result = await initiateS3MultipartUpload ( { fileName, contentType, fileSize } )
113+ uploadId = result . uploadId
114+ key = result . key
115+ } else if ( storageProvider === 'blob' ) {
116+ const { initiateMultipartUpload } = await import ( '@/lib/uploads/providers/blob/client' )
74117 const result = await initiateMultipartUpload ( {
75118 fileName,
76119 contentType,
@@ -82,46 +125,55 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
82125 connectionString : config . connectionString ,
83126 } ,
84127 } )
85-
86- logger . info (
87- `Initiated Azure multipart upload for ${ fileName } (context: ${ context } ): ${ result . uploadId } `
128+ uploadId = result . uploadId
129+ key = result . key
130+ } else {
131+ return NextResponse . json (
132+ { error : `Unsupported storage provider: ${ storageProvider } ` } ,
133+ { status : 400 }
88134 )
89-
90- return NextResponse . json ( {
91- uploadId : result . uploadId ,
92- key : result . key ,
93- } )
94135 }
95136
96- return NextResponse . json (
97- { error : `Unsupported storage provider: ${ storageProvider } ` } ,
98- { status : 400 }
137+ const uploadToken = signUploadToken ( {
138+ uploadId,
139+ key,
140+ userId,
141+ workspaceId,
142+ context,
143+ } )
144+
145+ logger . info (
146+ `Initiated ${ storageProvider } multipart upload for ${ fileName } (context: ${ context } , workspace: ${ workspaceId } ): ${ uploadId } `
99147 )
148+
149+ return NextResponse . json ( { uploadId, key, uploadToken } )
100150 }
101151
102152 case 'get-part-urls' : {
103- const data : GetPartUrlsRequest = await request . json ( )
104- const { uploadId, key, partNumbers, context = 'knowledge-base' } = data
153+ const data = ( await request . json ( ) ) as GetPartUrlsRequest
154+ const { partNumbers } = data
155+
156+ const tokenPayload = verifyTokenForUser ( data . uploadToken , userId )
157+ if ( ! tokenPayload ) {
158+ return NextResponse . json ( { error : 'Invalid or expired upload token' } , { status : 403 } )
159+ }
105160
161+ const { uploadId, key, context } = tokenPayload
106162 const config = getStorageConfig ( context )
107163
108164 if ( storageProvider === 's3' ) {
109165 const { getS3MultipartPartUrls } = await import ( '@/lib/uploads/providers/s3/client' )
110-
111166 const presignedUrls = await getS3MultipartPartUrls ( key , uploadId , partNumbers )
112-
113167 return NextResponse . json ( { presignedUrls } )
114168 }
115169 if ( storageProvider === 'blob' ) {
116170 const { getMultipartPartUrls } = await import ( '@/lib/uploads/providers/blob/client' )
117-
118171 const presignedUrls = await getMultipartPartUrls ( key , partNumbers , {
119172 containerName : config . containerName ! ,
120173 accountName : config . accountName ! ,
121174 accountKey : config . accountKey ,
122175 connectionString : config . connectionString ,
123176 } )
124-
125177 return NextResponse . json ( { presignedUrls } )
126178 }
127179
@@ -132,24 +184,32 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
132184 }
133185
134186 case 'complete' : {
135- const data = await request . json ( )
136- const context : StorageContext = data . context || 'knowledge-base'
187+ const data = ( await request . json ( ) ) as CompleteSingleRequest | CompleteBatchRequest
137188
138- const config = getStorageConfig ( context )
189+ if ( 'uploads' in data && Array . isArray ( data . uploads ) ) {
190+ const verified = data . uploads . map ( ( upload ) => {
191+ const payload = verifyTokenForUser ( upload . uploadToken , userId )
192+ return payload ? { payload, parts : upload . parts } : null
193+ } )
194+
195+ if ( verified . some ( ( entry ) => entry === null ) ) {
196+ return NextResponse . json ( { error : 'Invalid or expired upload token' } , { status : 403 } )
197+ }
198+
199+ const verifiedEntries = verified . filter (
200+ ( entry ) : entry is { payload : UploadTokenPayload ; parts : unknown } => entry !== null
201+ )
139202
140- if ( 'uploads' in data ) {
141203 const results = await Promise . all (
142- data . uploads . map ( async ( upload : any ) => {
143- const { uploadId, key } = upload
204+ verifiedEntries . map ( async ( { payload, parts } ) => {
205+ const { uploadId, key, context } = payload
206+ const config = getStorageConfig ( context )
144207
145208 if ( storageProvider === 's3' ) {
146209 const { completeS3MultipartUpload } = await import (
147210 '@/lib/uploads/providers/s3/client'
148211 )
149- const parts = upload . parts // S3 format: { ETag, PartNumber }
150-
151- const result = await completeS3MultipartUpload ( key , uploadId , parts )
152-
212+ const result = await completeS3MultipartUpload ( key , uploadId , parts as any )
153213 return {
154214 success : true ,
155215 location : result . location ,
@@ -161,15 +221,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
161221 const { completeMultipartUpload } = await import (
162222 '@/lib/uploads/providers/blob/client'
163223 )
164- const parts = upload . parts // Azure format: { blockId, partNumber }
165-
166- const result = await completeMultipartUpload ( key , parts , {
224+ const result = await completeMultipartUpload ( key , parts as any , {
167225 containerName : config . containerName ! ,
168226 accountName : config . accountName ! ,
169227 accountKey : config . accountKey ,
170228 connectionString : config . connectionString ,
171229 } )
172-
173230 return {
174231 success : true ,
175232 location : result . location ,
@@ -182,19 +239,23 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
182239 } )
183240 )
184241
185- logger . info ( `Completed ${ data . uploads . length } multipart uploads (context: ${ context } ) ` )
242+ logger . info ( `Completed ${ verifiedEntries . length } multipart uploads` )
186243 return NextResponse . json ( { results } )
187244 }
188245
189- const { uploadId, key, parts } = data
246+ const single = data as CompleteSingleRequest
247+ const tokenPayload = verifyTokenForUser ( single . uploadToken , userId )
248+ if ( ! tokenPayload ) {
249+ return NextResponse . json ( { error : 'Invalid or expired upload token' } , { status : 403 } )
250+ }
251+
252+ const { uploadId, key, context } = tokenPayload
253+ const config = getStorageConfig ( context )
190254
191255 if ( storageProvider === 's3' ) {
192256 const { completeS3MultipartUpload } = await import ( '@/lib/uploads/providers/s3/client' )
193-
194- const result = await completeS3MultipartUpload ( key , uploadId , parts )
195-
257+ const result = await completeS3MultipartUpload ( key , uploadId , single . parts as any )
196258 logger . info ( `Completed S3 multipart upload for key ${ key } (context: ${ context } )` )
197-
198259 return NextResponse . json ( {
199260 success : true ,
200261 location : result . location ,
@@ -204,16 +265,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
204265 }
205266 if ( storageProvider === 'blob' ) {
206267 const { completeMultipartUpload } = await import ( '@/lib/uploads/providers/blob/client' )
207-
208- const result = await completeMultipartUpload ( key , parts , {
268+ const result = await completeMultipartUpload ( key , single . parts as any , {
209269 containerName : config . containerName ! ,
210270 accountName : config . accountName ! ,
211271 accountKey : config . accountKey ,
212272 connectionString : config . connectionString ,
213273 } )
214-
215274 logger . info ( `Completed Azure multipart upload for key ${ key } (context: ${ context } )` )
216-
217275 return NextResponse . json ( {
218276 success : true ,
219277 location : result . location ,
@@ -229,27 +287,27 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
229287 }
230288
231289 case 'abort' : {
232- const data = await request . json ( )
233- const { uploadId, key, context = 'knowledge-base' } = data
290+ const data = ( await request . json ( ) ) as TokenBoundRequest
291+ const tokenPayload = verifyTokenForUser ( data . uploadToken , userId )
292+ if ( ! tokenPayload ) {
293+ return NextResponse . json ( { error : 'Invalid or expired upload token' } , { status : 403 } )
294+ }
234295
235- const config = getStorageConfig ( context as StorageContext )
296+ const { uploadId, key, context } = tokenPayload
297+ const config = getStorageConfig ( context )
236298
237299 if ( storageProvider === 's3' ) {
238300 const { abortS3MultipartUpload } = await import ( '@/lib/uploads/providers/s3/client' )
239-
240301 await abortS3MultipartUpload ( key , uploadId )
241-
242302 logger . info ( `Aborted S3 multipart upload for key ${ key } (context: ${ context } )` )
243303 } else if ( storageProvider === 'blob' ) {
244304 const { abortMultipartUpload } = await import ( '@/lib/uploads/providers/blob/client' )
245-
246305 await abortMultipartUpload ( key , {
247306 containerName : config . containerName ! ,
248307 accountName : config . accountName ! ,
249308 accountKey : config . accountKey ,
250309 connectionString : config . connectionString ,
251310 } )
252-
253311 logger . info ( `Aborted Azure multipart upload for key ${ key } (context: ${ context } )` )
254312 } else {
255313 return NextResponse . json (
0 commit comments