Skip to content

Commit

Permalink
first pass at app dir support
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanto committed Oct 28, 2023
1 parent f072bd3 commit 6e412da
Show file tree
Hide file tree
Showing 36 changed files with 6,971 additions and 3,958 deletions.
6 changes: 0 additions & 6 deletions .github/workflows/next-s3-upload.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,6 @@ jobs:
- name: Install deps and build (with cache)
uses: bahmutov/npm-install@v1

- name: Lint
run: yarn workspace next-s3-upload lint

- name: Test
run: yarn workspace next-s3-upload test --ci --coverage --maxWorkers=2

- name: Build
run: yarn workspace next-s3-upload build

Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.13.0
20.9.0
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"private": true,
"workspaces": ["packages/next-s3-upload", "packages/docs-site"],
"workspaces": ["packages/next-s3-upload", "packages/docs-site", "test-apps/app-dir"],
"scripts": {
"dev": "yarn workspace next-s3-upload start",
"dev": "yarn workspace next-s3-upload dev",
"build": "yarn workspace next-s3-upload build",
"docs": "yarn workspace docs-site dev",
"test": "yarn workspace docs-site cypress run"
Expand Down
19 changes: 18 additions & 1 deletion packages/docs-site/src/pages/setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,22 @@ Once the user is created you'll see a screen with their API keys. Copy these key

That's it! We're done configuring AWS for uploads.

## API Route
## API Route (App router)

_Only follow this step if you're using the App Router_

In order for our Next app to securely communicate with S3 we'll need to create an API route. Paste the following into `app/api/s3-upload/route.js`.

```js
// app/api/s3-upload/route.js
export { POST } from "next-s3-upload/route";
```

That's it. This module's `POST` takes care of all the communication with S3.

## API Route (Pages router)

_Only follow this step if you're using the pages router._

In order for our Next app to securely communicate with S3 we'll need to create an API route. Paste the following into `pages/api/s3-upload.js`.

Expand All @@ -171,4 +186,6 @@ export { APIRoute as default } from "next-s3-upload";

That's it. This module's `APIRoute` takes care of all the communication with S3.

## Next steps

You're now ready to [start uploading](/basic-example) files to your S3 bucket!
3 changes: 3 additions & 0 deletions packages/next-s3-upload/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["react-app"]
}
1 change: 1 addition & 0 deletions packages/next-s3-upload/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.cache
dist
coverage
.rollup.cache
63 changes: 30 additions & 33 deletions packages/next-s3-upload/package.json
Original file line number Diff line number Diff line change
@@ -1,63 +1,60 @@
{
"name": "next-s3-upload",
"author": "Ryan Toronto <[email protected]>",
"version": "0.3.3",
"license": "MIT",
"repository": "github:ryanto/next-s3-upload",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"module": "dist/index.js",
"files": [
"dist",
"src"
],
"engines": {
"node": ">=10"
},
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./route": {
"import": "./dist/route.js",
"require": "./dist/route.cjs.js"
}
},
"typings": "dist/types.d.ts",
"typesVersions": {
"*": {
"*": [
"./dist/*",
"./dist/index"
]
}
},
"scripts": {
"start": "tsdx watch",
"build": "tsdx build",
"test": "tsdx test --passWithNoTests",
"lint": "tsdx lint",
"prepare": "tsdx build",
"size": "size-limit",
"analyze": "size-limit --why"
"dev": "vite build --watch",
"build": "vite build"
},
"peerDependencies": {
"next": ">=9.4",
"react": ">=16"
},
"repository": "github:ryanto/next-s3-upload",
"prettier": {
"printWidth": 80,
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"name": "next-s3-upload",
"author": "Ryan Toronto <[email protected]>",
"module": "dist/next-s3-upload.esm.js",
"size-limit": [
{
"path": "dist/next-s3-upload.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/next-s3-upload.esm.js",
"limit": "10 KB"
}
],
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.5",
"@size-limit/preset-small-lib": "^4.7.0",
"@types/node": "^14.14.7",
"@types/react": "^16.9.56",
"@types/react-dom": "^16.9.9",
"@types/uuid": "^8.3.0",
"babel-jest": "^26.6.3",
"husky": "^4.3.0",
"@vitejs/plugin-react": "^4.1.0",
"eslint-config-react-app": "^7.0.1",
"next": "^10.0.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"size-limit": "^4.7.0",
"tsdx": "^0.14.1",
"tslib": "^2.1.0",
"typescript": "^4.2.3"
"typescript": "^5",
"vite": "^4.5.0"
},
"dependencies": {
"@aws-sdk/client-s3": ">=3.427.0",
Expand Down
47 changes: 0 additions & 47 deletions packages/next-s3-upload/src/app/api/s3-upload.ts

This file was deleted.

32 changes: 32 additions & 0 deletions packages/next-s3-upload/src/backend/app-dir-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextResponse, NextRequest } from 'next/server';
import { Options, handler } from './handler';

