mirror of
https://github.com/baptisteArno/typebot.io.git
synced 2026-06-05 21:04:43 +08:00
🔧 Migrate S3 uploads from presigned POST to presigned PUT (#2429)
## Changes - **Presigned POST → PUT**: Replace `generatePresignedPostPolicy` with `generatePresignedPutUrl` across all upload endpoints (builder + viewer v1/v2/v3). This makes uploads compatible with Cloudflare R2 which doesn't support the S3 POST Object API. Frontend consumers now use `PUT` with raw file body + `Content-Type`/`Cache-Control` headers instead of `POST` with FormData. - **XSS mitigation**: Block dangerous content types (SVG, HTML, XML, JS) in the builder `generateUploadUrl` endpoint. Restrict frontend `accept` attributes from `image/*` to an explicit list of safe raster types (`png, jpeg, gif, webp, avif, bmp, tiff`). Addresses GHSA-jj87-c343-26vp. - **Fix file upload URL validation**: `isURL` with `require_tld: true` rejected `localhost` and `NEXTAUTH_URL` proxy URLs for private files. Now uses a trusted host allowlist (`localhost`, `NEXTAUTH_URL`, `S3_PUBLIC_CUSTOM_DOMAIN`) to skip TLD requirement. - **Docs**: Update S3 CORS policy from `POST` to `PUT`, add Cloudflare R2 to supported providers list. - **Bump**: `@typebot.io/js` and `@typebot.io/react` → `0.10.0` ## Verification - Tested avatar upload on builder with R2 bucket (PUT succeeds, image displays) - Verified CORS preflight passes after R2 bucket config - Confirmed `generateUploadUrl` rejects `image/svg+xml` with 400 - All unit tests pass (`nx affected -t test`) - Typecheck passes on all affected packages --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a33051755f
commit
cc9839f2e7
@ -27,14 +27,13 @@ export const UploadButton = ({
|
||||
filePathProps,
|
||||
fileType: file.type,
|
||||
});
|
||||
const formData = new FormData();
|
||||
Object.entries(data.formData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
formData.append("file", file);
|
||||
const upload = await fetch(data.presignedUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
});
|
||||
if (!upload.ok) {
|
||||
toast({
|
||||
@ -47,7 +46,11 @@ export const UploadButton = ({
|
||||
|
||||
return (
|
||||
<UploadButtonPrimitive
|
||||
accept={fileType === "image" ? "image/avif, image/*" : "audio/*"}
|
||||
accept={
|
||||
fileType === "image"
|
||||
? "image/avif, image/png, image/jpeg, image/gif, image/webp, image/bmp, image/tiff"
|
||||
: "audio/*"
|
||||
}
|
||||
variant={variant}
|
||||
size={size}
|
||||
onFileUploadRequest={handleFileUploadRequest}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ORPCError } from "@orpc/server";
|
||||
import { authenticatedProcedure } from "@typebot.io/config/orpc/builder/middlewares";
|
||||
import { env } from "@typebot.io/env";
|
||||
import { generatePresignedPostPolicy } from "@typebot.io/lib/s3/generatePresignedPostPolicy";
|
||||
import { generatePresignedPutUrl } from "@typebot.io/lib/s3/generatePresignedPutUrl";
|
||||
import prisma from "@typebot.io/prisma";
|
||||
import { z } from "zod";
|
||||
import { isWriteTypebotForbidden } from "@/features/typebot/helpers/isWriteTypebotForbidden";
|
||||
@ -38,6 +38,18 @@ const inputSchema = z.object({
|
||||
fileType: z.string().optional(),
|
||||
});
|
||||
|
||||
const dangerousContentTypes = new Set([
|
||||
"image/svg+xml",
|
||||
"text/html",
|
||||
"text/xml",
|
||||
"application/xml",
|
||||
"application/xhtml+xml",
|
||||
"application/javascript",
|
||||
"application/ecmascript",
|
||||
"text/javascript",
|
||||
"text/ecmascript",
|
||||
]);
|
||||
|
||||
export type FilePathUploadProps = z.infer<
|
||||
typeof inputSchema.shape.filePathProps
|
||||
>;
|
||||
@ -52,6 +64,13 @@ export const generateUploadUrl = authenticatedProcedure
|
||||
"S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY",
|
||||
});
|
||||
|
||||
const normalizedFileType = fileType?.toLowerCase().split(";")[0]?.trim();
|
||||
if (!normalizedFileType || dangerousContentTypes.has(normalizedFileType))
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message:
|
||||
"File type not allowed. SVG, HTML, and XML files cannot be uploaded.",
|
||||
});
|
||||
|
||||
if ("resultId" in filePathProps && !user)
|
||||
throw new ORPCError("UNAUTHORIZED", {
|
||||
message: "You must be logged in to upload a file",
|
||||
@ -62,18 +81,10 @@ export const generateUploadUrl = authenticatedProcedure
|
||||
uploadProps: filePathProps,
|
||||
});
|
||||
|
||||
const presignedPostPolicy = await generatePresignedPostPolicy({
|
||||
return generatePresignedPutUrl({
|
||||
fileType,
|
||||
filePath,
|
||||
});
|
||||
|
||||
return {
|
||||
presignedUrl: presignedPostPolicy.postURL,
|
||||
formData: presignedPostPolicy.formData,
|
||||
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -164,14 +164,13 @@ export const SpacesList = ({
|
||||
},
|
||||
fileType: file.type,
|
||||
});
|
||||
const formData = new FormData();
|
||||
Object.entries(data.formData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
formData.append("file", file);
|
||||
const upload = await fetch(data.presignedUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
});
|
||||
if (!upload.ok) {
|
||||
toast({
|
||||
|
||||
@ -7,6 +7,7 @@ In order for Typebot to store your uploaded files, you need to configure an S3 b
|
||||
You can use any S3-compatible storage provider. Here are some examples:
|
||||
|
||||
- [AWS S3](https://aws.amazon.com/s3/)
|
||||
- [Cloudflare R2](https://developers.cloudflare.com/r2/)
|
||||
- [DigitalOcean Spaces](https://www.digitalocean.com/products/spaces/)
|
||||
- [Wasabi](https://wasabi.com/)
|
||||
- [MinIO](https://min.io/)
|
||||
@ -26,7 +27,7 @@ To function properly, your S3 bucket must have the following configuration:
|
||||
"CORSRules": [
|
||||
{
|
||||
"AllowedHeaders": ["*"],
|
||||
"AllowedMethods": ["PUT", "POST"],
|
||||
"AllowedMethods": ["PUT"],
|
||||
"AllowedOrigins": ["*"],
|
||||
"ExposeHeaders": ["ETag"]
|
||||
}
|
||||
@ -34,18 +35,18 @@ To function properly, your S3 bucket must have the following configuration:
|
||||
}
|
||||
```
|
||||
|
||||
If you are using the amazon console online you should paste it like this :
|
||||
If you are using the amazon console online you should paste it like this :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"AllowedHeaders": ["*"],
|
||||
"AllowedMethods": ["PUT", "POST"],
|
||||
"AllowedOrigins": ["*"],
|
||||
"ExposeHeaders": ["ETag"]
|
||||
}
|
||||
]
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"AllowedHeaders": ["*"],
|
||||
"AllowedMethods": ["PUT"],
|
||||
"AllowedOrigins": ["*"],
|
||||
"ExposeHeaders": ["ETag"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- Access policy (replace `<bucket-name>` with the name of your S3 bucket):
|
||||
```json
|
||||
@ -78,7 +79,7 @@ Some S3 providers like AWS provide a user interface that allows you to directly
|
||||
"CORSRules": [
|
||||
{
|
||||
"AllowedHeaders": ["*"],
|
||||
"AllowedMethods": ["PUT", "POST"],
|
||||
"AllowedMethods": ["PUT"],
|
||||
"AllowedOrigins": ["*"],
|
||||
"ExposeHeaders": ["ETag"]
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { getSession } from "@typebot.io/chat-session/queries/getSession";
|
||||
import { env } from "@typebot.io/env";
|
||||
import { getBlockById } from "@typebot.io/groups/helpers/getBlockById";
|
||||
import { parseGroups } from "@typebot.io/groups/helpers/parseGroups";
|
||||
import { generatePresignedPostPolicy } from "@typebot.io/lib/s3/generatePresignedPostPolicy";
|
||||
import { generatePresignedPutUrl } from "@typebot.io/lib/s3/generatePresignedPutUrl";
|
||||
import prisma from "@typebot.io/prisma";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -23,10 +23,11 @@ export const generateUploadUrlV1InputSchema = z.object({
|
||||
}),
|
||||
),
|
||||
fileType: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
});
|
||||
|
||||
export const handleGenerateUploadUrlV1 = async ({
|
||||
input: { filePathProps, fileType },
|
||||
input: { filePathProps, fileType, fileSize },
|
||||
}: {
|
||||
input: z.infer<typeof generateUploadUrlV1InputSchema>;
|
||||
}) => {
|
||||
@ -72,21 +73,20 @@ export const handleGenerateUploadUrlV1 = async ({
|
||||
message: "Can't find file upload block",
|
||||
});
|
||||
|
||||
const presignedPostPolicy = await generatePresignedPostPolicy({
|
||||
const maxFileSize =
|
||||
fileUploadBlock.options?.sizeLimit ??
|
||||
env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE;
|
||||
|
||||
if (maxFileSize && fileSize && fileSize > maxFileSize * 1024 * 1024)
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: `File size exceeds the ${maxFileSize}MB limit`,
|
||||
});
|
||||
|
||||
return generatePresignedPutUrl({
|
||||
fileType,
|
||||
filePath,
|
||||
maxFileSize:
|
||||
fileUploadBlock.options?.sizeLimit ??
|
||||
env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
||||
maxFileSize,
|
||||
});
|
||||
|
||||
return {
|
||||
presignedUrl: presignedPostPolicy.postURL,
|
||||
formData: presignedPostPolicy.formData,
|
||||
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
||||
};
|
||||
}
|
||||
|
||||
const session = await getSession(filePathProps.sessionId);
|
||||
@ -145,23 +145,29 @@ export const handleGenerateUploadUrlV1 = async ({
|
||||
filePathProps.fileName
|
||||
}`;
|
||||
|
||||
const presignedPostPolicy = await generatePresignedPostPolicy({
|
||||
const maxFileSize =
|
||||
fileUploadBlock.options && "sizeLimit" in fileUploadBlock.options
|
||||
? (fileUploadBlock.options.sizeLimit as number)
|
||||
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE;
|
||||
|
||||
if (maxFileSize && fileSize && fileSize > maxFileSize * 1024 * 1024)
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: `File size exceeds the ${maxFileSize}MB limit`,
|
||||
});
|
||||
|
||||
const { presignedUrl, fileUrl: defaultFileUrl, fileType: resolvedFileType, maxFileSize: maxFileSizeMB } = await generatePresignedPutUrl({
|
||||
fileType,
|
||||
filePath,
|
||||
maxFileSize:
|
||||
fileUploadBlock.options && "sizeLimit" in fileUploadBlock.options
|
||||
? (fileUploadBlock.options.sizeLimit as number)
|
||||
: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
|
||||
maxFileSize,
|
||||
});
|
||||
|
||||
return {
|
||||
presignedUrl: presignedPostPolicy.postURL,
|
||||
formData: presignedPostPolicy.formData,
|
||||
presignedUrl,
|
||||
fileType: resolvedFileType,
|
||||
maxFileSize: maxFileSizeMB,
|
||||
fileUrl:
|
||||
fileUploadBlock.options?.visibility === "Private"
|
||||
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${filePathProps.fileName}`
|
||||
: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
||||
: defaultFileUrl,
|
||||
};
|
||||
};
|
||||
|
||||
@ -6,7 +6,7 @@ import { getSession } from "@typebot.io/chat-session/queries/getSession";
|
||||
import { env } from "@typebot.io/env";
|
||||
import { getBlockById } from "@typebot.io/groups/helpers/getBlockById";
|
||||
import { parseGroups } from "@typebot.io/groups/helpers/parseGroups";
|
||||
import { generatePresignedPostPolicy } from "@typebot.io/lib/s3/generatePresignedPostPolicy";
|
||||
import { generatePresignedPutUrl } from "@typebot.io/lib/s3/generatePresignedPutUrl";
|
||||
import prisma from "@typebot.io/prisma";
|
||||
import type { Prisma } from "@typebot.io/prisma/types";
|
||||
import { z } from "zod";
|
||||
@ -15,10 +15,11 @@ export const generateUploadUrlV2InputSchema = z.object({
|
||||
sessionId: z.string(),
|
||||
fileName: z.string(),
|
||||
fileType: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
});
|
||||
|
||||
export const handleGenerateUploadUrlV2 = async ({
|
||||
input: { fileName, sessionId, fileType },
|
||||
input: { fileName, sessionId, fileType, fileSize },
|
||||
}: {
|
||||
input: z.infer<typeof generateUploadUrlV2InputSchema>;
|
||||
}) => {
|
||||
@ -72,6 +73,11 @@ export const handleGenerateUploadUrlV2 = async ({
|
||||
|
||||
const { visibility, maxFileSize } = parseFileUploadParams(block);
|
||||
|
||||
if (maxFileSize && fileSize && fileSize > maxFileSize * 1024 * 1024)
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: `File size exceeds the ${maxFileSize}MB limit`,
|
||||
});
|
||||
|
||||
const resultId = session.state.typebotsQueue[0].resultId;
|
||||
|
||||
const filePath =
|
||||
@ -81,21 +87,20 @@ export const handleGenerateUploadUrlV2 = async ({
|
||||
}/typebots/${typebotId}/results/${resultId}/${fileName}`
|
||||
: `public/tmp/${typebotId}/${fileName}`;
|
||||
|
||||
const presignedPostPolicy = await generatePresignedPostPolicy({
|
||||
const { presignedUrl, fileUrl: defaultFileUrl, fileType: resolvedFileType, maxFileSize: maxFileSizeMB } = await generatePresignedPutUrl({
|
||||
fileType,
|
||||
filePath,
|
||||
maxFileSize,
|
||||
});
|
||||
|
||||
return {
|
||||
presignedUrl: presignedPostPolicy.postURL,
|
||||
formData: presignedPostPolicy.formData,
|
||||
presignedUrl,
|
||||
fileType: resolvedFileType,
|
||||
maxFileSize: maxFileSizeMB,
|
||||
fileUrl:
|
||||
visibility === "Private" && !isPreview
|
||||
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/${fileName}`
|
||||
: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
||||
: defaultFileUrl,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import { env } from "@typebot.io/env";
|
||||
import { getBlockById } from "@typebot.io/groups/helpers/getBlockById";
|
||||
import { parseGroups } from "@typebot.io/groups/helpers/parseGroups";
|
||||
import { parseAllowedFileTypesMetadata } from "@typebot.io/lib/extensionFromMimeType";
|
||||
import { generatePresignedPostPolicy } from "@typebot.io/lib/s3/generatePresignedPostPolicy";
|
||||
import { generatePresignedPutUrl } from "@typebot.io/lib/s3/generatePresignedPutUrl";
|
||||
import prisma from "@typebot.io/prisma";
|
||||
import type { Prisma } from "@typebot.io/prisma/types";
|
||||
import { z } from "zod";
|
||||
@ -17,10 +17,11 @@ export const generateUploadUrlInputSchema = z.object({
|
||||
blockId: z.string(),
|
||||
fileName: z.string(),
|
||||
fileType: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
});
|
||||
|
||||
export const handleGenerateUploadUrl = async ({
|
||||
input: { fileName, sessionId, fileType, blockId },
|
||||
input: { fileName, sessionId, fileType, blockId, fileSize },
|
||||
}: {
|
||||
input: z.infer<typeof generateUploadUrlInputSchema>;
|
||||
}) => {
|
||||
@ -94,6 +95,11 @@ export const handleGenerateUploadUrl = async ({
|
||||
|
||||
const { visibility, maxFileSize } = parseFileUploadParams(block);
|
||||
|
||||
if (maxFileSize && fileSize && fileSize > maxFileSize * 1024 * 1024)
|
||||
throw new ORPCError("BAD_REQUEST", {
|
||||
message: `File size exceeds the ${maxFileSize}MB limit`,
|
||||
});
|
||||
|
||||
const resultId = session.state.typebotsQueue[0].resultId;
|
||||
|
||||
const filePath =
|
||||
@ -103,21 +109,20 @@ export const handleGenerateUploadUrl = async ({
|
||||
}/typebots/${typebotId}/results/${resultId}/blocks/${blockId}/${fileName}`
|
||||
: `public/tmp/typebots/${typebotId}/blocks/${blockId}/${fileName}`;
|
||||
|
||||
const presignedPostPolicy = await generatePresignedPostPolicy({
|
||||
const { presignedUrl, fileUrl: defaultFileUrl, fileType: resolvedFileType, maxFileSize: maxFileSizeMB } = await generatePresignedPutUrl({
|
||||
fileType,
|
||||
filePath,
|
||||
maxFileSize,
|
||||
});
|
||||
|
||||
return {
|
||||
presignedUrl: presignedPostPolicy.postURL,
|
||||
formData: presignedPostPolicy.formData,
|
||||
presignedUrl,
|
||||
fileType: resolvedFileType,
|
||||
maxFileSize: maxFileSizeMB,
|
||||
fileUrl:
|
||||
visibility === "Private" && !isPreview
|
||||
? `${env.NEXTAUTH_URL}/api/typebots/${typebotId}/results/${resultId}/blocks/${blockId}/${fileName}`
|
||||
: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||
: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
|
||||
: defaultFileUrl,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -54,7 +54,8 @@ export const fileUploadBuilderRouter = {
|
||||
.output(
|
||||
z.object({
|
||||
presignedUrl: z.string(),
|
||||
formData: z.record(z.string(), z.any()),
|
||||
fileType: z.string().optional(),
|
||||
maxFileSize: z.number().optional(),
|
||||
fileUrl: z.string(),
|
||||
}),
|
||||
)
|
||||
@ -74,7 +75,8 @@ export const fileUploadViewerRouter = {
|
||||
.output(
|
||||
z.object({
|
||||
presignedUrl: z.string(),
|
||||
formData: z.record(z.string(), z.any()),
|
||||
fileType: z.string().optional(),
|
||||
maxFileSize: z.number().optional(),
|
||||
fileUrl: z.string(),
|
||||
}),
|
||||
)
|
||||
@ -92,7 +94,8 @@ export const fileUploadViewerRouter = {
|
||||
.output(
|
||||
z.object({
|
||||
presignedUrl: z.string(),
|
||||
formData: z.record(z.string(), z.any()),
|
||||
fileType: z.string().optional(),
|
||||
maxFileSize: z.number().optional(),
|
||||
fileUrl: z.string(),
|
||||
}),
|
||||
)
|
||||
@ -110,7 +113,8 @@ export const fileUploadViewerRouter = {
|
||||
.output(
|
||||
z.object({
|
||||
presignedUrl: z.string(),
|
||||
formData: z.record(z.string(), z.any()),
|
||||
fileType: z.string().optional(),
|
||||
maxFileSize: z.number().optional(),
|
||||
fileUrl: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
@ -101,8 +101,25 @@ export const validateAndParseInputMessage = (
|
||||
|
||||
const replyValue = message.type === "audio" ? message.url : message.text;
|
||||
const urls = replyValue.split(", ");
|
||||
const hasValidUrls = urls.some((url) =>
|
||||
isURL(url, { require_tld: env.S3_ENDPOINT !== "localhost" }),
|
||||
const isTrustedHost = (url: string) => {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
if (hostname === "localhost") return true;
|
||||
if (
|
||||
env.S3_PUBLIC_CUSTOM_DOMAIN &&
|
||||
new URL(env.S3_PUBLIC_CUSTOM_DOMAIN).hostname === hostname
|
||||
)
|
||||
return true;
|
||||
if (env.NEXTAUTH_URL && new URL(env.NEXTAUTH_URL).hostname === hostname)
|
||||
return true;
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const hasValidUrls = urls.some(
|
||||
(url) =>
|
||||
isURL(url, { require_tld: !isTrustedHost(url) }),
|
||||
);
|
||||
|
||||
const allowedFileTypesMetadata =
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/js",
|
||||
"version": "0.9.24",
|
||||
"version": "0.10.0",
|
||||
"description": "Javascript library to display typebots on your website",
|
||||
"license": "FSL-1.1-ALv2",
|
||||
"type": "module",
|
||||
|
||||
@ -30,7 +30,7 @@ export const TextInputAddFileButton = (props: Props) => {
|
||||
<input
|
||||
type="file"
|
||||
id={photosUploadId}
|
||||
accept="image/avif, image/*, video/*, capture=camera"
|
||||
accept="image/avif, image/png, image/jpeg, image/gif, image/webp, image/bmp, image/tiff, video/*, capture=camera"
|
||||
multiple
|
||||
class="hidden"
|
||||
onChange={(e) => {
|
||||
|
||||
@ -33,8 +33,9 @@ export const uploadFiles = async ({
|
||||
i += 1;
|
||||
const { data, error } = await sendRequest<{
|
||||
presignedUrl: string;
|
||||
formData: Record<string, string>;
|
||||
fileUrl: string;
|
||||
fileType?: string;
|
||||
maxFileSize?: number;
|
||||
}>({
|
||||
method: "POST",
|
||||
url: `${apiHost}/api/v3/generate-upload-url`,
|
||||
@ -42,6 +43,7 @@ export const uploadFiles = async ({
|
||||
fileName: input.fileName,
|
||||
sessionId: input.sessionId,
|
||||
fileType: file.type,
|
||||
fileSize: file.size,
|
||||
blockId: input.blockId,
|
||||
},
|
||||
});
|
||||
@ -53,14 +55,13 @@ export const uploadFiles = async ({
|
||||
|
||||
if (!data?.presignedUrl) continue;
|
||||
|
||||
const formData = new FormData();
|
||||
Object.entries(data.formData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
formData.append("file", file);
|
||||
const upload = await fetch(data.presignedUrl, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
});
|
||||
|
||||
if (!upload.ok) continue;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/react",
|
||||
"version": "0.9.24",
|
||||
"version": "0.10.0",
|
||||
"description": "Convenient library to display typebots on your React app",
|
||||
"license": "FSL-1.1-ALv2",
|
||||
"type": "module",
|
||||
|
||||
33
packages/lib/src/s3/generatePresignedPutUrl.ts
Normal file
33
packages/lib/src/s3/generatePresignedPutUrl.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { env } from "@typebot.io/env";
|
||||
import { initClient } from "./initClient";
|
||||
|
||||
type Props = {
|
||||
filePath: string;
|
||||
fileType?: string;
|
||||
maxFileSize?: number;
|
||||
};
|
||||
|
||||
const tenMinutes = 10 * 60;
|
||||
|
||||
export const generatePresignedPutUrl = async ({
|
||||
filePath,
|
||||
fileType,
|
||||
maxFileSize,
|
||||
}: Props) => {
|
||||
const minioClient = initClient();
|
||||
|
||||
const presignedUrl = await minioClient.presignedPutObject(
|
||||
env.S3_BUCKET,
|
||||
filePath,
|
||||
tenMinutes,
|
||||
);
|
||||
|
||||
return {
|
||||
presignedUrl,
|
||||
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
|
||||
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
|
||||
: `${presignedUrl.split("?")[0]}`,
|
||||
fileType,
|
||||
maxFileSize,
|
||||
};
|
||||
};
|
||||
@ -154,7 +154,7 @@ const SpaceIconPopover = ({
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="upload" className="flex justify-center py-4">
|
||||
<UploadButton
|
||||
accept="image/avif, image/*"
|
||||
accept="image/avif, image/png, image/jpeg, image/gif, image/webp, image/bmp, image/tiff"
|
||||
onFileUploadRequest={onFileUploadRequest}
|
||||
onValueCommit={(icon) => { onValueCommit(icon); popoverControls.onClose(); }}
|
||||
/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user