diff --git a/README.md b/README.md index 9ce6f73c..0115ac5e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,12 @@ S3_UPLOAD_BUCKET=name-of-s3-bucket S3_UPLOAD_REGION=us-east-1 ``` +You can also use other S3 providers by providing a custom endpoint: + +```.env +S3_UPLOAD_ENDPOINT=http://localhost:9000 +``` + ### Create expense from receipt You can offer users to create expense by uploading a receipt. This feature relies on [OpenAI GPT-4 with Vision](https://platform.openai.com/docs/guides/vision). diff --git a/next.config.js b/next.config.js index 6d6b0c3a..89faa5de 100644 --- a/next.config.js +++ b/next.config.js @@ -1,14 +1,27 @@ +/** + * Undefined entries are not supported. Push optional patterns to this array only if defined. + * @type {import('next/dist/shared/lib/image-config').RemotePattern} + */ +const remotePatterns = [] + +// S3 Storage +if (process.env.S3_UPLOAD_ENDPOINT) { + // custom endpoint for providers other than AWS + const url = new URL(process.env.S3_UPLOAD_ENDPOINT); + remotePatterns.push({ + hostname: url.hostname, + }) +} else if (process.env.S3_UPLOAD_BUCKET && process.env.S3_UPLOAD_REGION) { + // default provider + remotePatterns.push({ + hostname: `${process.env.S3_UPLOAD_BUCKET}.s3.${process.env.S3_UPLOAD_REGION}.amazonaws.com`, + }) +} + /** @type {import('next').NextConfig} */ const nextConfig = { images: { - remotePatterns: - process.env.S3_UPLOAD_BUCKET && process.env.S3_UPLOAD_REGION - ? [ - { - hostname: `${process.env.S3_UPLOAD_BUCKET}.s3.${process.env.S3_UPLOAD_REGION}.amazonaws.com`, - }, - ] - : [], + remotePatterns }, } diff --git a/src/app/api/s3-upload/route.ts b/src/app/api/s3-upload/route.ts index 567cd405..ee3fb729 100644 --- a/src/app/api/s3-upload/route.ts +++ b/src/app/api/s3-upload/route.ts @@ -1,4 +1,5 @@ import { randomId } from '@/lib/api' +import { env } from '@/lib/env' import { POST as route } from 'next-s3-upload/route' export const POST = route.configure({ @@ -8,4 +9,7 @@ export const POST = route.configure({ const random = randomId() return `document-${timestamp}-${random}${extension.toLowerCase()}` }, + endpoint: env.S3_UPLOAD_ENDPOINT, + // forcing path style is only necessary for providers other than AWS + forcePathStyle: !!env.S3_UPLOAD_ENDPOINT, }) diff --git a/src/components/expense-documents-input.tsx b/src/components/expense-documents-input.tsx index b9cda334..7b69a6d5 100644 --- a/src/components/expense-documents-input.tsx +++ b/src/components/expense-documents-input.tsx @@ -18,7 +18,7 @@ import { useToast } from '@/components/ui/use-toast' import { randomId } from '@/lib/api' import { ExpenseFormValues } from '@/lib/schemas' import { Loader2, Plus, Trash, X } from 'lucide-react' -import { getImageData, useS3Upload } from 'next-s3-upload' +import { getImageData, usePresignedUpload } from 'next-s3-upload' import Image from 'next/image' import { useEffect, useState } from 'react' @@ -29,7 +29,7 @@ type Props = { export function ExpenseDocumentsInput({ documents, updateDocuments }: Props) { const [pending, setPending] = useState(false) - const { FileInput, openFileDialog, uploadToS3 } = useS3Upload() + const { FileInput, openFileDialog, uploadToS3 } = usePresignedUpload() // use presigned uploads to addtionally support providers other than AWS const { toast } = useToast() const handleFileChange = async (file: File) => { diff --git a/src/lib/env.ts b/src/lib/env.ts index 33b835d8..f1d59dbf 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -17,12 +17,14 @@ const envSchema = z S3_UPLOAD_SECRET: z.string().optional(), S3_UPLOAD_BUCKET: z.string().optional(), S3_UPLOAD_REGION: z.string().optional(), + S3_UPLOAD_ENDPOINT: z.string().optional(), NEXT_PUBLIC_ENABLE_RECEIPT_EXTRACT: z.coerce.boolean().default(false), OPENAI_API_KEY: z.string().optional(), }) .superRefine((env, ctx) => { if ( env.NEXT_PUBLIC_ENABLE_EXPENSE_DOCUMENTS && + // S3_UPLOAD_ENDPOINT is fully optional as it will only be used for providers other than AWS (!env.S3_UPLOAD_BUCKET || !env.S3_UPLOAD_KEY || !env.S3_UPLOAD_REGION ||