type NextAppRouteHandler = (req: NextRequest) => Promise<Response>;

type Configure = (options: Options<NextRequest>) => Handler;
type Handler = NextAppRouteHandler & { configure: Configure };

const makeRouteHandler = (options: Options<NextRequest> = {}): Handler => {
let nextAppRoute: NextAppRouteHandler = async function(req) {
try {
const response = await handler({
request: req,
options: options,
});

return NextResponse.json(response);
} catch (e) {
console.error(e);
return NextResponse.error();
}
};

const configure = (options: Options<NextRequest>) =>
makeRouteHandler(options);

return Object.assign(nextAppRoute, { configure });
};

const POST = makeRouteHandler();

export { POST };
110 changes: 110 additions & 0 deletions packages/next-s3-upload/src/backend/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
STSClient,
GetFederationTokenCommand,
STSClientConfig,
} from '@aws-sdk/client-sts';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { S3Config, getConfig } from '../utils/config';
import { getClient } from '../utils/client';
import { sanitizeKey, uuid } from '../utils/keys';
import { NextApiRequest } from 'next';
import { NextRequest } from 'next/server';

type AppOrPagesRequest = NextApiRequest | NextRequest;

export type Options<R extends AppOrPagesRequest> = S3Config & {
key?: (req: R, filename: string) => string | Promise<string>;
};

export async function handler<R extends NextApiRequest | NextRequest>({
request,
options,
}: {
request: R;
options: Options<R>;
}) {
let s3Config = getConfig(options);

let missing = missingEnvs(s3Config);
if (missing.length > 0) {
throw new Error(`Next S3 Upload: Missing ENVs ${missing.join(', ')}`);
}

// upgrade typescript and use 'in'
// @ts-ignore
let body = request.json ? await request.json() : request.body;
let { filename } = body;

const key = options.key
? await Promise.resolve(options.key(request, filename))
: `next-s3-uploads/${uuid()}/${sanitizeKey(filename)}`;

const uploadType = body._nextS3?.strategy;
const { bucket, region, endpoint } = s3Config;

if (uploadType === 'presigned') {
let { filetype } = body;
let client = getClient(s3Config);
let params = {
Bucket: bucket,
Key: key,
ContentType: filetype,
CacheControl: 'max-age=630720000',
};

let url = await getSignedUrl(client, new PutObjectCommand(params), {
expiresIn: 60 * 60,
});

return {
key,
bucket,
region,
endpoint,
url,
};
} else {
let stsConfig: STSClientConfig = {
credentials: {
accessKeyId: s3Config.accessKeyId,
secretAccessKey: s3Config.secretAccessKey,
},
region,
};

let policy = {
Statement: [
{
Sid: 'Stmt1S3UploadAssets',
Effect: 'Allow',
Action: ['s3:PutObject'],
Resource: [`arn:aws:s3:::${bucket}/${key}`],
},
],
};

let sts = new STSClient(stsConfig);

let command = new GetFederationTokenCommand({
Name: 'S3UploadWebToken',
Policy: JSON.stringify(policy),
DurationSeconds: 60 * 60, // 1 hour
});

let token = await sts.send(command);

return {
token,
key,
bucket,
region,
};
}
}

const missingEnvs = (config: Record<string, any>): string[] => {
const required = ['accessKeyId', 'secretAccessKey', 'bucket', 'region'];

return required.filter(key => !config[key] || config.key === '');
};
35 changes: 35 additions & 0 deletions packages/next-s3-upload/src/backend/pages-api-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { Options, handler } from './handler';

type NextRouteHandler = (
req: NextApiRequest,
res: NextApiResponse
) => Promise<void>;

type Configure = (options: Options<NextApiRequest>) => Handler;
type Handler = NextRouteHandler & { configure: Configure };

let makeRouteHandler = (options: Options<NextApiRequest> = {}): Handler => {
let nextPageRoute: NextRouteHandler = async function(req, res) {
try {
let body = await handler({
request: req,
options: options,
});

res.status(200).json(body);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'Internal Server Error' });
}
};

let configure = (options: Options<NextApiRequest>) =>
makeRouteHandler(options);

return Object.assign(nextPageRoute, { configure });
};

let APIRoute = makeRouteHandler();

export { APIRoute };
2 changes: 1 addition & 1 deletion packages/next-s3-upload/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { APIRoute } from './pages/api/s3-upload';
export { APIRoute } from './backend/pages-api-route';
export { useS3Upload } from './hooks/use-s3-upload';
export { usePresignedUpload } from './hooks/use-presigned-upload';
export { getImageData } from './utils/image-data';
Expand Down
Loading

0 comments on commit 6e412da

Please sign in to comment.