sbkl
sbkl4mo ago

File serving in a nextjs app via http action authenticated with clerk

After looking at the docs, this is the implementation I came with for serving my files securely via http action. Any feedback appreciated. /convex/http.ts
import { HonoWithConvex, HttpRouterWithHono } from "convex-helpers/server/hono";
import { Hono } from "hono";

import { ActionCtx } from "./_generated/server";

const app: HonoWithConvex<ActionCtx> = new Hono();

app.get("/storage", async (ctx) => {
const identity = await ctx.env.auth.getUserIdentity();
if (!identity?.subject) {
return new Response("User is not signed in.", { status: 401 });
}
const storageId = ctx.req.query("storageId");
const fileType = ctx.req.query("fileType");

if (!storageId) {
return new Response("Storage ID is required.", { status: 400 });
}

if (!fileType) {
return new Response("File type is required.", { status: 400 });
}

switch (fileType) {
case "document":
case "documentThumbnail":
await ctx.env.runQuery(internal.documents.query.verify, {
fileType,
storageId: storageId as Id<"_storage">,
userExternalId: identity.subject.toString(),
});
break;
default:
return new Response("Invalid file type.", { status: 400 });
}

const blob = await ctx.env.storage.get(storageId as Id<"_storage">);
if (blob === null) {
return new Response("File not found", {
status: 404,
});
}
return new Response(blob);
});

export default new HttpRouterWithHono(app);
import { HonoWithConvex, HttpRouterWithHono } from "convex-helpers/server/hono";
import { Hono } from "hono";

import { ActionCtx } from "./_generated/server";

const app: HonoWithConvex<ActionCtx> = new Hono();

app.get("/storage", async (ctx) => {
const identity = await ctx.env.auth.getUserIdentity();
if (!identity?.subject) {
return new Response("User is not signed in.", { status: 401 });
}
const storageId = ctx.req.query("storageId");
const fileType = ctx.req.query("fileType");

if (!storageId) {
return new Response("Storage ID is required.", { status: 400 });
}

if (!fileType) {
return new Response("File type is required.", { status: 400 });
}

switch (fileType) {
case "document":
case "documentThumbnail":
await ctx.env.runQuery(internal.documents.query.verify, {
fileType,
storageId: storageId as Id<"_storage">,
userExternalId: identity.subject.toString(),
});
break;
default:
return new Response("Invalid file type.", { status: 400 });
}

const blob = await ctx.env.storage.get(storageId as Id<"_storage">);
if (blob === null) {
return new Response("File not found", {
status: 404,
});
}
return new Response(blob);
});

export default new HttpRouterWithHono(app);
2 Replies
Convex Bot
Convex Bot4mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
sbkl
sbklOP4mo ago
Nextjs route to pass the clerk jwt token to the fetch call to the convex http action: /api/storage/route.ts
import { NextRequest } from "next/server";
import { auth } from "@clerk/nextjs/server";

import { env } from "~/env";

export const dynamic = "force-dynamic"; // defaults to auto

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const storageId = searchParams.get("storageId");
const fileType = searchParams.get("fileType");

if (!storageId) {
return new Response("Storage ID is required.", { status: 400 });
}

if (!fileType) {
return new Response("File type is required.", { status: 400 });
}

const { userId, getToken } = auth();

if (!userId) {
return new Response("User is not signed in.", { status: 401 });
}

try {
const token = await getToken({ template: "convex" });

if (!token) {
return new Response("User is not signed in.", { status: 401 });
}

const response = await fetch(
`${env.NEXT_PUBLIC_CONVEX_API_URL}/storage?fileType=${fileType}&storageId=${storageId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
},
);

if (!response.ok) {
return new Response("Failed to fetch image", { status: 500 });
}

const blob = await response.blob();

const headers = new Headers();

console.log("blob.type", blob.type);
headers.set("Content-Type", blob.type);

return new Response(blob, {
headers,
});
} catch (error) {
return Response.json(error);
}
}
import { NextRequest } from "next/server";
import { auth } from "@clerk/nextjs/server";

import { env } from "~/env";

export const dynamic = "force-dynamic"; // defaults to auto

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const storageId = searchParams.get("storageId");
const fileType = searchParams.get("fileType");

if (!storageId) {
return new Response("Storage ID is required.", { status: 400 });
}

if (!fileType) {
return new Response("File type is required.", { status: 400 });
}

const { userId, getToken } = auth();

if (!userId) {
return new Response("User is not signed in.", { status: 401 });
}

try {
const token = await getToken({ template: "convex" });

if (!token) {
return new Response("User is not signed in.", { status: 401 });
}

const response = await fetch(
`${env.NEXT_PUBLIC_CONVEX_API_URL}/storage?fileType=${fileType}&storageId=${storageId}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
},
);

if (!response.ok) {
return new Response("Failed to fetch image", { status: 500 });
}

const blob = await response.blob();

const headers = new Headers();

console.log("blob.type", blob.type);
headers.set("Content-Type", blob.type);

return new Response(blob, {
headers,
});
} catch (error) {
return Response.json(error);
}
}
Usage with Next Image for example
<Image
src={`/api/storage?fileType=documentThumbnail&storageId=${convexDoc.storageId}`}
...other props
/>
<Image
src={`/api/storage?fileType=documentThumbnail&storageId=${convexDoc.storageId}`}
...other props
/>

Did you find this page helpful?