diff --git a/next.config.js b/next.config.js index 69d6f13f..9ee18918 100644 --- a/next.config.js +++ b/next.config.js @@ -7,10 +7,10 @@ const VERCEL_BLOB_HOSTNAME = VERCEL_BLOB_STORE_ID : undefined; const AWS_S3_HOSTNAME = - process.env.NEXT_PUBLIC_S3_BUCKET && - process.env.NEXT_PUBLIC_S3_REGION + process.env.NEXT_PUBLIC_AWS_S3_BUCKET && + process.env.NEXT_PUBLIC_AWS_S3_REGION // eslint-disable-next-line max-len - ? `${process.env.NEXT_PUBLIC_S3_BUCKET}.s3.${process.env.NEXT_PUBLIC_S3_REGION}.amazonaws.com` + ? `${process.env.NEXT_PUBLIC_AWS_S3_BUCKET}.s3.${process.env.NEXT_PUBLIC_AWS_S3_REGION}.amazonaws.com` : undefined; const createRemotePattern = (hostname) => hostname diff --git a/src/app/api/aws-s3/presigned-url/[key]/route.ts b/src/app/api/aws-s3/presigned-url/[key]/route.ts new file mode 100644 index 00000000..f22ad573 --- /dev/null +++ b/src/app/api/aws-s3/presigned-url/[key]/route.ts @@ -0,0 +1,20 @@ +import { auth } from '@/auth'; +import { awsS3GetSignedUploadUrl } from '@/services/blob/aws-s3'; + +export const runtime = 'edge'; + +export async function GET( + _: Request, + { params: { key } }: { params: { key: string } }, +) { + const session = await auth(); + if (session?.user && key) { + const url = await awsS3GetSignedUploadUrl(key); + return new Response( + url, + { headers: { 'content-type': 'text/plain' } }, + ); + } else { + return new Response('Unauthorized request', { status: 401 }); + } +} diff --git a/src/app/api/route.ts b/src/app/api/route.ts index 491e7610..ba5f82ad 100644 --- a/src/app/api/route.ts +++ b/src/app/api/route.ts @@ -19,6 +19,6 @@ export async function GET() { photos: photos.map(formatPhotoForApi), }); } else { - return Response.json({ message: 'API is disabled' }); + return new Response('API access disabled', { status: 404 }); } } diff --git a/src/services/blob/aws-s3.ts b/src/services/blob/aws-s3.ts index 2971dada..5803b2d2 100644 --- a/src/services/blob/aws-s3.ts +++ b/src/services/blob/aws-s3.ts @@ -6,24 +6,20 @@ import { ListObjectsCommand, PutObjectCommand, } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET ?? ''; -const S3_REGION = process.env.NEXT_PUBLIC_S3_REGION ?? ''; -const S3_UPLOAD_ACCESS_KEY = - process.env.NEXT_PUBLIC_S3_UPLOAD_ACCESS_KEY ?? ''; -const S3_UPLOAD_SECRET_ACCESS_KEY = - process.env.NEXT_PUBLIC_S3_UPLOAD_SECRET_ACCESS_KEY ?? ''; -const S3_ADMIN_ACCESS_KEY = - process.env.S3_ADMIN_ACCESS_KEY; -const S3_ADMIN_SECRET_ACCESS_KEY = - process.env.S3_ADMIN_SECRET_ACCESS_KEY; +const S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? ''; +const S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? ''; +const S3_ACCESS_KEY = process.env.AWS_S3_ACCESS_KEY ?? ''; +const S3_SECRET_ACCESS_KEY = process.env.AWS_S3_SECRET_ACCESS_KEY ?? ''; + +const API_PATH_PRESIGNED_URL = '/api/aws-s3/presigned-url'; const client = () => new S3Client({ region: S3_REGION, credentials: { - // Fall back on upload credentials when admin credentials aren't available - accessKeyId: S3_ADMIN_ACCESS_KEY ?? S3_UPLOAD_ACCESS_KEY, - secretAccessKey: S3_ADMIN_SECRET_ACCESS_KEY ?? S3_UPLOAD_SECRET_ACCESS_KEY, + accessKeyId: S3_ACCESS_KEY, + secretAccessKey: S3_SECRET_ACCESS_KEY, }, }); @@ -37,22 +33,30 @@ const urlForKey = (key?: string) => `${AWS_S3_BASE_URL}/${key}`; const generateBlobId = () => generateNanoid(16); +// Runs on server +export const awsS3GetSignedUploadUrl = async (Key: string) => + getSignedUrl( + client(), + new PutObjectCommand({ Bucket: S3_BUCKET, Key, ACL: 'public-read' }), + { expiresIn: 3600 } + ); + +// Runs on client export const awsS3UploadFromClient = async ( file: File | Blob, fileName: string, extension: string, addRandomSuffix?: boolean, ) => { - const Key = addRandomSuffix + const key = addRandomSuffix ? `${fileName}-${generateBlobId()}.${extension}` : `${fileName}.${extension}`; - return client().send(new PutObjectCommand({ - Bucket: S3_BUCKET, - Key, - Body: file, - ACL: 'public-read', - })) - .then(() => urlForKey(Key)); + + const url = await fetch(`${API_PATH_PRESIGNED_URL}/${key}`) + .then((response) => response.text()); + + return fetch(url, { method: 'PUT', body: file }) + .then(() => urlForKey(key)); }; export const awsS3Copy = async ( diff --git a/src/services/blob/index.ts b/src/services/blob/index.ts index 42e4f8d5..ccfe0c5d 100644 --- a/src/services/blob/index.ts +++ b/src/services/blob/index.ts @@ -13,7 +13,7 @@ import { awsS3UploadFromClient, isUrlFromAwsS3, } from './aws-s3'; -import { HAS_AWS_S3_STORAGE_CLIENT, HAS_AWS_S3_STORAGE } from '@/site/config'; +import { HAS_AWS_S3_STORAGE, HAS_AWS_S3_STORAGE_CLIENT } from '@/site/config'; const PREFIX_UPLOAD = 'upload'; const PREFIX_PHOTO = 'photo'; diff --git a/src/site/config.ts b/src/site/config.ts index a1f510c7..119c9ed4 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -35,17 +35,15 @@ export const HAS_VERCEL_BLOB = (process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0; // STORAGE: AWS S3 -// Includes separate check for client-side usage, i.e., uploading, +// Includes separate check for client-side usage, +// i.e., uploading, url construction export const HAS_AWS_S3_STORAGE_CLIENT = - (process.env.NEXT_PUBLIC_S3_BUCKET ?? '').length > 0 && - (process.env.NEXT_PUBLIC_S3_REGION ?? '').length > 0 && - (process.env.NEXT_PUBLIC_S3_UPLOAD_ACCESS_KEY ?? '').length > 0 && - (process.env.NEXT_PUBLIC_S3_UPLOAD_SECRET_ACCESS_KEY ?? '').length > 0; + (process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '').length > 0 && + (process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '').length > 0; export const HAS_AWS_S3_STORAGE = HAS_AWS_S3_STORAGE_CLIENT && - (process.env.S3_ADMIN_ACCESS_KEY ?? '').length > 0 && - (process.env.S3_ADMIN_SECRET_ACCESS_KEY ?? '').length > 0; - + (process.env.AWS_S3_ACCESS_KEY ?? '').length > 0 && + (process.env.AWS_S3_SECRET_ACCESS_KEY ?? '').length > 0; // SETTINGS