@@ -5,7 +5,7 @@ import { fileViewContract } from '@/lib/api/contracts/storage-transfer'
55import { parseRequest } from '@/lib/api/server'
66import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
77import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8- import { USE_BLOB_STORAGE } from '@/lib/uploads/config'
8+ import { type StorageContext , USE_BLOB_STORAGE } from '@/lib/uploads/config'
99import { getFileMetadataById } from '@/lib/uploads/server/metadata'
1010import { verifyFileAccess } from '@/app/api/files/authorization'
1111
@@ -29,16 +29,44 @@ export const GET = withRouteHandler(
2929 return NextResponse . json ( { error : 'Not found' } , { status : 404 } )
3030 }
3131
32- const hasAccess = await verifyFileAccess ( record . key , authResult . userId )
32+ // Authorize before disclosing anything about the file. Pass the record's own context so access is
33+ // resolved from the DB row (workspace and mothership both map to workspace membership) rather than
34+ // re-inferred from the key, and deny with 404 so a caller without access cannot distinguish a
35+ // file's existence or context from a missing id.
36+ const hasAccess = await verifyFileAccess (
37+ record . key ,
38+ authResult . userId ,
39+ undefined ,
40+ record . context as StorageContext | 'general'
41+ )
3342 if ( ! hasAccess ) {
3443 logger . warn ( 'Unauthorized file view attempt' , { id, userId : authResult . userId } )
35- return NextResponse . json ( { error : 'Forbidden' } , { status : 403 } )
44+ return NextResponse . json ( { error : 'Not found' } , { status : 404 } )
45+ }
46+
47+ // Only workspace-scoped files are embeddable/viewable here. Other contexts (e.g. chat-scoped
48+ // `mothership` uploads) are not durable workspace artifacts; now that the caller is known to have
49+ // access, reject with a legible 422 so the embed fails cleanly and the file agent can self-correct.
50+ if ( record . context !== 'workspace' ) {
51+ logger . warn ( 'Rejected non-workspace file view' , { id, context : record . context } )
52+ return NextResponse . json (
53+ {
54+ error : `File ${ id } has context "${ record . context } " and is not embeddable. Only workspace files can be viewed via /api/files/view. Save it into the workspace and reference the workspace copy.` ,
55+ } ,
56+ { status : 422 }
57+ )
3658 }
3759
3860 const storagePrefix = USE_BLOB_STORAGE ? 'blob' : 's3'
3961 const servePath = `/api/files/serve/${ storagePrefix } /${ encodeURIComponent ( record . key ) } `
4062 logger . info ( 'Redirecting file view to serve path' , { id, servePath } )
4163
42- return NextResponse . redirect ( new URL ( servePath , request . url ) , { status : 302 } )
64+ // Emit a relative Location so the browser resolves it against the public origin it requested.
65+ // `NextResponse.redirect(new URL(servePath, request.url))` bakes in the host from `request.url`,
66+ // which behind the load balancer is the internal pod host (e.g. ip-10-0-x.ec2.internal:3000) —
67+ // unreachable from the browser, so embedded <img src="/api/files/view/<id>"> loads fail. A
68+ // relative Location resolves against the original public URL (matching how the Files tab fetches
69+ // serve URLs directly).
70+ return new NextResponse ( null , { status : 302 , headers : { Location : servePath } } )
4371 }
4472)
0 commit comments