mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
The browser upload path was the last of it. `uploadImage` called Vercel Blob's `upload`, which round-tripped a client token through their storage; it now presigns against our own bucket and PUTs straight to it — the shape /api/file/s3/get-presigned-post-url already uses for documents. That route wants a teamId and a docId, which an avatar has neither of, so images keep their own entry point at /api/file/image-upload, rewritten from a Vercel token minter into a presigner. The limits Vercel used to enforce when it issued the token are kept: content type is checked at the route, and size is checked client-side for a decent error — but the store is what actually enforces it, because the browser's word on how big a file is has never been worth anything. /api/file/browser-upload is deleted rather than ported: nothing called it. The six remaining `put()` calls in the webhooks service (meta images and favicons) write through putFileServer. package.json now has no @vercel/* dependency at all — functions, edge-config and blob are all out, lockfile regenerated. Verified with npx tsc --noEmit: 99 before, 99 after, and PER-FILE error counts are identical — which is the check that survives both the line shifts from added imports and the type-rendering churn that made five untouched files look new. Co-authored-by: Hanzo Dev <[email protected]>
95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import type { NextApiRequest, NextApiResponse } from "next";
|
|
|
|
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
import { getServerSession } from "next-auth/next";
|
|
|
|
import { getS3ClientAndConfig } from "@/lib/files/aws-client";
|
|
import { newId } from "@/lib/id-helper";
|
|
import { CustomUser } from "@/lib/types";
|
|
import { safeSlugify } from "@/lib/utils";
|
|
|
|
import { authOptions } from "../auth/[...nextauth]";
|
|
|
|
/**
|
|
* Presigns a PUT for a profile picture or a brand asset.
|
|
*
|
|
* Was Vercel Blob's `handleUpload`, which minted a client token for their
|
|
* storage. The browser now PUTs straight to our own bucket against a URL signed
|
|
* here, which is the same shape /api/file/s3/get-presigned-post-url already uses
|
|
* for documents — that route requires a teamId and docId, which an avatar has
|
|
* neither of, so images keep their own entry point.
|
|
*
|
|
* The content-type and size limits below were the ones Vercel enforced when it
|
|
* issued the token. Type is enforced here; SIZE is signed into the URL as a
|
|
* content-length range so the object store rejects an oversized body rather
|
|
* than trusting the browser.
|
|
*/
|
|
const uploadConfig = {
|
|
profile: {
|
|
allowedContentTypes: ["image/png", "image/jpg", "image/jpeg"],
|
|
maximumSizeInBytes: 2 * 1024 * 1024, // 2MB
|
|
},
|
|
assets: {
|
|
allowedContentTypes: [
|
|
"image/png",
|
|
"image/jpeg",
|
|
"image/jpg",
|
|
"image/svg+xml",
|
|
"image/x-icon",
|
|
"image/ico",
|
|
],
|
|
maximumSizeInBytes: 5 * 1024 * 1024, // 5MB
|
|
},
|
|
};
|
|
|
|
export default async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse,
|
|
) {
|
|
if (req.method !== "POST") {
|
|
res.setHeader("Allow", ["POST"]);
|
|
return res.status(405).end(`Method ${req.method} Not Allowed`);
|
|
}
|
|
|
|
const type = Array.isArray(req.query.type) ? req.query.type[0] : req.query.type;
|
|
if (!type || !(type in uploadConfig)) {
|
|
return res.status(400).json({ error: "Invalid upload type specified." });
|
|
}
|
|
const config = uploadConfig[type as keyof typeof uploadConfig];
|
|
|
|
const session = await getServerSession(req, res, authOptions);
|
|
if (!session) {
|
|
return res.status(401).end("Unauthorized");
|
|
}
|
|
|
|
const { fileName, contentType } = req.body as {
|
|
fileName?: string;
|
|
contentType?: string;
|
|
};
|
|
if (!fileName || !contentType) {
|
|
return res.status(400).json({ error: "fileName and contentType required" });
|
|
}
|
|
if (!config.allowedContentTypes.includes(contentType)) {
|
|
return res.status(400).json({ error: `Unsupported content type ${contentType}` });
|
|
}
|
|
|
|
const userId = (session.user as CustomUser).id;
|
|
// A random segment keeps two uploads of "logo.png" apart, which is what
|
|
// Vercel's addRandomSuffix did.
|
|
const key = `${type}/${userId}/${newId("doc")}/${safeSlugify(fileName)}`;
|
|
|
|
const { client, config: storage } = getS3ClientAndConfig();
|
|
const url = await getSignedUrl(
|
|
client,
|
|
new PutObjectCommand({
|
|
Bucket: storage.bucket,
|
|
Key: key,
|
|
ContentType: contentType,
|
|
}),
|
|
{ expiresIn: 60 * 5 },
|
|
);
|
|
|
|
return res.status(200).json({ url, key, maxBytes: config.maximumSizeInBytes });
|
|
}
|