diff --git a/.env.example b/.env.example index 617f951a..8c947521 100644 --- a/.env.example +++ b/.env.example @@ -29,4 +29,10 @@ SRH_CONNECTION_STRING="redis://redis:6379" # Marble Blog (optional) MARBLE_WORKSPACE_KEY= -MARBLE_API_URL=https://api.marblecms.com \ No newline at end of file +MARBLE_API_URL=https://api.marblecms.com + +# AWS S3 for file uploads +AWS_REGION="us-east-1" +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_S3_BUCKET_NAME= \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 284c14d6..2026a865 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,6 +13,8 @@ "@ai-sdk/openai": "^2.0.64", "@ai-sdk/react": "^2.0.89", "@ariakit/react": "^0.4.18", + "@better-upload/client": "^3.0.2", + "@better-upload/server": "^3.0.2", "@formkit/tempo": "^0.1.2", "@googlemaps/js-api-loader": "^2.0.0", "@heroicons/react": "^2.2.0", diff --git a/apps/web/src/app/api/upload/route.ts b/apps/web/src/app/api/upload/route.ts new file mode 100644 index 00000000..1e92313b --- /dev/null +++ b/apps/web/src/app/api/upload/route.ts @@ -0,0 +1,36 @@ +import { headers } from "next/headers"; +import { RejectUpload, route, type Router } from "@better-upload/server"; +import { toRouteHandler } from "@better-upload/server/adapters/next"; +import { aws } from "@better-upload/server/clients"; + +import { auth } from "@repo/auth/server"; +import { env } from "@repo/env/server"; + +const uploadRouter: Router = { + client: aws({ + region: env.AWS_REGION, + accessKeyId: env.AWS_ACCESS_KEY_ID, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY, + }), + bucketName: env.AWS_S3_BUCKET_NAME, + routes: { + files: route({ + fileTypes: ["*/*"], + multipleFiles: true, + maxFiles: 10, + maxFileSize: 100 * 1024 * 1024, // 100MB in bytes + onBeforeUpload: async ({ req }) => { + const headersList = await headers(); + const session = await auth.api.getSession({ + headers: headersList, + }); + + if (!session?.user) { + throw new RejectUpload("Not logged in!"); + } + }, + }), + }, +}; + +export const { POST } = toRouteHandler(uploadRouter); diff --git a/bun.lock b/bun.lock index b43c8210..4f46a384 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,8 @@ "@ai-sdk/openai": "^2.0.64", "@ai-sdk/react": "^2.0.89", "@ariakit/react": "^0.4.18", + "@better-upload/client": "^3.0.2", + "@better-upload/server": "^3.0.2", "@formkit/tempo": "^0.1.2", "@googlemaps/js-api-loader": "^2.0.0", "@heroicons/react": "^2.2.0", @@ -241,6 +243,9 @@ "@repo/env": "workspace:*", "postgres": "^3.4.7", "server-only": "^0.0.1", + "superjson": "^2.2.2", + "superjson-temporal": "^0.4.0", + "temporal-polyfill": "^0.3.0", }, "devDependencies": { "@repo/eslint-config": "workspace:*", @@ -656,6 +661,10 @@ "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], + "@better-upload/client": ["@better-upload/client@3.0.2", "", { "peerDependencies": { "react": "*" } }, "sha512-wNdRmee0/X55MK+TugZ6EvuSTN1IAowmNzRWm5aH7ymSZQmdWBCo0SS5mj7A0Fw65VYbr2EtLzwM1KoE5Mliyw=="], + + "@better-upload/server": ["@better-upload/server@3.0.2", "", { "dependencies": { "aws4fetch": "^1.0.20", "fast-xml-parser": "^5.3.0", "zod": "^4.1.12" } }, "sha512-NNYAsVgjZvwXVtxRdrg6ARwGxAL6aNQof7UAdrxJrOJ9unNeZQplF1YyNt/BXYGLDDZ4I97E+cqfkaomF0RXwA=="], + "@browserbasehq/sdk": ["@browserbasehq/sdk@2.6.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA=="], "@composio/client": ["@composio/client@0.1.0-alpha.38", "", {}, "sha512-+Gm5EQdFCu408YqPb9rKdVK7QD9eLS5wJTeYw1QH9eA6hfLmTBKVHFLc32tbLlY9nFwmClyF7KIB6y4NUfYcQw=="], @@ -1824,6 +1833,8 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], "axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="], @@ -2154,6 +2165,8 @@ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-xml-parser": ["fast-xml-parser@5.3.1", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-jbNkWiv2Ec1A7wuuxk0br0d0aTMUtQ4IkL+l/i1r9PRf6pLXjDgsBsWwO+UyczmQlnehi4Tbc8/KIvxGQe+I/A=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fault": ["fault@1.0.4", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA=="], @@ -2996,6 +3009,8 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="], + "style-to-js": ["style-to-js@1.1.18", "", { "dependencies": { "style-to-object": "1.0.11" } }, "sha512-JFPn62D4kJaPTnhFUI244MThx+FEGbi+9dw1b9yBBQ+1CZpV7QAT8kUtJ7b7EUNdHajjF/0x8fT+16oLJoojLg=="], "style-to-object": ["style-to-object@1.0.11", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow=="], diff --git a/packages/env/src/server.ts b/packages/env/src/server.ts index 863da8b7..50819c88 100644 --- a/packages/env/src/server.ts +++ b/packages/env/src/server.ts @@ -22,6 +22,10 @@ export const env = createEnv({ COMPOSIO_API_KEY: z.string().optional(), FIRECRAWL_API_KEY: z.string().min(1), BROWSERBASE_API_KEY: z.string().min(1), + AWS_REGION: z.string().min(1), + AWS_ACCESS_KEY_ID: z.string().min(1), + AWS_SECRET_ACCESS_KEY: z.string().min(1), + AWS_S3_BUCKET_NAME: z.string().min(1), }, experimental__runtimeEnv: process.env, skipValidation: process.env.NODE_ENV !== "production",