Merge pull request #23 from sambecker/s3-presigned-url
Switch S3 to presigned url strategy
This commit is contained in:
commit
205d5fb0bf
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -18,6 +18,7 @@
|
|||||||
"Makernote",
|
"Makernote",
|
||||||
"nanoids",
|
"nanoids",
|
||||||
"nextjs",
|
"nextjs",
|
||||||
|
"presigner",
|
||||||
"Provia",
|
"Provia",
|
||||||
"qaub",
|
"qaub",
|
||||||
"QRSTUVWXYZ",
|
"QRSTUVWXYZ",
|
||||||
|
|||||||
34
README.md
34
README.md
@ -91,31 +91,9 @@ Installation
|
|||||||
}]
|
}]
|
||||||
```
|
```
|
||||||
- Store configuration
|
- Store configuration
|
||||||
- Bucket name: `NEXT_PUBLIC_S3_BUCKET`
|
- Bucket name: `NEXT_PUBLIC_AWS_S3_BUCKET`
|
||||||
- Bucket region: `NEXT_PUBLIC_S3_REGION`
|
- Bucket region: `NEXT_PUBLIC_AWS_S3_REGION`
|
||||||
2. Setup client upload credentials
|
2. Setup credentials
|
||||||
- [Create IAM policy](https://console.aws.amazon.com/iam/home#/policies) using JSON editor:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Version": "2012-10-17",
|
|
||||||
"Statement": [
|
|
||||||
{
|
|
||||||
"Effect": "Allow",
|
|
||||||
"Action": [
|
|
||||||
"s3:PutObject",
|
|
||||||
"s3:PutObjectACL"
|
|
||||||
],
|
|
||||||
"Resource": [
|
|
||||||
"arn:aws:s3:::{BUCKET_NAME}/upload-*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- [Create IAM user](https://console.aws.amazon.com/iam/home#/users) by choosing "Attach policies directly." Create access key under "Security credentials," choose "Application running outside AWS," and store credentials:
|
|
||||||
- `NEXT_PUBLIC_S3_UPLOAD_ACCESS_KEY`
|
|
||||||
- `NEXT_PUBLIC_S3_UPLOAD_SECRET_ACCESS_KEY`
|
|
||||||
3. Setup server admin credentials
|
|
||||||
- [Create IAM policy](https://console.aws.amazon.com/iam/home#/policies) using JSON editor:
|
- [Create IAM policy](https://console.aws.amazon.com/iam/home#/policies) using JSON editor:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@ -138,9 +116,9 @@ Installation
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- [Create IAM user](https://console.aws.amazon.com/iam/home#/users) by choosing "Attach policies directly." Create access key under "Security credentials," choose "Application running outside AWS," and store credentials (⚠️ _Ensure admin environment variables are not prefixed with `NEXT_PUBLIC`_):
|
- [Create IAM user](https://console.aws.amazon.com/iam/home#/users) by choosing "Attach policies directly." Create access key under "Security credentials," choose "Application running outside AWS," and store credentials (⚠️ _Ensure credential environment variables are not prefixed with `NEXT_PUBLIC`_):
|
||||||
- `S3_ADMIN_ACCESS_KEY`
|
- `AWS_S3_ACCESS_KEY`
|
||||||
- `S3_ADMIN_SECRET_ACCESS_KEY`
|
- `AWS_S3_SECRET_ACCESS_KEY`
|
||||||
|
|
||||||
FAQ
|
FAQ
|
||||||
-
|
-
|
||||||
|
|||||||
@ -7,10 +7,10 @@ const VERCEL_BLOB_HOSTNAME = VERCEL_BLOB_STORE_ID
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const AWS_S3_HOSTNAME =
|
const AWS_S3_HOSTNAME =
|
||||||
process.env.NEXT_PUBLIC_S3_BUCKET &&
|
process.env.NEXT_PUBLIC_AWS_S3_BUCKET &&
|
||||||
process.env.NEXT_PUBLIC_S3_REGION
|
process.env.NEXT_PUBLIC_AWS_S3_REGION
|
||||||
// eslint-disable-next-line max-len
|
// 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;
|
: undefined;
|
||||||
|
|
||||||
const createRemotePattern = (hostname) => hostname
|
const createRemotePattern = (hostname) => hostname
|
||||||
|
|||||||
15
package.json
15
package.json
@ -9,17 +9,18 @@
|
|||||||
"analyze": "ANALYZE=true next build"
|
"analyze": "ANALYZE=true next build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.456.0",
|
"@aws-sdk/client-s3": "3.462.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "3.462.0",
|
||||||
"@next/bundle-analyzer": "14.0.3",
|
"@next/bundle-analyzer": "14.0.3",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@testing-library/jest-dom": "^6.1.4",
|
"@testing-library/jest-dom": "^6.1.4",
|
||||||
"@testing-library/react": "^14.1.2",
|
"@testing-library/react": "^14.1.2",
|
||||||
"@types/jest": "^29.5.10",
|
"@types/jest": "^29.5.10",
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"@types/react": "18.2.38",
|
"@types/react": "18.2.39",
|
||||||
"@types/react-dom": "18.2.17",
|
"@types/react-dom": "18.2.17",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
||||||
"@typescript-eslint/parser": "^6.12.0",
|
"@typescript-eslint/parser": "^6.13.1",
|
||||||
"@vercel/analytics": "^1.1.1",
|
"@vercel/analytics": "^1.1.1",
|
||||||
"@vercel/blob": "^0.15.1",
|
"@vercel/blob": "^0.15.1",
|
||||||
"@vercel/postgres": "0.5.1",
|
"@vercel/postgres": "0.5.1",
|
||||||
@ -29,18 +30,18 @@
|
|||||||
"eslint": "8.54.0",
|
"eslint": "8.54.0",
|
||||||
"eslint-config-next": "14.0.3",
|
"eslint-config-next": "14.0.3",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"framer-motion": "^10.16.5",
|
"framer-motion": "^10.16.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"nanoid": "^5.0.3",
|
"nanoid": "^5.0.3",
|
||||||
"next": "14.0.3",
|
"next": "14.0.3",
|
||||||
"next-auth": "5.0.0-beta.3",
|
"next-auth": "5.0.0-beta.4",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"postcss": "8.4.31",
|
"postcss": "8.4.31",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-icons": "^4.12.0",
|
"react-icons": "^4.12.0",
|
||||||
"sonner": "^1.2.3",
|
"sonner": "^1.2.4",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
"ts-exif-parser": "^0.2.2",
|
"ts-exif-parser": "^0.2.2",
|
||||||
"typescript": "5.3.2"
|
"typescript": "5.3.2"
|
||||||
|
|||||||
930
pnpm-lock.yaml
generated
930
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
28
src/app/api/aws-s3/presigned-url/[key]/route.ts
Normal file
28
src/app/api/aws-s3/presigned-url/[key]/route.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { auth } from '@/auth';
|
||||||
|
import {
|
||||||
|
awsS3Client,
|
||||||
|
awsS3PutObjectCommandForKey,
|
||||||
|
} from '@/services/blob/aws-s3';
|
||||||
|
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||||
|
|
||||||
|
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 getSignedUrl(
|
||||||
|
awsS3Client(),
|
||||||
|
awsS3PutObjectCommandForKey(key),
|
||||||
|
{ expiresIn: 3600 }
|
||||||
|
);
|
||||||
|
return new Response(
|
||||||
|
url,
|
||||||
|
{ headers: { 'content-type': 'text/plain' } },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new Response('Unauthorized request', { status: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,6 +19,6 @@ export async function GET() {
|
|||||||
photos: photos.map(formatPhotoForApi),
|
photos: photos.map(formatPhotoForApi),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Response.json({ message: 'API is disabled' });
|
return new Response('API access disabled', { status: 404 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,28 +7,23 @@ import {
|
|||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
} from '@aws-sdk/client-s3';
|
} from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
const S3_BUCKET = process.env.NEXT_PUBLIC_S3_BUCKET ?? '';
|
const AWS_S3_BUCKET = process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '';
|
||||||
const S3_REGION = process.env.NEXT_PUBLIC_S3_REGION ?? '';
|
const AWS_S3_REGION = process.env.NEXT_PUBLIC_AWS_S3_REGION ?? '';
|
||||||
const S3_UPLOAD_ACCESS_KEY =
|
const AWS_S3_ACCESS_KEY = process.env.AWS_S3_ACCESS_KEY ?? '';
|
||||||
process.env.NEXT_PUBLIC_S3_UPLOAD_ACCESS_KEY ?? '';
|
const AWS_S3_SECRET_ACCESS_KEY = process.env.AWS_S3_SECRET_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 client = () => new S3Client({
|
const API_PATH_PRESIGNED_URL = '/api/aws-s3/presigned-url';
|
||||||
region: S3_REGION,
|
|
||||||
|
export const awsS3Client = () => new S3Client({
|
||||||
|
region: AWS_S3_REGION,
|
||||||
credentials: {
|
credentials: {
|
||||||
// Fall back on upload credentials when admin credentials aren't available
|
accessKeyId: AWS_S3_ACCESS_KEY,
|
||||||
accessKeyId: S3_ADMIN_ACCESS_KEY ?? S3_UPLOAD_ACCESS_KEY,
|
secretAccessKey: AWS_S3_SECRET_ACCESS_KEY,
|
||||||
secretAccessKey: S3_ADMIN_SECRET_ACCESS_KEY ?? S3_UPLOAD_SECRET_ACCESS_KEY,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AWS_S3_BASE_URL =
|
export const AWS_S3_BASE_URL =
|
||||||
`https://${S3_BUCKET}.s3.${S3_REGION}.amazonaws.com`;
|
`https://${AWS_S3_BUCKET}.s3.${AWS_S3_REGION}.amazonaws.com`;
|
||||||
|
|
||||||
export const isUrlFromAwsS3 = (url: string) =>
|
export const isUrlFromAwsS3 = (url: string) =>
|
||||||
url.startsWith(AWS_S3_BASE_URL);
|
url.startsWith(AWS_S3_BASE_URL);
|
||||||
@ -37,22 +32,24 @@ const urlForKey = (key?: string) => `${AWS_S3_BASE_URL}/${key}`;
|
|||||||
|
|
||||||
const generateBlobId = () => generateNanoid(16);
|
const generateBlobId = () => generateNanoid(16);
|
||||||
|
|
||||||
|
export const awsS3PutObjectCommandForKey = (Key: string) =>
|
||||||
|
new PutObjectCommand({ Bucket: AWS_S3_BUCKET, Key, ACL: 'public-read' });
|
||||||
|
|
||||||
export const awsS3UploadFromClient = async (
|
export const awsS3UploadFromClient = async (
|
||||||
file: File | Blob,
|
file: File | Blob,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
extension: string,
|
extension: string,
|
||||||
addRandomSuffix?: boolean,
|
addRandomSuffix?: boolean,
|
||||||
) => {
|
) => {
|
||||||
const Key = addRandomSuffix
|
const key = addRandomSuffix
|
||||||
? `${fileName}-${generateBlobId()}.${extension}`
|
? `${fileName}-${generateBlobId()}.${extension}`
|
||||||
: `${fileName}.${extension}`;
|
: `${fileName}.${extension}`;
|
||||||
return client().send(new PutObjectCommand({
|
|
||||||
Bucket: S3_BUCKET,
|
const url = await fetch(`${API_PATH_PRESIGNED_URL}/${key}`)
|
||||||
Key,
|
.then((response) => response.text());
|
||||||
Body: file,
|
|
||||||
ACL: 'public-read',
|
return fetch(url, { method: 'PUT', body: file })
|
||||||
}))
|
.then(() => urlForKey(key));
|
||||||
.then(() => urlForKey(Key));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const awsS3Copy = async (
|
export const awsS3Copy = async (
|
||||||
@ -65,8 +62,8 @@ export const awsS3Copy = async (
|
|||||||
const Key = addRandomSuffix
|
const Key = addRandomSuffix
|
||||||
? `${name}-${generateBlobId()}.${extension}`
|
? `${name}-${generateBlobId()}.${extension}`
|
||||||
: fileNameDestination;
|
: fileNameDestination;
|
||||||
return client().send(new CopyObjectCommand({
|
return awsS3Client().send(new CopyObjectCommand({
|
||||||
Bucket: S3_BUCKET,
|
Bucket: AWS_S3_BUCKET,
|
||||||
CopySource: fileNameSource,
|
CopySource: fileNameSource,
|
||||||
Key,
|
Key,
|
||||||
ACL: 'public-read',
|
ACL: 'public-read',
|
||||||
@ -75,15 +72,15 @@ export const awsS3Copy = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const awsS3Delete = async (Key: string) => {
|
export const awsS3Delete = async (Key: string) => {
|
||||||
client().send(new DeleteObjectCommand({
|
awsS3Client().send(new DeleteObjectCommand({
|
||||||
Bucket: S3_BUCKET,
|
Bucket: AWS_S3_BUCKET,
|
||||||
Key,
|
Key,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const awsS3List = async (Prefix: string) =>
|
export const awsS3List = async (Prefix: string) =>
|
||||||
client().send(new ListObjectsCommand({
|
awsS3Client().send(new ListObjectsCommand({
|
||||||
Bucket: S3_BUCKET,
|
Bucket: AWS_S3_BUCKET,
|
||||||
Prefix,
|
Prefix,
|
||||||
}))
|
}))
|
||||||
.then((data) => data.Contents?.map(({ Key }) => urlForKey(Key)) ?? []);
|
.then((data) => data.Contents?.map(({ Key }) => urlForKey(Key)) ?? []);
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
awsS3UploadFromClient,
|
awsS3UploadFromClient,
|
||||||
isUrlFromAwsS3,
|
isUrlFromAwsS3,
|
||||||
} from './aws-s3';
|
} 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_UPLOAD = 'upload';
|
||||||
const PREFIX_PHOTO = 'photo';
|
const PREFIX_PHOTO = 'photo';
|
||||||
|
|||||||
@ -35,17 +35,15 @@ export const HAS_VERCEL_BLOB =
|
|||||||
(process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0;
|
(process.env.BLOB_READ_WRITE_TOKEN ?? '').length > 0;
|
||||||
|
|
||||||
// STORAGE: AWS S3
|
// 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 =
|
export const HAS_AWS_S3_STORAGE_CLIENT =
|
||||||
(process.env.NEXT_PUBLIC_S3_BUCKET ?? '').length > 0 &&
|
(process.env.NEXT_PUBLIC_AWS_S3_BUCKET ?? '').length > 0 &&
|
||||||
(process.env.NEXT_PUBLIC_S3_REGION ?? '').length > 0 &&
|
(process.env.NEXT_PUBLIC_AWS_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;
|
|
||||||
export const HAS_AWS_S3_STORAGE =
|
export const HAS_AWS_S3_STORAGE =
|
||||||
HAS_AWS_S3_STORAGE_CLIENT &&
|
HAS_AWS_S3_STORAGE_CLIENT &&
|
||||||
(process.env.S3_ADMIN_ACCESS_KEY ?? '').length > 0 &&
|
(process.env.AWS_S3_ACCESS_KEY ?? '').length > 0 &&
|
||||||
(process.env.S3_ADMIN_SECRET_ACCESS_KEY ?? '').length > 0;
|
(process.env.AWS_S3_SECRET_ACCESS_KEY ?? '').length > 0;
|
||||||
|
|
||||||
|
|
||||||
// SETTINGS
|
// SETTINGS
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user