mirror of
https://github.com/wasp-lang/open-saas.git
synced 2025-04-01 00:18:19 +02:00
Merge pull request #385 from wasp-lang/miho-s3-validation
This commit is contained in:
commit
d088cff84b
@ -1,6 +1,6 @@
|
||||
--- template/app/package.json
|
||||
+++ opensaas-sh/app/package.json
|
||||
@@ -1,13 +1,17 @@
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"name": "opensaas",
|
||||
"type": "module",
|
||||
@ -11,7 +11,8 @@
|
||||
+ },
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.523.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.523.0",
|
||||
"@aws-sdk/s3-presigned-post": "^3.750.0",
|
||||
@@ -8,7 +13,6 @@
|
||||
"@faker-js/faker": "8.3.1",
|
||||
"@google-analytics/data": "4.1.0",
|
||||
"@headlessui/react": "1.7.13",
|
||||
|
@ -1,8 +1,8 @@
|
||||
--- template/app/src/file-upload/operations.ts
|
||||
+++ opensaas-sh/app/src/file-upload/operations.ts
|
||||
@@ -25,6 +25,18 @@
|
||||
|
||||
const { fileType, fileName } = ensureArgsSchemaOrThrowHttpError(createFileInputSchema, rawArgs);
|
||||
@@ -37,6 +37,18 @@
|
||||
userId: context.user.id,
|
||||
});
|
||||
|
||||
+ const numberOfFilesByUser = await context.entities.File.count({
|
||||
+ where: {
|
||||
@ -16,6 +16,6 @@
|
||||
+ throw new HttpError(403, 'Thanks for trying Open SaaS. This demo only allows 2 file uploads per user.');
|
||||
+ }
|
||||
+
|
||||
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({
|
||||
fileType,
|
||||
fileName,
|
||||
await context.entities.File.create({
|
||||
data: {
|
||||
name: fileName,
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 81 KiB |
@ -11,7 +11,6 @@ import defaultSettings from '@assets/file-uploads/default-settings.png';
|
||||
import newBucket from '@assets/file-uploads/new-bucket.png';
|
||||
import permissions from '@assets/file-uploads/permissions.png';
|
||||
import cors from '@assets/file-uploads/cors.png';
|
||||
import corsExample from '@assets/file-uploads/cors-example.png';
|
||||
import username from '@assets/file-uploads/username.png';
|
||||
import keys from '@assets/file-uploads/keys.png';
|
||||
|
||||
@ -94,7 +93,7 @@ Now we need to change some permissions on the bucket to allow for file uploads f
|
||||
"*"
|
||||
],
|
||||
"AllowedMethods": [
|
||||
"PUT",
|
||||
"POST",
|
||||
"GET"
|
||||
],
|
||||
"AllowedOrigins": [
|
||||
@ -105,8 +104,6 @@ Now we need to change some permissions on the bucket to allow for file uploads f
|
||||
}
|
||||
]
|
||||
```
|
||||
As an example, here are the CORS permissions for this site - https://opensaas.sh:
|
||||
<Image src={corsExample} alt="cors-example" loading="lazy" />
|
||||
|
||||
### Get your AWS S3 credentials
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.523.0",
|
||||
"@aws-sdk/s3-presigned-post": "^3.750.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.523.0",
|
||||
"@faker-js/faker": "8.3.1",
|
||||
"@google-analytics/data": "4.1.0",
|
||||
@ -18,17 +19,17 @@
|
||||
"prettier": "3.1.1",
|
||||
"prettier-plugin-tailwindcss": "0.5.11",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-apexcharts": "1.4.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "4.11.0",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"stripe": "11.15.0",
|
||||
"tailwind-merge": "^2.2.1",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"vanilla-cookieconsent": "^3.0.1",
|
||||
"wasp": "file:.wasp/out/sdk/wasp",
|
||||
"zod": "^3.23.8",
|
||||
"tailwindcss": "^3.2.7"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.13",
|
||||
|
@ -1,21 +1,20 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { createFile } from 'wasp/client/operations';
|
||||
import axios from 'axios';
|
||||
import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE } from './validation';
|
||||
import { ALLOWED_FILE_TYPES, MAX_FILE_SIZE_BYTES } from './validation';
|
||||
|
||||
export type FileWithValidType = Omit<File, 'type'> & { type: AllowedFileType };
|
||||
type AllowedFileType = (typeof ALLOWED_FILE_TYPES)[number];
|
||||
interface FileUploadProgress {
|
||||
file: FileWithValidType;
|
||||
setUploadProgressPercent: Dispatch<SetStateAction<number>>;
|
||||
setUploadProgressPercent: (percentage: number) => void;
|
||||
}
|
||||
|
||||
export async function uploadFileWithProgress({ file, setUploadProgressPercent }: FileUploadProgress) {
|
||||
const { uploadUrl } = await createFile({ fileType: file.type, fileName: file.name });
|
||||
return axios.put(uploadUrl, file, {
|
||||
headers: {
|
||||
'Content-Type': file.type,
|
||||
},
|
||||
const { s3UploadUrl, s3UploadFields } = await createFile({ fileType: file.type, fileName: file.name });
|
||||
|
||||
const formData = getFileUploadFormData(file, s3UploadFields);
|
||||
|
||||
return axios.post(s3UploadUrl, formData, {
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const percentage = Math.round((progressEvent.loaded / progressEvent.total) * 100);
|
||||
@ -25,15 +24,24 @@ export async function uploadFileWithProgress({ file, setUploadProgressPercent }:
|
||||
});
|
||||
}
|
||||
|
||||
function getFileUploadFormData(file: File, s3UploadFields: Record<string, string>) {
|
||||
const formData = new FormData();
|
||||
Object.entries(s3UploadFields).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
formData.append('file', file);
|
||||
return formData;
|
||||
}
|
||||
|
||||
export interface FileUploadError {
|
||||
message: string;
|
||||
code: 'NO_FILE' | 'INVALID_FILE_TYPE' | 'FILE_TOO_LARGE' | 'UPLOAD_FAILED';
|
||||
}
|
||||
|
||||
export function validateFile(file: File) {
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
if (file.size > MAX_FILE_SIZE_BYTES) {
|
||||
return {
|
||||
message: `File size exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`,
|
||||
message: `File size exceeds ${MAX_FILE_SIZE_BYTES / 1024 / 1024}MB limit.`,
|
||||
code: 'FILE_TOO_LARGE' as const,
|
||||
};
|
||||
}
|
||||
|
@ -18,28 +18,39 @@ const createFileInputSchema = z.object({
|
||||
|
||||
type CreateFileInput = z.infer<typeof createFileInputSchema>;
|
||||
|
||||
export const createFile: CreateFile<CreateFileInput, File> = async (rawArgs, context) => {
|
||||
export const createFile: CreateFile<
|
||||
CreateFileInput,
|
||||
{
|
||||
s3UploadUrl: string;
|
||||
s3UploadFields: Record<string, string>;
|
||||
}
|
||||
> = async (rawArgs, context) => {
|
||||
if (!context.user) {
|
||||
throw new HttpError(401);
|
||||
}
|
||||
|
||||
const { fileType, fileName } = ensureArgsSchemaOrThrowHttpError(createFileInputSchema, rawArgs);
|
||||
|
||||
const { uploadUrl, key } = await getUploadFileSignedURLFromS3({
|
||||
const { s3UploadUrl, s3UploadFields, key } = await getUploadFileSignedURLFromS3({
|
||||
fileType,
|
||||
fileName,
|
||||
userId: context.user.id,
|
||||
});
|
||||
|
||||
return await context.entities.File.create({
|
||||
await context.entities.File.create({
|
||||
data: {
|
||||
name: fileName,
|
||||
key,
|
||||
uploadUrl,
|
||||
uploadUrl: s3UploadUrl,
|
||||
type: fileType,
|
||||
user: { connect: { id: context.user.id } },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
s3UploadUrl,
|
||||
s3UploadFields,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAllFilesByUser: GetAllFilesByUser<void, File[]> = async (_args, context) => {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import * as path from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { S3Client } from '@aws-sdk/client-s3';
|
||||
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
||||
import { MAX_FILE_SIZE_BYTES } from './validation';
|
||||
|
||||
const s3Client = new S3Client({
|
||||
region: process.env.AWS_S3_REGION,
|
||||
@ -20,13 +21,18 @@ type S3Upload = {
|
||||
|
||||
export const getUploadFileSignedURLFromS3 = async ({ fileName, fileType, userId }: S3Upload) => {
|
||||
const key = getS3Key(fileName, userId);
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: process.env.AWS_S3_FILES_BUCKET,
|
||||
|
||||
const { url: s3UploadUrl, fields: s3UploadFields } = await createPresignedPost(s3Client, {
|
||||
Bucket: process.env.AWS_S3_FILES_BUCKET!,
|
||||
Key: key,
|
||||
ContentType: fileType,
|
||||
Conditions: [['content-length-range', 0, MAX_FILE_SIZE_BYTES]],
|
||||
Fields: {
|
||||
'Content-Type': fileType,
|
||||
},
|
||||
Expires: 3600,
|
||||
});
|
||||
const uploadUrl = await getSignedUrl(s3Client, command, { expiresIn: 3600 });
|
||||
return { uploadUrl, key };
|
||||
|
||||
return { s3UploadUrl, key, s3UploadFields };
|
||||
};
|
||||
|
||||
export const getDownloadFileSignedURLFromS3 = async ({ key }: { key: string }) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Set this to the max file size you want to allow (currently 5MB).
|
||||
export const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||
export const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
||||
export const ALLOWED_FILE_TYPES = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
|
Loading…
x
Reference in New Issue
Block a user