Skip to content

Commit 4e99a4e

Browse files
committed
fix(blob): switch to private access with delivery route
User-uploaded images should not be publicly accessible. This switches from public to private blob storage and adds a delivery route that proxies files through the server. Changes: - Upload route: access 'public' -> 'private', addRandomSuffix, returns delivery route URL instead of raw blob URL - New /api/files/serve route: proxies private blobs with ETag caching, Content-Disposition: inline for browser display - next.config.ts: localPatterns for next/image, removed public blob domain - proxy.ts: skip middleware auth for serve route (image optimizer can't forward cookies, pathname unguessability provides security) - vercel-template.json: force private access in 1-click deploy - Upgraded @vercel/blob to 2.3.3 for get() support
1 parent 146b3cb commit 4e99a4e

File tree

7 files changed

+74
-33
lines changed

7 files changed

+74
-33
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { get } from "@vercel/blob";
2+
import { type NextRequest, NextResponse } from "next/server";
3+
4+
export async function GET(request: NextRequest) {
5+
const pathname = request.nextUrl.searchParams.get("pathname");
6+
7+
if (!pathname) {
8+
return NextResponse.json({ error: "Missing pathname" }, { status: 400 });
9+
}
10+
11+
const result = await get(pathname, {
12+
access: "private",
13+
ifNoneMatch: request.headers.get("if-none-match") ?? undefined,
14+
});
15+
16+
if (!result) {
17+
return new NextResponse("Not found", { status: 404 });
18+
}
19+
20+
if (result.statusCode === 304) {
21+
return new NextResponse(null, {
22+
status: 304,
23+
headers: {
24+
ETag: result.blob.etag,
25+
"Cache-Control": "private, no-cache",
26+
},
27+
});
28+
}
29+
30+
return new NextResponse(result.stream, {
31+
headers: {
32+
"Content-Type": result.blob.contentType,
33+
"Content-Disposition": "inline",
34+
"X-Content-Type-Options": "nosniff",
35+
ETag: result.blob.etag,
36+
"Cache-Control": "private, no-cache",
37+
},
38+
});
39+
}

app/(chat)/api/files/upload/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@ export async function POST(request: Request) {
5050

5151
try {
5252
const data = await put(`${safeName}`, fileBuffer, {
53-
access: "public",
53+
access: "private",
54+
addRandomSuffix: true,
5455
});
5556

56-
return NextResponse.json(data);
57+
return NextResponse.json({
58+
url: `/api/files/serve?pathname=${encodeURIComponent(data.pathname)}`,
59+
pathname: data.pathname,
60+
contentType: data.contentType,
61+
});
5762
} catch (_error) {
5863
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
5964
}

next.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ const nextConfig: NextConfig = {
3636
{
3737
hostname: "avatar.vercel.sh",
3838
},
39+
],
40+
localPatterns: [
3941
{
40-
protocol: "https",
41-
hostname: "*.public.blob.vercel-storage.com",
42+
pathname: "/api/files/serve",
4243
},
4344
],
4445
},

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@streamdown/math": "^1.0.2",
3333
"@streamdown/mermaid": "^1.0.2",
3434
"@vercel/analytics": "^1.3.1",
35-
"@vercel/blob": "^0.24.1",
35+
"@vercel/blob": "^2.3.3",
3636
"@vercel/functions": "^2.0.0",
3737
"@vercel/otel": "^1.12.0",
3838
"ai": "6.0.116",

pnpm-lock.yaml

Lines changed: 18 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proxy.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export async function proxy(request: NextRequest) {
99
return new Response("pong", { status: 200 });
1010
}
1111

12-
if (pathname.startsWith("/api/auth")) {
12+
if (
13+
pathname.startsWith("/api/auth") ||
14+
pathname.startsWith("/api/files/serve")
15+
) {
1316
return NextResponse.next();
1417
}
1518

vercel-template.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"integrationSlug": "upstash"
1414
},
1515
{
16-
"type": "blob"
16+
"type": "blob",
17+
"access": "private"
1718
}
1819
]
1920
}

0 commit comments

Comments
 (0)