├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── cli ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github │ ├── dependabot.yml │ └── workflows │ │ ├── automerge.yml │ │ ├── failureNotifications.yml │ │ ├── manualRelease.yml │ │ ├── notify-slack-on-pr-open.yml │ │ ├── onPushToMain.yml │ │ ├── onRelease.yml │ │ └── test.yml ├── .gitignore ├── .mocharc.json ├── .vscode │ └── launch.json ├── CONTRIBUTING.md ├── README.md ├── bin │ ├── dev │ ├── dev.cmd │ ├── run │ └── run.cmd ├── cdk.json ├── oclif.md ├── package.json ├── src │ ├── assume-role.ts │ ├── commands │ │ └── init.ts │ ├── configure-aws-client.ts │ ├── constants │ │ ├── _code-gen-presigned-api.ts │ │ ├── _code-gen-upload-component.ts │ │ ├── file-names.ts │ │ └── index.ts │ ├── create-files.ts │ ├── fetch-user.ts │ ├── iam │ │ ├── createRoleParams.ts │ │ └── index.ts │ ├── index.ts │ ├── launch-bucket.ts │ ├── update-status.ts │ └── update-user.ts ├── test │ ├── commands │ │ ├── clean-up.ts │ │ └── init.test.ts │ ├── helpers │ │ └── init.js │ └── tsconfig.json └── tsconfig.json ├── client ├── .eslintrc.cjs ├── .gitignore ├── .npmignore ├── README.md ├── dist │ ├── index.cjs │ ├── index.cjs.map │ ├── index.d.ts │ ├── index.mjs │ └── index.mjs.map ├── package.json ├── pnpm-lock.yaml ├── rollup.config.mjs ├── src │ ├── client.ts │ ├── index.ts │ ├── nextjs │ │ ├── components │ │ │ ├── Demo.tsx │ │ │ └── FileUploadInput.tsx │ │ ├── file-upload-hook.ts │ │ └── s3-presigned-api.ts │ └── util │ │ ├── config.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── s3-client.ts ├── test │ └── main.test.mjs └── tsconfig.json ├── docs ├── NR_PLATFORM.md └── logo │ ├── file-upload-completed.png │ ├── gifs │ ├── bucket-creation.gif │ ├── demo.gif │ ├── file-upload.gif │ └── hero-gif-full-demo.gif │ ├── netrunner-main-character.png │ ├── screenshot-10-july.png │ ├── screenshot-22-july.png │ └── screenshot-25-july.png ├── examples ├── app-router-example │ ├── .env.template │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── api │ │ │ └── upload │ │ │ │ ├── helper.ts │ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── styles │ │ └── globals.css │ ├── tailwind.config.js │ ├── tsconfig.json │ └── utils │ │ ├── components │ │ └── Common.tsx │ │ ├── index.tsx │ │ └── upload │ │ ├── components │ │ ├── FilePreview.tsx │ │ ├── Status.tsx │ │ └── icons │ │ │ └── IconUpload.tsx │ │ ├── hooks │ │ └── useUploadFile.tsx │ │ ├── index.tsx │ │ └── utils │ │ └── index.ts └── pages-router-example │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── components │ ├── index.tsx │ └── upload │ │ ├── components │ │ ├── FilePreview.tsx │ │ ├── Status.tsx │ │ └── icons │ │ │ └── IconUpload.tsx │ │ ├── hooks │ │ └── useUploadFile.tsx │ │ ├── index.tsx │ │ └── utils │ │ └── index.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── generateImgUrl.ts │ └── index.tsx │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── styles │ └── globals.css │ ├── tailwind.config.js │ └── tsconfig.json └── iam_netrunner_setup.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm " # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | 6 | **/.env 7 | 8 | .env 9 | 10 | **/.env.Local 11 | 12 | **/*.env 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | xata 20 | 21 | # Diagnostic reports (https://nodejs.org/api/report.html) 22 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 23 | 24 | # Runtime data 25 | pids 26 | *.pid 27 | *.seed 28 | *.pid.lock 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | .nyc_output 39 | 40 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 41 | .grunt 42 | 43 | # Bower dependency directory (https://bower.io/) 44 | bower_components 45 | 46 | # node-waf configuration 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | build/Release 51 | 52 | # Dependency directories 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # TypeScript v1 declaration files 57 | typings/ 58 | 59 | # TypeScript cache 60 | *.tsbuildinfo 61 | 62 | # Optional npm cache directory 63 | .npm 64 | 65 | # Optional eslint cache 66 | .eslintcache 67 | 68 | # Microbundle cache 69 | .rpt2_cache/ 70 | .rts2_cache_cjs/ 71 | .rts2_cache_es/ 72 | .rts2_cache_umd/ 73 | 74 | # Optional REPL history 75 | .node_repl_history 76 | 77 | # Output of 'npm pack' 78 | *.tgz 79 | 80 | # Yarn Integrity file 81 | .yarn-integrity 82 | 83 | # dotenv environment variables file 84 | .env 85 | .env.test 86 | 87 | # parcel-bundler cache (https://parceljs.org/) 88 | .cache 89 | 90 | # Next.js build output 91 | .next 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # Serverless directories 107 | .serverless/ 108 | 109 | # FuseBox cache 110 | .fusebox/ 111 | 112 | # DynamoDB Local files 113 | .dynamodb/ 114 | 115 | # TernJS port file 116 | .tern-port 117 | 118 | # NPM config 119 | .npmrc 120 | 121 | .DS_Store 122 | .turbo 123 | 124 | local-example 125 | local-example/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 netrunnerhq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ⚡️ Implement AWS S3 In Your Next.js App In Under 5 Minutes 3 |

4 | 5 |

6 | Netrunner demo hero gif 7 | 8 |

9 | 10 |

11 | 👩‍💻 Demo 12 |   •   13 | 🦾 Examples  •   15 | 🕸️ Website 16 |   •   17 | 🤝 Contribute 18 |

19 | 20 |

21 | 22 | Netrunner is a batteries included tool to implement secure AWS storage in Next.js apps. Unlike other tools, it also solves the problem of configuring S3 buckets in your AWS account. Here's how it works: 23 | 24 | 1. view the [example](/examples) repositories for both app-and-pages router 25 | 2. create an S3 bucket in the Netrunner dashboard: https://netrunnerhq.com/ 26 | 3. copy the template .env file in your terminal: ```cp .env.template .env.local``` and paste your S3 bucket name and env variables 27 | 4. run ```pnpm dev``` and upload a file! 28 | 29 | ## 🪄 Get started with Next.js and AWS S3 file uploads 30 | 31 | 32 | 33 | Make sure you have a Next.js app and AWS account at the ready. Let's get started! 34 | 35 | ### 🔋 Step 1. Package installation 36 | 37 | First install the Netrunner [npm](https://npmwebsite.com) package to handle file uploads. 38 | 39 | ```console 40 | npm install @netrunner/next-s3 41 | ``` 42 | 43 | ```console 44 | yarn add @netrunner/next-s3 45 | ``` 46 | 47 | ```console 48 | pnpm add @netrunner/nextjs-s3 49 | ``` 50 | 51 | ### 🌱 Step 2. Implement file upload component 52 | 53 | When using app router: 54 | 55 | ```tsx 56 | // app/upload-page.tsx or for page router: pages/upload-page.tsx 57 | import { useFileUpload } from "netrunnerhq/next-s3-upload"; 58 | 59 | export default function UploadComponent() { 60 | const { FileUploadInput, uploadFile } = useFileUpload(); 61 | return ; 62 | } 63 | ``` 64 | 65 | ### 🔌 Step 3. Create a new API route 66 | 67 | ```tsx 68 | // app/upload/route.ts or for page router: pages/api/upload.ts 69 | import { apiSignS3Url } from "@netrunnerhq/client"; 70 | 71 | export default async function handler(req, res) { 72 | if (req.method !== "GET") 73 | // different res for app router 74 | return res.status(405).json({ message: "Method not allowed" }); 75 | 76 | if (!req.query.filename || !req.query.filetype) 77 | return res.status(400).json({ message: "Missing filename or filetype" }); 78 | 79 | try { 80 | const { filename, filetype } = req.query; 81 | const { signed_url } = await apiSignS3Url(filename, filetype); 82 | res.status(200).json({ signed_url }); 83 | } catch (error) { 84 | console.error(error); 85 | res.status(500).json({ error: "An error occurred" }); 86 | } 87 | } 88 | ``` 89 | 90 | ### 🪣 Step 4. Bucket configuration 91 | 92 | - Create an S3 bucket inside your AWS account from the Netrunner app by logging in with GitHub and following the quickstart on [netrunnerhq.com](https://netrunnerhq.com/) 93 | 94 | - Enter your AWS account ID that you can find on the right top of the AWS console 95 | 96 | - Verify in your AWS console if the bucket is deployed correctly in the S3 service [page](https://s3.console.aws.amazon.com/s3/home?region=us-east-1) 97 | 98 |

99 | Netrunner bucket creation gif 100 |

101 | 102 | ### 🎉 Step 5. Environment variables and upload file! 103 | 104 | - Add the environment variables to your .env.local file for the bucket name and region. You can copy them from the Netrunner console. 105 | 106 | - Run your app and or use the [example](https://github.com/netrunnerhq/nextjs-aws-s3/tree/main/example) code in this repository if convenient. 107 | 108 | - Click the upload button to select a test image. The file should upload correctly! 109 | 110 |

111 | Netrunner file upload gif 112 |

113 | 114 | ## 🦾 About Netrunner 115 | 116 | Netrunner is being developed by [Vincent Hus](https://github.com/davincios) with the mission to enable JavaScript software engineers to transform their AWS cloud account into a personalised Firebase developer platform. 117 | 118 | You can learn more by visiting our [website](https://netrunnerhq.com) or ping Vincent on twitter [@jvf_hus](https://twitter.com/vincent_hus) 119 | -------------------------------------------------------------------------------- /cli/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /cli/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | 3 | src/templates/** 4 | /src/templates 5 | -------------------------------------------------------------------------------- /cli/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "oclif", 4 | "oclif-typescript" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /cli/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | day: 'saturday' 8 | versioning-strategy: 'increase' 9 | labels: 10 | - 'dependencies' 11 | open-pull-requests-limit: 5 12 | pull-request-branch-name: 13 | separator: '-' 14 | commit-message: 15 | # cause a release for non-dev-deps 16 | prefix: fix(deps) 17 | # no release for dev-deps 18 | prefix-development: chore(dev-deps) 19 | ignore: 20 | - dependency-name: '@salesforce/dev-scripts' 21 | - dependency-name: '*' 22 | update-types: ['version-update:semver-major'] 23 | -------------------------------------------------------------------------------- /cli/.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '17 2,5,8,11 * * *' 6 | 7 | jobs: 8 | automerge: 9 | uses: oclif/github-workflows/.github/workflows/automerge.yml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /cli/.github/workflows/failureNotifications.yml: -------------------------------------------------------------------------------- 1 | name: failureNotifications 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - version, tag and github release 7 | - publish 8 | types: 9 | - completed 10 | 11 | jobs: 12 | failure-notify: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'failure' }} 15 | steps: 16 | - name: Announce Failure 17 | id: slack 18 | uses: slackapi/slack-github-action@v1.21.0 19 | env: 20 | # for non-CLI-team-owned plugins, you can send this anywhere you like 21 | SLACK_WEBHOOK_URL: ${{ secrets.CLI_ALERTS_SLACK_WEBHOOK }} 22 | SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK 23 | with: 24 | payload: | 25 | { 26 | "text": "${{ github.event.workflow_run.name }} failed: ${{ github.event.workflow_run.repository.name }}", 27 | "blocks": [ 28 | { 29 | "type": "header", 30 | "text": { 31 | "type": "plain_text", 32 | "text": ":bh-alert: ${{ github.event.workflow_run.name }} failed: ${{ github.event.workflow_run.repository.name }} :bh-alert:" 33 | } 34 | }, 35 | { 36 | "type": "section", 37 | "text": { 38 | "type": "mrkdwn", 39 | "text": "Repo: ${{ github.event.workflow_run.repository.html_url }}\nWorkflow name: `${{ github.event.workflow_run.name }}`\nJob url: ${{ github.event.workflow_run.html_url }}" 40 | } 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /cli/.github/workflows/manualRelease.yml: -------------------------------------------------------------------------------- 1 | name: manual release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | with: 12 | token: ${{ secrets.SVC_CLI_BOT_GITHUB_TOKEN }} 13 | - name: Conventional Changelog Action 14 | id: changelog 15 | uses: TriPSs/conventional-changelog-action@d360fad3a42feca6462f72c97c165d60a02d4bf2 16 | # overriding some of the basic behaviors to just get the changelog 17 | with: 18 | git-user-name: svc-cli-bot 19 | git-user-email: svc_cli_bot@salesforce.com 20 | github-token: ${{ secrets.SVC_CLI_BOT_GITHUB_TOKEN }} 21 | output-file: false 22 | # always do the release, even if there are no semantic commits 23 | skip-on-empty: false 24 | tag-prefix: '' 25 | - uses: notiz-dev/github-action-json-property@2192e246737701f108a4571462b76c75e7376216 26 | id: packageVersion 27 | with: 28 | path: 'package.json' 29 | prop_path: 'version' 30 | - name: Create Github Release 31 | uses: actions/create-release@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.SVC_CLI_BOT_GITHUB_TOKEN }} 34 | with: 35 | tag_name: ${{ steps.packageVersion.outputs.prop }} 36 | release_name: ${{ steps.packageVersion.outputs.prop }} 37 | -------------------------------------------------------------------------------- /cli/.github/workflows/notify-slack-on-pr-open.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Slack Notification 2 | 3 | on: 4 | pull_request: 5 | types: [opened, reopened] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Notify Slack on PR open 12 | env: 13 | WEBHOOK_URL : ${{ secrets.CLI_TEAM_SLACK_WEBHOOK_URL }} 14 | PULL_REQUEST_AUTHOR_ICON_URL : ${{ github.event.pull_request.user.avatar_url }} 15 | PULL_REQUEST_AUTHOR_NAME : ${{ github.event.pull_request.user.login }} 16 | PULL_REQUEST_AUTHOR_PROFILE_URL: ${{ github.event.pull_request.user.html_url }} 17 | PULL_REQUEST_BASE_BRANCH_NAME : ${{ github.event.pull_request.base.ref }} 18 | PULL_REQUEST_COMPARE_BRANCH_NAME : ${{ github.event.pull_request.head.ref }} 19 | PULL_REQUEST_NUMBER : ${{ github.event.pull_request.number }} 20 | PULL_REQUEST_REPO: ${{ github.event.pull_request.head.repo.name }} 21 | PULL_REQUEST_TITLE : ${{ github.event.pull_request.title }} 22 | PULL_REQUEST_URL : ${{ github.event.pull_request.html_url }} 23 | uses: salesforcecli/github-workflows/.github/actions/prNotification@main 24 | -------------------------------------------------------------------------------- /cli/.github/workflows/onPushToMain.yml: -------------------------------------------------------------------------------- 1 | # test 2 | name: version, tag and github release 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | release: 10 | uses: oclif/github-workflows/.github/workflows/githubRelease.yml@main 11 | secrets: inherit 12 | 13 | # most repos won't use this 14 | # depends on previous job to avoid git collisions, not for any functionality reason 15 | # docs: 16 | # uses: salesforcecli/github-workflows/.github/workflows/publishTypedoc.yml@main 17 | # secrets: inherit 18 | # needs: release 19 | -------------------------------------------------------------------------------- /cli/.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [released] 6 | # support manual release in case something goes wrong and needs to be repeated or tested 7 | workflow_dispatch: 8 | inputs: 9 | tag: 10 | description: tag that needs to publish 11 | type: string 12 | required: true 13 | jobs: 14 | npm: 15 | uses: oclif/github-workflows/.github/workflows/npmPublish.yml@main 16 | with: 17 | tag: latest 18 | githubTag: ${{ github.event.release.tag_name || inputs.tag }} 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /cli/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: 3 | push: 4 | branches-ignore: [main] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | unit-tests: 9 | uses: oclif/github-workflows/.github/workflows/unitTest.yml@main 10 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /dist 5 | /lib 6 | /package-lock.json 7 | /tmp 8 | /yarn.lock 9 | node_modules 10 | oclif.manifest.json 11 | pages 12 | 13 | .env.local 14 | 15 | # local env files 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local -------------------------------------------------------------------------------- /cli/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "test/helpers/init.js", 4 | "ts-node/register" 5 | ], 6 | "watch-extensions": [ 7 | "ts" 8 | ], 9 | "recursive": true, 10 | "reporter": "spec", 11 | "timeout": 60000 12 | } 13 | -------------------------------------------------------------------------------- /cli/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Execute Command", 8 | "skipFiles": [ 9 | "/**" 10 | ], 11 | "program": "${workspaceFolder}/bin/dev", 12 | "args": [ 13 | "hello", 14 | "world", 15 | ], 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /cli/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development Notes 2 | 3 | ### 4 | 5 | Add command: 6 | 7 | ```shell 8 | oclif generate command NAME 9 | ``` 10 | 11 | Add hook: 12 | 13 | ```shell 14 | oclif generate hook NAME 15 | ``` 16 | 17 | ### Todos May 28th 18 | 19 | #### [A] Adjust the CL function: 20 | 21 | 1. Customer does init command via the CLI. This CLI creates a custom role in the customer's account. 22 | 2. The CLI stores the signup parameters in a users table in Xata to keep developing fast. For the API call we need (targetAccountId, roleName, targetExternalId, targetRegion). 23 | 3. Once the role has been created, a second request is executed (all in the same API call) to create the bucket in the target account. This bucket name is also stored in the users table database 24 | —------- 25 | 26 | #### [B] NextJS API: 27 | 28 | 4. On the front-end web application, we make an API call to the users table, and check if there is a key present for the user’s bucket. If that key is present, a second request is made with an assumed role to check if the bucket exists (with the head http method). 29 | 5. If the bucket exists in the other account, we return “bucket verified status” 30 | 31 | ## AWS Switching Profile Notes 32 | 33 | - aws sts get-caller-identity 34 | - 35 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # oclif-hello-world 2 | 3 | oclif example Hello World CLI 4 | 5 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 6 | [![Version](https://img.shields.io/npm/v/oclif-hello-world.svg)](https://npmjs.org/package/oclif-hello-world) 7 | [![CircleCI](https://circleci.com/gh/oclif/hello-world/tree/main.svg?style=shield)](https://circleci.com/gh/oclif/hello-world/tree/main) 8 | [![Downloads/week](https://img.shields.io/npm/dw/oclif-hello-world.svg)](https://npmjs.org/package/oclif-hello-world) 9 | [![License](https://img.shields.io/npm/l/oclif-hello-world.svg)](https://github.com/oclif/hello-world/blob/main/package.json) 10 | 11 | 12 | * [oclif-hello-world](#oclif-hello-world) 13 | * [Usage](#usage) 14 | * [Commands](#commands) 15 | 16 | 17 | # Usage 18 | 19 | 20 | ```sh-session 21 | $ npm install -g @storengine/cli 22 | $ storengine COMMAND 23 | running command... 24 | $ storengine (--version) 25 | @storengine/cli/0.0.9 darwin-arm64 node-v18.15.0 26 | $ storengine --help [COMMAND] 27 | USAGE 28 | $ storengine COMMAND 29 | ... 30 | ``` 31 | 32 | 33 | # Commands 34 | 35 | 36 | * [`storengine cross-acc`](#storengine-cross-acc) 37 | * [`storengine help [COMMANDS]`](#storengine-help-commands) 38 | * [`storengine init USERNAME TEMPLATEID`](#storengine-init-username-templateid) 39 | * [`storengine plugins`](#storengine-plugins) 40 | * [`storengine plugins:install PLUGIN...`](#storengine-pluginsinstall-plugin) 41 | * [`storengine plugins:inspect PLUGIN...`](#storengine-pluginsinspect-plugin) 42 | * [`storengine plugins:install PLUGIN...`](#storengine-pluginsinstall-plugin-1) 43 | * [`storengine plugins:link PLUGIN`](#storengine-pluginslink-plugin) 44 | * [`storengine plugins:uninstall PLUGIN...`](#storengine-pluginsuninstall-plugin) 45 | * [`storengine plugins:uninstall PLUGIN...`](#storengine-pluginsuninstall-plugin-1) 46 | * [`storengine plugins:uninstall PLUGIN...`](#storengine-pluginsuninstall-plugin-2) 47 | * [`storengine plugins update`](#storengine-plugins-update) 48 | 49 | ## `storengine cross-acc` 50 | 51 | ``` 52 | USAGE 53 | $ storengine cross-acc 54 | ``` 55 | 56 | _See code: [dist/commands/cross-acc.ts](https://github.com/davincios/storengine/blob/v0.0.9/dist/commands/cross-acc.ts)_ 57 | 58 | ## `storengine help [COMMANDS]` 59 | 60 | Display help for storengine. 61 | 62 | ``` 63 | USAGE 64 | $ storengine help [COMMANDS] [-n] 65 | 66 | ARGUMENTS 67 | COMMANDS Command to show help for. 68 | 69 | FLAGS 70 | -n, --nested-commands Include all nested commands in the output. 71 | 72 | DESCRIPTION 73 | Display help for storengine. 74 | ``` 75 | 76 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.2.9/src/commands/help.ts)_ 77 | 78 | ## `storengine init USERNAME TEMPLATEID` 79 | 80 | Creates an upload demo in your project, consisting of an API route and page component. 81 | 82 | ``` 83 | USAGE 84 | $ storengine init USERNAME TEMPLATEID 85 | 86 | ARGUMENTS 87 | USERNAME Your username 88 | TEMPLATEID The ID of the template to use 89 | 90 | DESCRIPTION 91 | Creates an upload demo in your project, consisting of an API route and page component. 92 | ``` 93 | 94 | _See code: [dist/commands/init/index.ts](https://github.com/davincios/storengine/blob/v0.0.9/dist/commands/init/index.ts)_ 95 | 96 | ## `storengine plugins` 97 | 98 | List installed plugins. 99 | 100 | ``` 101 | USAGE 102 | $ storengine plugins [--core] 103 | 104 | FLAGS 105 | --core Show core plugins. 106 | 107 | DESCRIPTION 108 | List installed plugins. 109 | 110 | EXAMPLES 111 | $ storengine plugins 112 | ``` 113 | 114 | _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v2.4.7/src/commands/plugins/index.ts)_ 115 | 116 | ## `storengine plugins:install PLUGIN...` 117 | 118 | Installs a plugin into the CLI. 119 | 120 | ``` 121 | USAGE 122 | $ storengine plugins:install PLUGIN... 123 | 124 | ARGUMENTS 125 | PLUGIN Plugin to install. 126 | 127 | FLAGS 128 | -f, --force Run yarn install with force flag. 129 | -h, --help Show CLI help. 130 | -v, --verbose 131 | 132 | DESCRIPTION 133 | Installs a plugin into the CLI. 134 | Can be installed from npm or a git url. 135 | 136 | Installation of a user-installed plugin will override a core plugin. 137 | 138 | e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command 139 | will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in 140 | the CLI without the need to patch and update the whole CLI. 141 | 142 | 143 | ALIASES 144 | $ storengine plugins add 145 | 146 | EXAMPLES 147 | $ storengine plugins:install myplugin 148 | 149 | $ storengine plugins:install https://github.com/someuser/someplugin 150 | 151 | $ storengine plugins:install someuser/someplugin 152 | ``` 153 | 154 | ## `storengine plugins:inspect PLUGIN...` 155 | 156 | Displays installation properties of a plugin. 157 | 158 | ``` 159 | USAGE 160 | $ storengine plugins:inspect PLUGIN... 161 | 162 | ARGUMENTS 163 | PLUGIN [default: .] Plugin to inspect. 164 | 165 | FLAGS 166 | -h, --help Show CLI help. 167 | -v, --verbose 168 | 169 | GLOBAL FLAGS 170 | --json Format output as json. 171 | 172 | DESCRIPTION 173 | Displays installation properties of a plugin. 174 | 175 | EXAMPLES 176 | $ storengine plugins:inspect myplugin 177 | ``` 178 | 179 | ## `storengine plugins:install PLUGIN...` 180 | 181 | Installs a plugin into the CLI. 182 | 183 | ``` 184 | USAGE 185 | $ storengine plugins:install PLUGIN... 186 | 187 | ARGUMENTS 188 | PLUGIN Plugin to install. 189 | 190 | FLAGS 191 | -f, --force Run yarn install with force flag. 192 | -h, --help Show CLI help. 193 | -v, --verbose 194 | 195 | DESCRIPTION 196 | Installs a plugin into the CLI. 197 | Can be installed from npm or a git url. 198 | 199 | Installation of a user-installed plugin will override a core plugin. 200 | 201 | e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command 202 | will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in 203 | the CLI without the need to patch and update the whole CLI. 204 | 205 | 206 | ALIASES 207 | $ storengine plugins add 208 | 209 | EXAMPLES 210 | $ storengine plugins:install myplugin 211 | 212 | $ storengine plugins:install https://github.com/someuser/someplugin 213 | 214 | $ storengine plugins:install someuser/someplugin 215 | ``` 216 | 217 | ## `storengine plugins:link PLUGIN` 218 | 219 | Links a plugin into the CLI for development. 220 | 221 | ``` 222 | USAGE 223 | $ storengine plugins:link PLUGIN 224 | 225 | ARGUMENTS 226 | PATH [default: .] path to plugin 227 | 228 | FLAGS 229 | -h, --help Show CLI help. 230 | -v, --verbose 231 | 232 | DESCRIPTION 233 | Links a plugin into the CLI for development. 234 | Installation of a linked plugin will override a user-installed or core plugin. 235 | 236 | e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello' 237 | command will override the user-installed or core plugin implementation. This is useful for development work. 238 | 239 | 240 | EXAMPLES 241 | $ storengine plugins:link myplugin 242 | ``` 243 | 244 | ## `storengine plugins:uninstall PLUGIN...` 245 | 246 | Removes a plugin from the CLI. 247 | 248 | ``` 249 | USAGE 250 | $ storengine plugins:uninstall PLUGIN... 251 | 252 | ARGUMENTS 253 | PLUGIN plugin to uninstall 254 | 255 | FLAGS 256 | -h, --help Show CLI help. 257 | -v, --verbose 258 | 259 | DESCRIPTION 260 | Removes a plugin from the CLI. 261 | 262 | ALIASES 263 | $ storengine plugins unlink 264 | $ storengine plugins remove 265 | ``` 266 | 267 | ## `storengine plugins:uninstall PLUGIN...` 268 | 269 | Removes a plugin from the CLI. 270 | 271 | ``` 272 | USAGE 273 | $ storengine plugins:uninstall PLUGIN... 274 | 275 | ARGUMENTS 276 | PLUGIN plugin to uninstall 277 | 278 | FLAGS 279 | -h, --help Show CLI help. 280 | -v, --verbose 281 | 282 | DESCRIPTION 283 | Removes a plugin from the CLI. 284 | 285 | ALIASES 286 | $ storengine plugins unlink 287 | $ storengine plugins remove 288 | ``` 289 | 290 | ## `storengine plugins:uninstall PLUGIN...` 291 | 292 | Removes a plugin from the CLI. 293 | 294 | ``` 295 | USAGE 296 | $ storengine plugins:uninstall PLUGIN... 297 | 298 | ARGUMENTS 299 | PLUGIN plugin to uninstall 300 | 301 | FLAGS 302 | -h, --help Show CLI help. 303 | -v, --verbose 304 | 305 | DESCRIPTION 306 | Removes a plugin from the CLI. 307 | 308 | ALIASES 309 | $ storengine plugins unlink 310 | $ storengine plugins remove 311 | ``` 312 | 313 | ## `storengine plugins update` 314 | 315 | Update installed plugins. 316 | 317 | ``` 318 | USAGE 319 | $ storengine plugins update [-h] [-v] 320 | 321 | FLAGS 322 | -h, --help Show CLI help. 323 | -v, --verbose 324 | 325 | DESCRIPTION 326 | Update installed plugins. 327 | ``` 328 | 329 | -------------------------------------------------------------------------------- /cli/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | const path = require('path') 6 | const project = path.join(__dirname, '..', 'tsconfig.json') 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = 'development' 10 | 11 | require('ts-node').register({project}) 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle) 18 | -------------------------------------------------------------------------------- /cli/bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /cli/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core') 4 | 5 | oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) 6 | -------------------------------------------------------------------------------- /cli/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /cli/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "dist/infra/main.js", 3 | "context": { 4 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 5 | "@aws-cdk/core:checkSecretUsage": true, 6 | "@aws-cdk/core:target-partitions": [ 7 | "aws", 8 | "aws-cn" 9 | ], 10 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 11 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 12 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 13 | "@aws-cdk/aws-iam:minimizePolicies": true, 14 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 15 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 16 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 17 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 18 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 19 | "@aws-cdk/core:enablePartitionLiterals": true, 20 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 21 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 22 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 23 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 24 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 25 | "@aws-cdk/aws-route53-patters:useCertificate": true, 26 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 27 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 28 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 29 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 30 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 31 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 32 | "@aws-cdk/aws-redshift:columnId": true, 33 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 34 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 35 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true 36 | } 37 | } -------------------------------------------------------------------------------- /cli/oclif.md: -------------------------------------------------------------------------------- 1 | # oclif-hello-world 2 | 3 | oclif example Hello World CLI 4 | 5 | [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io) 6 | [![Version](https://img.shields.io/npm/v/oclif-hello-world.svg)](https://npmjs.org/package/oclif-hello-world) 7 | [![CircleCI](https://circleci.com/gh/oclif/hello-world/tree/main.svg?style=shield)](https://circleci.com/gh/oclif/hello-world/tree/main) 8 | [![Downloads/week](https://img.shields.io/npm/dw/oclif-hello-world.svg)](https://npmjs.org/package/oclif-hello-world) 9 | [![License](https://img.shields.io/npm/l/oclif-hello-world.svg)](https://github.com/oclif/hello-world/blob/main/package.json) 10 | 11 | 12 | * [oclif-hello-world](#oclif-hello-world) 13 | * [Usage](#usage) 14 | * [Commands](#commands) 15 | 16 | 17 | # Usage 18 | 19 | 20 | ```sh-session 21 | $ npm install -g @storengine/cli 22 | $ storengine COMMAND 23 | running command... 24 | $ storengine (--version) 25 | @storengine/cli/0.0.9 darwin-arm64 node-v18.15.0 26 | $ storengine --help [COMMAND] 27 | USAGE 28 | $ storengine COMMAND 29 | ... 30 | ``` 31 | 32 | 33 | # Commands 34 | 35 | 36 | * [`storengine cross-acc`](#storengine-cross-acc) 37 | * [`storengine help [COMMANDS]`](#storengine-help-commands) 38 | * [`storengine init USERNAME TEMPLATEID`](#storengine-init-username-templateid) 39 | * [`storengine plugins`](#storengine-plugins) 40 | * [`storengine plugins:install PLUGIN...`](#storengine-pluginsinstall-plugin) 41 | * [`storengine plugins:inspect PLUGIN...`](#storengine-pluginsinspect-plugin) 42 | * [`storengine plugins:install PLUGIN...`](#storengine-pluginsinstall-plugin-1) 43 | * [`storengine plugins:link PLUGIN`](#storengine-pluginslink-plugin) 44 | * [`storengine plugins:uninstall PLUGIN...`](#storengine-pluginsuninstall-plugin) 45 | * [`storengine plugins:uninstall PLUGIN...`](#storengine-pluginsuninstall-plugin-1) 46 | * [`storengine plugins:uninstall PLUGIN...`](#storengine-pluginsuninstall-plugin-2) 47 | * [`storengine plugins update`](#storengine-plugins-update) 48 | 49 | ## `storengine cross-acc` 50 | 51 | ``` 52 | USAGE 53 | $ storengine cross-acc 54 | ``` 55 | 56 | _See code: [dist/commands/cross-acc.ts](https://github.com/davincios/storengine/blob/v0.0.9/dist/commands/cross-acc.ts)_ 57 | 58 | ## `storengine help [COMMANDS]` 59 | 60 | Display help for storengine. 61 | 62 | ``` 63 | USAGE 64 | $ storengine help [COMMANDS] [-n] 65 | 66 | ARGUMENTS 67 | COMMANDS Command to show help for. 68 | 69 | FLAGS 70 | -n, --nested-commands Include all nested commands in the output. 71 | 72 | DESCRIPTION 73 | Display help for storengine. 74 | ``` 75 | 76 | _See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v5.2.9/src/commands/help.ts)_ 77 | 78 | ## `storengine init USERNAME TEMPLATEID` 79 | 80 | Creates an upload demo in your project, consisting of an API route and page component. 81 | 82 | ``` 83 | USAGE 84 | $ storengine init USERNAME TEMPLATEID 85 | 86 | ARGUMENTS 87 | USERNAME Your username 88 | TEMPLATEID The ID of the template to use 89 | 90 | DESCRIPTION 91 | Creates an upload demo in your project, consisting of an API route and page component. 92 | ``` 93 | 94 | _See code: [dist/commands/init/index.ts](https://github.com/davincios/storengine/blob/v0.0.9/dist/commands/init/index.ts)_ 95 | 96 | ## `storengine plugins` 97 | 98 | List installed plugins. 99 | 100 | ``` 101 | USAGE 102 | $ storengine plugins [--core] 103 | 104 | FLAGS 105 | --core Show core plugins. 106 | 107 | DESCRIPTION 108 | List installed plugins. 109 | 110 | EXAMPLES 111 | $ storengine plugins 112 | ``` 113 | 114 | _See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v2.4.7/src/commands/plugins/index.ts)_ 115 | 116 | ## `storengine plugins:install PLUGIN...` 117 | 118 | Installs a plugin into the CLI. 119 | 120 | ``` 121 | USAGE 122 | $ storengine plugins:install PLUGIN... 123 | 124 | ARGUMENTS 125 | PLUGIN Plugin to install. 126 | 127 | FLAGS 128 | -f, --force Run yarn install with force flag. 129 | -h, --help Show CLI help. 130 | -v, --verbose 131 | 132 | DESCRIPTION 133 | Installs a plugin into the CLI. 134 | Can be installed from npm or a git url. 135 | 136 | Installation of a user-installed plugin will override a core plugin. 137 | 138 | e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command 139 | will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in 140 | the CLI without the need to patch and update the whole CLI. 141 | 142 | 143 | ALIASES 144 | $ storengine plugins add 145 | 146 | EXAMPLES 147 | $ storengine plugins:install myplugin 148 | 149 | $ storengine plugins:install https://github.com/someuser/someplugin 150 | 151 | $ storengine plugins:install someuser/someplugin 152 | ``` 153 | 154 | ## `storengine plugins:inspect PLUGIN...` 155 | 156 | Displays installation properties of a plugin. 157 | 158 | ``` 159 | USAGE 160 | $ storengine plugins:inspect PLUGIN... 161 | 162 | ARGUMENTS 163 | PLUGIN [default: .] Plugin to inspect. 164 | 165 | FLAGS 166 | -h, --help Show CLI help. 167 | -v, --verbose 168 | 169 | GLOBAL FLAGS 170 | --json Format output as json. 171 | 172 | DESCRIPTION 173 | Displays installation properties of a plugin. 174 | 175 | EXAMPLES 176 | $ storengine plugins:inspect myplugin 177 | ``` 178 | 179 | ## `storengine plugins:install PLUGIN...` 180 | 181 | Installs a plugin into the CLI. 182 | 183 | ``` 184 | USAGE 185 | $ storengine plugins:install PLUGIN... 186 | 187 | ARGUMENTS 188 | PLUGIN Plugin to install. 189 | 190 | FLAGS 191 | -f, --force Run yarn install with force flag. 192 | -h, --help Show CLI help. 193 | -v, --verbose 194 | 195 | DESCRIPTION 196 | Installs a plugin into the CLI. 197 | Can be installed from npm or a git url. 198 | 199 | Installation of a user-installed plugin will override a core plugin. 200 | 201 | e.g. If you have a core plugin that has a 'hello' command, installing a user-installed plugin with a 'hello' command 202 | will override the core plugin implementation. This is useful if a user needs to update core plugin functionality in 203 | the CLI without the need to patch and update the whole CLI. 204 | 205 | 206 | ALIASES 207 | $ storengine plugins add 208 | 209 | EXAMPLES 210 | $ storengine plugins:install myplugin 211 | 212 | $ storengine plugins:install https://github.com/someuser/someplugin 213 | 214 | $ storengine plugins:install someuser/someplugin 215 | ``` 216 | 217 | ## `storengine plugins:link PLUGIN` 218 | 219 | Links a plugin into the CLI for development. 220 | 221 | ``` 222 | USAGE 223 | $ storengine plugins:link PLUGIN 224 | 225 | ARGUMENTS 226 | PATH [default: .] path to plugin 227 | 228 | FLAGS 229 | -h, --help Show CLI help. 230 | -v, --verbose 231 | 232 | DESCRIPTION 233 | Links a plugin into the CLI for development. 234 | Installation of a linked plugin will override a user-installed or core plugin. 235 | 236 | e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello' 237 | command will override the user-installed or core plugin implementation. This is useful for development work. 238 | 239 | 240 | EXAMPLES 241 | $ storengine plugins:link myplugin 242 | ``` 243 | 244 | ## `storengine plugins:uninstall PLUGIN...` 245 | 246 | Removes a plugin from the CLI. 247 | 248 | ``` 249 | USAGE 250 | $ storengine plugins:uninstall PLUGIN... 251 | 252 | ARGUMENTS 253 | PLUGIN plugin to uninstall 254 | 255 | FLAGS 256 | -h, --help Show CLI help. 257 | -v, --verbose 258 | 259 | DESCRIPTION 260 | Removes a plugin from the CLI. 261 | 262 | ALIASES 263 | $ storengine plugins unlink 264 | $ storengine plugins remove 265 | ``` 266 | 267 | ## `storengine plugins:uninstall PLUGIN...` 268 | 269 | Removes a plugin from the CLI. 270 | 271 | ``` 272 | USAGE 273 | $ storengine plugins:uninstall PLUGIN... 274 | 275 | ARGUMENTS 276 | PLUGIN plugin to uninstall 277 | 278 | FLAGS 279 | -h, --help Show CLI help. 280 | -v, --verbose 281 | 282 | DESCRIPTION 283 | Removes a plugin from the CLI. 284 | 285 | ALIASES 286 | $ storengine plugins unlink 287 | $ storengine plugins remove 288 | ``` 289 | 290 | ## `storengine plugins:uninstall PLUGIN...` 291 | 292 | Removes a plugin from the CLI. 293 | 294 | ``` 295 | USAGE 296 | $ storengine plugins:uninstall PLUGIN... 297 | 298 | ARGUMENTS 299 | PLUGIN plugin to uninstall 300 | 301 | FLAGS 302 | -h, --help Show CLI help. 303 | -v, --verbose 304 | 305 | DESCRIPTION 306 | Removes a plugin from the CLI. 307 | 308 | ALIASES 309 | $ storengine plugins unlink 310 | $ storengine plugins remove 311 | ``` 312 | 313 | ## `storengine plugins update` 314 | 315 | Update installed plugins. 316 | 317 | ``` 318 | USAGE 319 | $ storengine plugins update [-h] [-v] 320 | 321 | FLAGS 322 | -h, --help Show CLI help. 323 | -v, --verbose 324 | 325 | DESCRIPTION 326 | Update installed plugins. 327 | ``` 328 | 329 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storengine/cli", 3 | "version": "0.0.9", 4 | "description": "storengine CLI", 5 | "author": "Vincent Hus @davincios", 6 | "bin": { 7 | "storengine": "bin/run" 8 | }, 9 | "homepage": "https://github.com/davincios/storengine", 10 | "license": "UNLICENSED", 11 | "main": "dist/index.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/davincios/storengine.git" 15 | }, 16 | "files": [ 17 | "/bin", 18 | "/dist", 19 | "/npm-shrinkwrap.json", 20 | "/oclif.manifest.json" 21 | ], 22 | "dependencies": { 23 | "@aws-sdk/client-iam": "3.354.0", 24 | "@oclif/core": "^2", 25 | "@oclif/plugin-help": "^5", 26 | "@oclif/plugin-plugins": "^2.4.6", 27 | "dotenv": "^16.0.3", 28 | "install": "^0.13.0", 29 | "log-symbols": "^5.1.0", 30 | "npm": "^9.7.1" 31 | }, 32 | "devDependencies": { 33 | "@oclif/test": "^2.3.16", 34 | "@types/chai": "^4", 35 | "@types/mocha": "^9.0.0", 36 | "@types/node": "^16.18.24", 37 | "chai": "^4", 38 | "eslint": "^7.32.0", 39 | "eslint-config-oclif": "^4", 40 | "eslint-config-oclif-typescript": "^1.0.3", 41 | "mocha": "^9", 42 | "oclif": "^3", 43 | "shx": "^0.3.3", 44 | "ts-node": "^10.9.1", 45 | "tslib": "^2.5.0", 46 | "typescript": "^5.0" 47 | }, 48 | "oclif": { 49 | "bin": "storengine", 50 | "dirname": "storengine", 51 | "commands": "./dist/commands", 52 | "plugins": [ 53 | "@oclif/plugin-help", 54 | "@oclif/plugin-plugins" 55 | ], 56 | "topicSeparator": " ", 57 | "topics": { 58 | "hello": { 59 | "description": "Say hello to the world and others" 60 | } 61 | } 62 | }, 63 | "scripts": { 64 | "build": "shx rm -rf dist && tsc -b", 65 | "postpack": "shx rm -f oclif.manifest.json", 66 | "prepack": "npm run build && oclif manifest && oclif readme", 67 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 68 | "version": "oclif readme && git add README.md", 69 | "init": "npm run build && node ./bin/run init 6494cc4dde7a3181e1aa6a71", 70 | "setup": "npm install" 71 | }, 72 | "engines": { 73 | "node": ">=12.0.0" 74 | }, 75 | "bugs": { 76 | "url": "https://github.com/davincios/storengine/issues" 77 | }, 78 | "keywords": [ 79 | "oclif" 80 | ], 81 | "types": "dist/index.d.ts", 82 | "directories": { 83 | "test": "test" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cli/src/assume-role.ts: -------------------------------------------------------------------------------- 1 | import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; 2 | import { 3 | IAwsAccountConfig, 4 | getEnvAwsAccountConfig, 5 | } from "./configure-aws-client"; 6 | 7 | export async function assumeRole( 8 | targetAccountID: string, 9 | targetRoleName: string, 10 | targetExternalId: string, 11 | targetRegion: string 12 | ): Promise { 13 | if (!targetAccountID || !targetRoleName || !targetExternalId) { 14 | throw new Error( 15 | "Missing required input. Please provide targetAccountID, targetRoleName, and targetExternalId." 16 | ); 17 | } 18 | 19 | const uniqueSessionName = `AssumedRoleSession_${Date.now()}`; 20 | 21 | const assumeRoleParams = { 22 | RoleArn: `arn:aws:iam::${targetAccountID}:role/${targetRoleName}`, 23 | RoleSessionName: uniqueSessionName, 24 | ExternalId: targetExternalId, 25 | }; 26 | 27 | const config = await getEnvAwsAccountConfig(); 28 | const stsClient = new STSClient(config); 29 | 30 | try { 31 | const assumeRoleCommand = new AssumeRoleCommand(assumeRoleParams); 32 | const data = await stsClient.send(assumeRoleCommand); 33 | const assumedCredentials = data.Credentials; 34 | 35 | return { 36 | region: `${targetRegion}`, // Replace with your desired AWS region 37 | credentials: { 38 | accessKeyId: `${assumedCredentials?.AccessKeyId}`, 39 | secretAccessKey: `${assumedCredentials?.SecretAccessKey}`, 40 | sessionToken: `${assumedCredentials?.SessionToken}`, 41 | }, 42 | }; 43 | } catch (err) { 44 | console.error("Error assuming role:", err); 45 | throw err; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cli/src/commands/init.ts: -------------------------------------------------------------------------------- 1 | import { Command, Args } from "@oclif/core"; 2 | import { createIamRoleInTargetAccount } from "../iam/index"; 3 | import { getEnvAwsAccountConfig } from "../configure-aws-client"; 4 | 5 | import { createInitFiles } from "../create-files"; 6 | import { updateUser } from "../update-user"; 7 | import { launchBucketApi } from "../launch-bucket"; 8 | import { fetchUserById } from "../fetch-user"; 9 | 10 | import { DEFAULT_ROLE_NAME, STATUS_IAM_ROLE_CREATED } from "../constants/index"; 11 | 12 | export default class Init extends Command { 13 | static description = 14 | "Creates an upload demo in your nextjs project, consisting of an API route and upload demo page component."; 15 | 16 | static args = { 17 | userId: Args.string({ 18 | name: "userId", 19 | required: true, 20 | description: "The ID of the user", 21 | }), 22 | }; 23 | 24 | public async run(): Promise { 25 | try { 26 | const { args } = await this.parse(Init); 27 | 28 | const user = await fetchUserById(args.userId); 29 | const localAwsClientConfigForTarget = await getEnvAwsAccountConfig(); 30 | 31 | const { userId, trustId, quickStartBucketName } = user; 32 | 33 | if (!quickStartBucketName) throw new Error("No quickstartbucket found"); 34 | if (!userId) throw new Error("No user id found in the retrieved user"); 35 | if (!trustId) throw new Error("No trust id found in the retrieved user"); 36 | 37 | await createIamRoleInTargetAccount({ 38 | targetAccountClientConfig: localAwsClientConfigForTarget, 39 | targetNewRoleName: DEFAULT_ROLE_NAME, 40 | trustId: trustId, 41 | }); 42 | 43 | await updateUser(userId, { 44 | quickStartStatus: STATUS_IAM_ROLE_CREATED, 45 | targetAccountId: localAwsClientConfigForTarget.accountId, 46 | targetRoleName: DEFAULT_ROLE_NAME, 47 | iamRegion: localAwsClientConfigForTarget.region, 48 | }); 49 | 50 | const reponse = await launchBucketApi({ 51 | userId: userId, 52 | bucketParams: { 53 | region: localAwsClientConfigForTarget.region, 54 | templateName: "FILE_UPLOADS", 55 | serviceName: "quickstart-netrunner", 56 | }, 57 | }); 58 | console.log("[launchBucketApi] response", reponse); 59 | await createInitFiles(quickStartBucketName); 60 | } catch (error) { 61 | console.error(`Error in Init: ${error}`); 62 | throw error; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cli/src/configure-aws-client.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts"; 3 | 4 | dotenv.config({ path: `.env.local`, override: true }); 5 | 6 | export async function getLocalEnvAWSAccountId(): Promise { 7 | try { 8 | const stsClient = new STSClient({}); 9 | const command = new GetCallerIdentityCommand({}); 10 | const response = await stsClient.send(command); 11 | return response.Account ?? ""; 12 | } catch (error) { 13 | console.error("Error retrieving default account ID:", error); 14 | throw error; 15 | } 16 | } 17 | 18 | export interface IAwsAccountConfig { 19 | region: string; 20 | accountId?: string; 21 | credentials: { 22 | accessKeyId: string; 23 | secretAccessKey: string; 24 | sessionToken?: string; 25 | }; 26 | } 27 | 28 | function validateEnvVar(varName: string, varValue: string | undefined): void { 29 | if (!varValue) throw new Error(`${varName} must be set in the environment`); 30 | } 31 | 32 | export const getEnvAwsAccountConfig = async (): Promise => { 33 | const { 34 | AWS_ACCESS_KEY_ID, 35 | AWS_SECRET_ACCESS_KEY, 36 | AWS_REGION, 37 | AWS_ACCOUNT_ID: accountIdFromEnv, 38 | } = process.env; 39 | 40 | validateEnvVar("AWS_ACCESS_KEY_ID", AWS_ACCESS_KEY_ID); 41 | validateEnvVar("AWS_SECRET_ACCESS_KEY", AWS_SECRET_ACCESS_KEY); 42 | validateEnvVar("AWS_REGION", AWS_REGION); 43 | 44 | const AWS_ACCOUNT_ID = accountIdFromEnv ?? (await getLocalEnvAWSAccountId()); 45 | validateEnvVar("AWS_ACCOUNT_ID", AWS_ACCOUNT_ID); 46 | 47 | return { 48 | region: "" + AWS_REGION, 49 | accountId: "" + AWS_ACCOUNT_ID, 50 | credentials: { 51 | accessKeyId: "" + AWS_ACCESS_KEY_ID, 52 | secretAccessKey: "" + AWS_SECRET_ACCESS_KEY, 53 | }, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /cli/src/constants/_code-gen-presigned-api.ts: -------------------------------------------------------------------------------- 1 | export const TEMPLATE_API = ` 2 | import { NextApiHandler } from "next"; 3 | import { apiSignS3Url } from "@storengine/client"; 4 | 5 | const handler: NextApiHandler = async (req, res) => { 6 | if (req.method !== "GET") 7 | return res.status(405).json({ message: "Method not allowed" }); 8 | 9 | try { 10 | const { filename, filetype } = req.query; 11 | const { signed_url } = await apiSignS3Url(filename, filetype); 12 | res.status(200).json({ signed_url }); 13 | } catch (error) { 14 | console.error(error); 15 | res.status(500).json({ error: error.message }); 16 | } 17 | }; 18 | 19 | export default handler; 20 | `; 21 | -------------------------------------------------------------------------------- /cli/src/constants/_code-gen-upload-component.ts: -------------------------------------------------------------------------------- 1 | export const TEMPLATE_UPLOAD_COMPONENT = ` 2 | 3 | import { useFileUpload } from "@storengine/client"; 4 | 5 | export default function UploadComponent() { 6 | const { FileUploadInput, uploadFile } = useFileUpload(); 7 | // @ts-ignore 8 | return ; 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /cli/src/constants/file-names.ts: -------------------------------------------------------------------------------- 1 | export const API_FILE_NAME = "upload-presigned.ts"; 2 | export const PAGES_FILE_NAME = "upload-demo.tsx"; 3 | 4 | export const PAGES_DIR_PATH = "pages"; 5 | export const API_DIR_PATH = "pages/api"; 6 | export const ENV_DIR_PATH = ""; 7 | -------------------------------------------------------------------------------- /cli/src/constants/index.ts: -------------------------------------------------------------------------------- 1 | // Main url 2 | export const DEFAULT_ROLE_NAME = `netrunner-S3FullAndCloudWatchRead`; 3 | export const NETRUNNER_API_URL = "https://connect.forge-api.com/netrunner/"; 4 | 5 | // Operating IDs 6 | export const NETRUNNER_AWS_ACCOUNT_ID = "395261708130"; 7 | export const TEST_USER_ID = "6494cc4dde7a3181e1aa6a71"; 8 | export const TEST_EXTERNAL_CUSTOMER_ID = TEST_USER_ID; 9 | 10 | // Status constants 11 | export const STATUS_DEMO_COMPLETED = "DEMO_COMPLETED"; 12 | export const STATUS_IAM_ROLE_CREATED = "STATUS_IAM_ROLE_CREATED"; 13 | -------------------------------------------------------------------------------- /cli/src/create-files.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import * as path from "path"; 3 | import { TEMPLATE_API } from "./constants/_code-gen-presigned-api"; 4 | import { TEMPLATE_UPLOAD_COMPONENT } from "./constants/_code-gen-upload-component"; 5 | import { 6 | API_FILE_NAME, 7 | API_DIR_PATH, 8 | PAGES_FILE_NAME, 9 | PAGES_DIR_PATH, 10 | ENV_DIR_PATH, 11 | } from "./constants/file-names"; 12 | 13 | async function createDirectory(directoryPath: string): Promise { 14 | try { 15 | await fs.mkdir(directoryPath, { recursive: true }); 16 | } catch (err) { 17 | throw new Error( 18 | "[createFiles.ts] Error creating directory: " + directoryPath 19 | ); 20 | } 21 | } 22 | 23 | async function writeFile(filePath: string, fileContent: string): Promise { 24 | await fs.writeFile(filePath, fileContent, "utf-8"); 25 | } 26 | 27 | export interface IWriteFileResponse { 28 | apiFilePath: string; 29 | pagesFilePath: string; 30 | } 31 | 32 | export async function createInitFiles( 33 | bucketName: string 34 | ): Promise { 35 | // Define the path where to write the new api that will upload files to S3 36 | const apiDirPath = path.join(process.cwd(), API_DIR_PATH); 37 | const apiFilePath = path.join(apiDirPath, API_FILE_NAME); 38 | 39 | // define the path where to create a new page file for the upload demo 40 | const pagesDirPath = path.join(process.cwd(), PAGES_DIR_PATH); 41 | const pagesFilePath = path.join(pagesDirPath, PAGES_FILE_NAME); 42 | 43 | // 44 | const envDirPath = path.join(process.cwd(), ENV_DIR_PATH); 45 | const envFilePath = path.join(envDirPath, ".env.local"); 46 | 47 | // Read the contents of the ".env" file in the pagesDirPath 48 | const envFileContent = await fs.readFile(envFilePath, "utf-8"); 49 | 50 | // Define the code template of the new files. append the new bucketname to the existing .env.local file 51 | const apiCodeContent = TEMPLATE_API; 52 | const pagesCodeContent = TEMPLATE_UPLOAD_COMPONENT; 53 | const updatedEnvFileContent = `${envFileContent}\nS3_BUCKET_NAME=${bucketName}`; 54 | 55 | // Create the directories if they're not present yet 56 | await createDirectory(apiDirPath); 57 | await createDirectory(pagesDirPath); 58 | 59 | // Write the updated contents to the .env file 60 | await writeFile(envFilePath, updatedEnvFileContent); 61 | 62 | // Write the new files to disk 63 | await writeFile(apiFilePath, apiCodeContent); 64 | await writeFile(pagesFilePath, pagesCodeContent); 65 | 66 | return { 67 | apiFilePath, 68 | pagesFilePath, 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /cli/src/fetch-user.ts: -------------------------------------------------------------------------------- 1 | import { NETRUNNER_API_URL } from "./constants"; 2 | 3 | export async function fetchUserById(userId: string): Promise { 4 | try { 5 | if (!userId) { 6 | throw new Error("[launchBucketApi.ts] userId is required."); 7 | } 8 | const url = NETRUNNER_API_URL; 9 | const params = { 10 | action: "FETCH_USER", 11 | userId: userId, 12 | }; 13 | 14 | const response = await fetch(url, { 15 | method: "POST", 16 | headers: { 17 | "Content-Type": "application/json", 18 | }, 19 | body: JSON.stringify(params), 20 | }); 21 | 22 | const body = await response.json(); 23 | 24 | if (!response.ok) { 25 | console.log("[fetch-user.ts] error response", body); 26 | throw new Error(`HTTP error! status: ${response.status}`); 27 | } 28 | 29 | return body; 30 | } catch (error) { 31 | console.log("\n"); 32 | console.log(error); 33 | console.log("\n"); 34 | console.error(`Error in launchBucketApi: ${error}\n`); 35 | // rethrow the error so it can be caught further up the call stack if necessary 36 | throw error; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cli/src/iam/createRoleParams.ts: -------------------------------------------------------------------------------- 1 | import { CreateRoleCommandInput } from "@aws-sdk/client-iam"; 2 | 3 | export function createRoleParams( 4 | targetNewRoleName: string, 5 | trustId: string 6 | ): CreateRoleCommandInput { 7 | const NETRUNNER_AWS_ACCOUNT_ID = "395261708130"; 8 | const roleParams = { 9 | RoleName: targetNewRoleName, 10 | AssumeRolePolicyDocument: JSON.stringify({ 11 | Version: "2012-10-17", 12 | Statement: [ 13 | { 14 | Effect: "Allow", 15 | Principal: { 16 | AWS: [ 17 | `arn:aws:iam::${NETRUNNER_AWS_ACCOUNT_ID}:user/netrunner-app`, 18 | ], 19 | }, 20 | Action: "sts:AssumeRole", 21 | Condition: { 22 | StringEquals: { 23 | "sts:ExternalId": `${trustId}`, 24 | }, 25 | }, 26 | }, 27 | ], 28 | }), 29 | }; 30 | 31 | return roleParams; 32 | } 33 | -------------------------------------------------------------------------------- /cli/src/iam/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IAMClient, 3 | CreateRoleCommand, 4 | AttachRolePolicyCommand, 5 | GetRoleCommand, 6 | } from "@aws-sdk/client-iam"; 7 | import { IAwsAccountConfig } from "../configure-aws-client"; 8 | import { createRoleParams } from "./createRoleParams"; 9 | import chalk from "chalk"; 10 | 11 | export interface ICreateIamRoleInTargetAccountProps { 12 | targetNewRoleName: string; 13 | trustId: string; 14 | targetAccountClientConfig: IAwsAccountConfig; 15 | } 16 | 17 | async function doesRoleExist(iamClient: IAMClient, roleName: string) { 18 | const getRoleCommand = new GetRoleCommand({ RoleName: roleName }); 19 | 20 | try { 21 | await iamClient.send(getRoleCommand); 22 | return true; 23 | } catch (err) { 24 | return false; 25 | } 26 | } 27 | 28 | async function createAndAttachIamPolicies( 29 | iamClient: IAMClient, 30 | roleName: string, 31 | trustId: string 32 | ) { 33 | const createRoleCommand = new CreateRoleCommand( 34 | createRoleParams(roleName, trustId) 35 | ); 36 | 37 | try { 38 | const createRoleResponse = await iamClient.send(createRoleCommand); 39 | const roleArn = createRoleResponse?.Role?.Arn; 40 | 41 | console.log("[implement-policy.ts] createRoleResponse", createRoleResponse); 42 | 43 | const attachCloudWatchPolicyCommand = new AttachRolePolicyCommand({ 44 | PolicyArn: "arn:aws:iam::aws:policy/CloudWatchLogsReadOnlyAccess", 45 | RoleName: roleName, 46 | }); 47 | await iamClient.send(attachCloudWatchPolicyCommand); 48 | 49 | const attachS3PolicyCommand = new AttachRolePolicyCommand({ 50 | PolicyArn: "arn:aws:iam::aws:policy/AmazonS3FullAccess", 51 | RoleName: roleName, 52 | }); 53 | await iamClient.send(attachS3PolicyCommand); 54 | console.log( 55 | `[implement-policy.ts] Successfully created the ${roleName} role.\nRole ARN: ${roleArn}` 56 | ); 57 | } catch (err) { 58 | console.error( 59 | `[implement-policy.ts] Error creating the ${roleName} role:`, 60 | err 61 | ); 62 | return false; 63 | } 64 | } 65 | 66 | export async function createIamRoleInTargetAccount({ 67 | targetNewRoleName, 68 | trustId, 69 | targetAccountClientConfig, 70 | }: ICreateIamRoleInTargetAccountProps): Promise { 71 | const { accountId } = targetAccountClientConfig; 72 | 73 | if ( 74 | !accountId || 75 | !targetNewRoleName || 76 | !trustId || 77 | !targetAccountClientConfig 78 | ) { 79 | throw new Error( 80 | "Missing required properties in ICreateIamRoleInTargetAccountProps" 81 | ); 82 | } 83 | 84 | const iamClient = new IAMClient(targetAccountClientConfig); 85 | 86 | if (await doesRoleExist(iamClient, targetNewRoleName)) { 87 | console.log( 88 | chalk.redBright( 89 | `[implement-policy.ts] The role ${targetNewRoleName} already exists, skipping creation of policies\n` 90 | ) 91 | ); 92 | return true; 93 | } 94 | 95 | try { 96 | await createAndAttachIamPolicies(iamClient, targetNewRoleName, trustId); 97 | } catch (err) { 98 | console.error(`Error creating the ${targetNewRoleName} role:`, err); 99 | return false; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from "@oclif/core"; 2 | -------------------------------------------------------------------------------- /cli/src/launch-bucket.ts: -------------------------------------------------------------------------------- 1 | import { NETRUNNER_API_URL } from "./constants"; 2 | 3 | // Data types 4 | interface BucketParams { 5 | region: string; 6 | templateName: string; 7 | serviceName: string; 8 | } 9 | 10 | interface LaunchBucketApiParams { 11 | userId: string; 12 | bucketParams: BucketParams; 13 | } 14 | 15 | // Utility function to check input data 16 | function validateInput(params: LaunchBucketApiParams) { 17 | if (!params.userId) { 18 | throw new Error("[launchBucketApi.ts] userId is required."); 19 | } 20 | 21 | const { region, templateName, serviceName } = params.bucketParams; 22 | if (!region || !templateName || !serviceName) { 23 | throw new Error("[launchBucketApi.ts] Invalid bucket parameters."); 24 | } 25 | } 26 | 27 | export async function launchBucketApi( 28 | params: LaunchBucketApiParams 29 | ): Promise { 30 | try { 31 | // Validate input data 32 | validateInput(params); 33 | 34 | const url = NETRUNNER_API_URL; 35 | 36 | // Request parameters 37 | const requestParams = { 38 | action: "LAUNCH_NEW_BUCKET", 39 | userId: params.userId, 40 | params: params.bucketParams, 41 | }; 42 | 43 | // Send request 44 | const response = await fetch(url, { 45 | method: "POST", 46 | headers: { 47 | "Content-Type": "application/json", 48 | }, 49 | body: JSON.stringify(requestParams), 50 | }); 51 | 52 | // Parse response 53 | const body = await response.json(); 54 | if (!response.ok) { 55 | console.error("HTTP response error", body); 56 | throw new Error( 57 | `[launch-bucket.ts] HTTP error! status: ${response.status}` 58 | ); 59 | } 60 | 61 | return body; 62 | } catch (error) { 63 | console.error( 64 | `[launch-bucket.ts] Error in launchBucketApi: ${JSON.stringify(error)}` 65 | ); 66 | // tslint:disable-next-line: no-throw 67 | throw new Error( 68 | `[launch-bucket.ts] cError in launchBucketApi ${JSON.stringify(error)}` 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /cli/src/update-status.ts: -------------------------------------------------------------------------------- 1 | import { NETRUNNER_API_URL } from "./constants"; 2 | 3 | export async function updateStatus( 4 | userId: string, 5 | status: string 6 | ): Promise { 7 | try { 8 | if (!userId) throw new Error("[launchBucketApi.ts] userId is required."); 9 | if (!status) throw new Error("[launchBucketApi.ts] status is required."); 10 | 11 | const url = NETRUNNER_API_URL; 12 | const params = { 13 | action: "UPDATE_USER_STATUS", 14 | userId: userId, 15 | params: { 16 | status: status, 17 | }, 18 | }; 19 | 20 | const response = await fetch(url, { 21 | method: "POST", 22 | headers: { 23 | "Content-Type": "application/json", 24 | }, 25 | body: JSON.stringify(params), 26 | }); 27 | 28 | if (!response.ok) { 29 | const body = await response.text(); // or response.json() if you know the response is JSON 30 | console.log("response", body); 31 | throw new Error(`HTTP error! status: ${response.status}`); 32 | } 33 | 34 | return response; 35 | } catch (error) { 36 | console.log("\n"); 37 | console.log(error); 38 | console.log("\n"); 39 | console.error(`Error in launchBucketApi: ${error}\n`); 40 | // rethrow the error so it can be caught further up the call stack if necessary 41 | throw error; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cli/src/update-user.ts: -------------------------------------------------------------------------------- 1 | import { NETRUNNER_API_URL } from "./constants"; 2 | 3 | export async function updateUser( 4 | userId: string, 5 | toUpdateValues: any 6 | ): Promise { 7 | try { 8 | if (!userId) throw new Error("[update-user.ts] userId is required."); 9 | if (!toUpdateValues) 10 | throw new Error("[update-user.ts] toUpdateValues is required."); 11 | 12 | const url = NETRUNNER_API_URL; 13 | const params = { 14 | action: "UPDATE_USER", 15 | userId: userId, 16 | params: { 17 | updateParams: { 18 | ...toUpdateValues, 19 | anotherParam: "lol3293409349", 20 | }, 21 | }, 22 | }; 23 | 24 | const response = await fetch(url, { 25 | method: "POST", 26 | headers: { 27 | "Content-Type": "application/json", 28 | }, 29 | body: JSON.stringify(params), 30 | }); 31 | 32 | const body = await response.text(); // or response.json() if you know the response is JSON 33 | 34 | if (!response.ok) { 35 | console.error("response", body); 36 | throw new Error( 37 | `HTTP error! status: ${response.status}. Response body: ${body}` 38 | ); 39 | } 40 | 41 | return body; 42 | } catch (error) { 43 | console.error(`Error in launchBucketApi:`, error); 44 | // rethrow the error so it can be caught further up the call stack if necessary 45 | throw error; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cli/test/commands/clean-up.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs/promises"; 2 | import * as path from "path"; 3 | import chalk from "chalk"; 4 | import { API_DIR_PATH, PAGES_DIR_PATH } from "../../src/constants/file-names"; 5 | 6 | export async function cleanupTestFiles() { 7 | // Clean up any existing test files 8 | let pagesExist = false; 9 | let apiExist = false; 10 | try { 11 | await fs.access(path.join(process.cwd(), PAGES_DIR_PATH)); 12 | pagesExist = true; 13 | } catch {} 14 | 15 | try { 16 | await fs.access(path.join(process.cwd(), API_DIR_PATH)); 17 | apiExist = true; 18 | } catch {} 19 | 20 | if (pagesExist || apiExist) { 21 | console.log( 22 | "\n " + 23 | chalk.magenta("pages/api directory deleted") + 24 | (pagesExist ? `, ${chalk.magenta("pages directory deleted\n")}` : "") 25 | ); 26 | } 27 | 28 | if (pagesExist) { 29 | await fs.rm(path.join(process.cwd(), PAGES_DIR_PATH), { 30 | recursive: true, 31 | force: true, 32 | }); 33 | } 34 | 35 | if (apiExist) { 36 | await fs.rm(path.join(process.cwd(), API_DIR_PATH), { 37 | recursive: true, 38 | force: true, 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cli/test/commands/init.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@oclif/test"; 2 | import * as fs from "fs/promises"; 3 | import * as path from "path"; 4 | import Init from "../../src/commands/init"; 5 | import { 6 | API_FILE_NAME, 7 | API_DIR_PATH, 8 | PAGES_FILE_NAME, 9 | PAGES_DIR_PATH, 10 | } from "../../src/constants/file-names"; 11 | import { cleanupTestFiles } from "./clean-up"; 12 | import { TEST_USER_ID } from "../../src/constants"; 13 | 14 | describe("Init command", () => { 15 | test.it("exports the Init command", () => { 16 | expect(Init).to.not.be.undefined; 17 | }); 18 | 19 | it("executes without throwing an error", async () => { 20 | await cleanupTestFiles(); 21 | 22 | // Call the run() function within a try-catch block. 23 | try { 24 | // execute the Init command 25 | await Init.run([TEST_USER_ID]); 26 | // If no error is thrown, the test will pass. 27 | } catch (error) { 28 | // If an error is thrown, fail the test. 29 | throw new Error(`The Init command threw an error: ${error}`); 30 | } 31 | }); 32 | 33 | test.it("creates new files and logs their paths", async () => { 34 | const apiFilePath = path.join(process.cwd(), API_DIR_PATH, API_FILE_NAME); 35 | const pagesFilePath = path.join( 36 | process.cwd(), 37 | PAGES_DIR_PATH, 38 | PAGES_FILE_NAME 39 | ); 40 | 41 | expect(await fs.access(apiFilePath)).to.not.throw; 42 | expect(await fs.access(pagesFilePath)).to.not.throw; 43 | 44 | // Clean up the test files after the test 45 | // await cleanupTestFiles(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /cli/test/helpers/init.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') 3 | process.env.NODE_ENV = 'development' 4 | 5 | global.oclif = global.oclif || {} 6 | global.oclif.columns = 80 7 | -------------------------------------------------------------------------------- /cli/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | } 6 | // "references": [ 7 | // {"path": ".."} 8 | // ] 9 | } 10 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "declaration": true, 5 | "importHelpers": true, 6 | "module": "commonjs", 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "strict": true, 10 | "target": "es2019" 11 | }, 12 | "include": ["src/**/*", "src/*"], 13 | "exclude": ["node_modules", "dist", "cdk"] 14 | } 15 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignorePatterns: ['dist'], 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | sourceType: 'module', 6 | project: 'packages/client/tsconfig.json' 7 | }, 8 | rules: { 9 | '@typescript-eslint/no-floating-promises': 'error', 10 | '@typescript-eslint/strict-boolean-expressions': ['error', { allowNullableString: true, allowNullableObject: true }], 11 | "@typescript-eslint/ban-types": "off", 12 | } 13 | }; -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Auto generated version file 2 | src/version.ts 3 | 4 | node_modules 5 | node_modules/ 6 | node_modules/* 7 | 8 | ROADMAP.md -------------------------------------------------------------------------------- /client/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | rollup.config.mjs 4 | .eslintrc.cjs 5 | .gitignore -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client/dist/index.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var clientS3 = require('@aws-sdk/client-s3'); 4 | var uuid = require('uuid'); 5 | var s3RequestPresigner = require('@aws-sdk/s3-request-presigner'); 6 | var axios = require('axios'); 7 | var React = require('react'); 8 | 9 | function validateParams(params) { 10 | const { accessKeyId, secretAccessKey, bucket, region } = params; 11 | if (!accessKeyId) { 12 | throw new Error("Missing AWS_ACCESS_KEY_ID environment variable"); 13 | } 14 | if (!secretAccessKey) { 15 | throw new Error("Missing AWS_SECRET_ACCESS_KEY environment variable"); 16 | } 17 | if (!bucket) { 18 | console.warn( 19 | "\x1B[41m\x1B[31m%s\x1B[0m", 20 | "Missing BUCKET_NAME environment variable" 21 | ); 22 | } 23 | if (!region) { 24 | console.warn( 25 | "\x1B[41m\x1B[31m%s\x1B[0m", 26 | "Missing AWS_REGION environment variable" 27 | ); 28 | } 29 | } 30 | function getConfig(params) { 31 | validateParams(params); 32 | const { accessKeyId, secretAccessKey, bucket, region } = params; 33 | const _DEFAULT_BUCKET_NAME = `--undefined-bucket-name-${uuid.v4()}`; 34 | const _DEFAULT_REGION = "us-east-1"; 35 | const config = { 36 | accessKeyId: `${accessKeyId}`, 37 | secretAccessKey: `${secretAccessKey}`, 38 | region: `${region || _DEFAULT_REGION}`, 39 | bucket: `${bucket || _DEFAULT_BUCKET_NAME}` 40 | }; 41 | return config; 42 | } 43 | 44 | function getS3Client(config) { 45 | let client = new clientS3.S3Client({ 46 | credentials: { 47 | accessKeyId: config.accessKeyId, 48 | secretAccessKey: config.secretAccessKey 49 | }, 50 | region: config.region 51 | }); 52 | return client; 53 | } 54 | const getS3ClientNative = () => { 55 | const params = { 56 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 57 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 58 | region: process.env.AWS_REGION, 59 | bucket: process.env.S3_BUCKET_NAME 60 | }; 61 | let config = getConfig(params); 62 | return getS3Client(config); 63 | }; 64 | 65 | function validateStringInput(input) { 66 | if (!input) { 67 | throw new Error("Input cannot be empty"); 68 | } else if (Array.isArray(input)) { 69 | return input.join(","); 70 | } else { 71 | return input; 72 | } 73 | } 74 | 75 | async function apiSignS3Url(filename, filetype) { 76 | const client = getS3ClientNative(); 77 | filename = validateStringInput(filename); 78 | filetype = validateStringInput(filetype); 79 | const objectKey = `${filename}-${Date.now()}`; 80 | const params = { 81 | Bucket: process.env.S3_BUCKET_NAME, 82 | Key: objectKey, 83 | ContentType: filetype, 84 | CacheControl: "max-age=630720000", 85 | Metadata: { 86 | metadata1: "value12", 87 | metadata2: "value2" 88 | } 89 | }; 90 | const command = new clientS3.PutObjectCommand(params); 91 | const signed_url = await s3RequestPresigner.getSignedUrl(client, command, { expiresIn: 3600 }); 92 | return { signed_url, params, command }; 93 | } 94 | 95 | function FileUploadInput({ handleUpload }) { 96 | const handleFileChange = (e) => { 97 | e.preventDefault(); 98 | const file = e.target.files?.[0]; 99 | if (!file) { 100 | return alert("No file selected."); 101 | } else { 102 | handleUpload(file); 103 | } 104 | }; 105 | return /* @__PURE__ */ React.createElement( 106 | "input", 107 | { 108 | type: "file", 109 | accept: "image/png, image/jpeg", 110 | onChange: handleFileChange 111 | } 112 | ); 113 | } 114 | 115 | function validateS3SignedUrl(url) { 116 | const regex = new RegExp(/^(ftp|http|https):\/\/[^ "]+$/); 117 | if (!regex.test(url)) { 118 | throw new Error("[UploadSection/uploadFile] Invalid URL"); 119 | } 120 | } 121 | async function fetchPresignedUrl(filename, filetype) { 122 | const res = await fetch( 123 | `/api/upload-presigned?filename=${filename}&filetype=${encodeURIComponent( 124 | filetype 125 | )}` 126 | ); 127 | const signed_url = (await res.json()).signed_url; 128 | validateS3SignedUrl(signed_url); 129 | return signed_url; 130 | } 131 | async function uploadFile(file) { 132 | if (!file) { 133 | throw new Error("[UploadSection/uploadFile] NO FILE"); 134 | } 135 | const signed_url = await fetchPresignedUrl( 136 | file.name || "unknown-front-end-filename", 137 | file.type 138 | ); 139 | try { 140 | const upload_response = await axios.put(signed_url, file); 141 | return upload_response; 142 | } catch (err) { 143 | console.error({ err }); 144 | return err; 145 | } 146 | } 147 | const useFileUpload = () => ({ 148 | FileUploadInput, 149 | uploadFile 150 | }); 151 | 152 | function demo(a, b) { 153 | console.log("The package is correctly compiled and distributed!"); 154 | return a + b; 155 | } 156 | 157 | exports.apiSignS3Url = apiSignS3Url; 158 | exports.demo = demo; 159 | exports.fetchPresignedUrl = fetchPresignedUrl; 160 | exports.getConfig = getConfig; 161 | exports.getS3Client = getS3Client; 162 | exports.getS3ClientNative = getS3ClientNative; 163 | exports.uploadFile = uploadFile; 164 | exports.useFileUpload = useFileUpload; 165 | //# sourceMappingURL=index.cjs.map 166 | -------------------------------------------------------------------------------- /client/dist/index.cjs.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.cjs","sources":["../src/util/config.ts","../src/util/s3-client.ts","../src/util/helpers.ts","../src/nextjs/s3-presigned-api.ts","../src/nextjs/components/FileUploadInput.tsx","../src/nextjs/file-upload-hook.ts","../src/client.ts"],"sourcesContent":["import { v4 as uuidv4 } from \"uuid\";\n\nexport type TS3Config = {\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n region: string;\n};\n\nexport type TGetConfigParams = {\n accessKeyId: string | undefined;\n secretAccessKey: string | undefined;\n bucket: string | undefined;\n region: string | undefined;\n};\n\nfunction validateParams(params: TGetConfigParams) {\n const { accessKeyId, secretAccessKey, bucket, region } = params;\n\n if (!accessKeyId) {\n throw new Error(\"Missing AWS_ACCESS_KEY_ID environment variable\");\n }\n\n if (!secretAccessKey) {\n throw new Error(\"Missing AWS_SECRET_ACCESS_KEY environment variable\");\n }\n if (!bucket) {\n console.warn(\n \"\\x1b[41m\\x1b[31m%s\\x1b[0m\",\n \"Missing BUCKET_NAME environment variable\"\n );\n }\n\n if (!region) {\n console.warn(\n \"\\x1b[41m\\x1b[31m%s\\x1b[0m\",\n \"Missing AWS_REGION environment variable\"\n );\n }\n}\n\nexport function getConfig(params: TGetConfigParams): TS3Config {\n validateParams(params);\n\n const { accessKeyId, secretAccessKey, bucket, region } = params;\n\n // If bucket or region are not defined, we use a default value\n const _DEFAULT_BUCKET_NAME = `--undefined-bucket-name-${uuidv4()}`;\n const _DEFAULT_REGION = \"us-east-1\";\n\n const config: TS3Config = {\n accessKeyId: `${accessKeyId}`,\n secretAccessKey: `${secretAccessKey}`,\n region: `${region || _DEFAULT_REGION}`,\n bucket: `${bucket || _DEFAULT_BUCKET_NAME}`,\n };\n\n return config;\n}\n","import { S3Client } from \"@aws-sdk/client-s3\";\nimport { TS3Config, getConfig } from \"./config\";\n\nexport function getS3Client(config: TS3Config) {\n let client = new S3Client({\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n region: config.region,\n });\n\n return client;\n}\n\nexport const getS3ClientNative = () => {\n // this version of the function gets the config from the environment variables\n const params = {\n accessKeyId: process.env.AWS_ACCESS_KEY_ID,\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\n region: process.env.AWS_REGION,\n bucket: process.env.S3_BUCKET_NAME,\n };\n\n let config = getConfig(params);\n\n return getS3Client(config);\n};\n","export function validateStringInput(input: string | string[]): string {\n // convert string of arrays or string to string / used in apiSignS3Url\n if (!input) {\n throw new Error(\"Input cannot be empty\");\n } else if (Array.isArray(input)) {\n return input.join(\",\");\n } else {\n return input;\n }\n}\n","import { PutObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport { getS3ClientNative } from \"../util\";\nimport { validateStringInput } from \"../util/helpers\";\n\ninterface S3UploadResponse {\n signed_url: string;\n params: any;\n command: any;\n}\n\nexport async function apiSignS3Url(\n filename: string | string[],\n filetype: string | string[]\n): Promise {\n const client = getS3ClientNative();\n filename = validateStringInput(filename);\n filetype = validateStringInput(filetype);\n\n const objectKey = `${filename}-${Date.now()}`;\n const params = {\n Bucket: process.env.S3_BUCKET_NAME,\n Key: objectKey,\n ContentType: filetype,\n CacheControl: \"max-age=630720000\",\n Metadata: {\n metadata1: \"value12\",\n metadata2: \"value2\",\n },\n };\n\n const command = new PutObjectCommand(params);\n const signed_url = await getSignedUrl(client, command, { expiresIn: 3600 });\n\n return { signed_url, params, command };\n}\n","import React from \"react\";\n\nexport function FileUploadInput({ handleUpload }: { handleUpload: any }) {\n const handleFileChange = (e: React.ChangeEvent) => {\n e.preventDefault();\n const file = e.target.files?.[0]!;\n if (!file) {\n return alert(\"No file selected.\");\n } else {\n handleUpload(file);\n }\n };\n\n return (\n \n );\n}\n","import axios from \"axios\";\nimport { FileUploadInput } from \"./components/FileUploadInput\";\n\nfunction validateS3SignedUrl(url: string) {\n const regex = new RegExp(/^(ftp|http|https):\\/\\/[^ \"]+$/);\n\n if (!regex.test(url)) {\n throw new Error(\"[UploadSection/uploadFile] Invalid URL\");\n }\n}\n\nexport async function fetchPresignedUrl(\n filename: string,\n filetype: string\n): Promise {\n const res = await fetch(\n `/api/upload-presigned?filename=${filename}&filetype=${encodeURIComponent(\n filetype\n )}`\n );\n const signed_url = (await res.json()).signed_url;\n validateS3SignedUrl(signed_url);\n return signed_url;\n}\n\nexport async function uploadFile(file: File): Promise {\n if (!file) {\n throw new Error(\"[UploadSection/uploadFile] NO FILE\");\n }\n\n const signed_url = await fetchPresignedUrl(\n file.name || \"unknown-front-end-filename\",\n file.type\n );\n\n try {\n const upload_response = await axios.put(signed_url, file);\n return upload_response;\n } catch (err) {\n console.error({ err });\n return err;\n }\n}\n\nexport const useFileUpload = () => ({\n FileUploadInput,\n uploadFile,\n});\n","export * from \"./util/s3-client\";\nexport * from \"./util/config\";\nexport * from \"./nextjs/s3-presigned-api\";\nexport * from \"./nextjs/file-upload-hook\";\n\n// for testing purposes should be removed at some point\nexport function demo(a: number, b: number): number {\n console.log(\"The package is correctly compiled and distributed!\");\n return a + b;\n}\n"],"names":["uuidv4","S3Client","PutObjectCommand","getSignedUrl"],"mappings":"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"} -------------------------------------------------------------------------------- /client/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from '@aws-sdk/client-s3'; 2 | 3 | type TS3Config = { 4 | accessKeyId: string; 5 | secretAccessKey: string; 6 | bucket: string; 7 | region: string; 8 | }; 9 | type TGetConfigParams = { 10 | accessKeyId: string | undefined; 11 | secretAccessKey: string | undefined; 12 | bucket: string | undefined; 13 | region: string | undefined; 14 | }; 15 | declare function getConfig(params: TGetConfigParams): TS3Config; 16 | 17 | declare function getS3Client(config: TS3Config): S3Client; 18 | declare const getS3ClientNative: () => S3Client; 19 | 20 | interface S3UploadResponse { 21 | signed_url: string; 22 | params: any; 23 | command: any; 24 | } 25 | declare function apiSignS3Url(filename: string | string[], filetype: string | string[]): Promise; 26 | 27 | declare function FileUploadInput({ handleUpload }: { 28 | handleUpload: any; 29 | }): JSX.Element; 30 | 31 | declare function fetchPresignedUrl(filename: string, filetype: string): Promise; 32 | declare function uploadFile(file: File): Promise; 33 | declare const useFileUpload: () => { 34 | FileUploadInput: typeof FileUploadInput; 35 | uploadFile: typeof uploadFile; 36 | }; 37 | 38 | declare function demo(a: number, b: number): number; 39 | 40 | export { TGetConfigParams, TS3Config, apiSignS3Url, demo, fetchPresignedUrl, getConfig, getS3Client, getS3ClientNative, uploadFile, useFileUpload }; 41 | -------------------------------------------------------------------------------- /client/dist/index.mjs: -------------------------------------------------------------------------------- 1 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; 2 | import { v4 } from 'uuid'; 3 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 4 | import axios from 'axios'; 5 | import React from 'react'; 6 | 7 | function validateParams(params) { 8 | const { accessKeyId, secretAccessKey, bucket, region } = params; 9 | if (!accessKeyId) { 10 | throw new Error("Missing AWS_ACCESS_KEY_ID environment variable"); 11 | } 12 | if (!secretAccessKey) { 13 | throw new Error("Missing AWS_SECRET_ACCESS_KEY environment variable"); 14 | } 15 | if (!bucket) { 16 | console.warn( 17 | "\x1B[41m\x1B[31m%s\x1B[0m", 18 | "Missing BUCKET_NAME environment variable" 19 | ); 20 | } 21 | if (!region) { 22 | console.warn( 23 | "\x1B[41m\x1B[31m%s\x1B[0m", 24 | "Missing AWS_REGION environment variable" 25 | ); 26 | } 27 | } 28 | function getConfig(params) { 29 | validateParams(params); 30 | const { accessKeyId, secretAccessKey, bucket, region } = params; 31 | const _DEFAULT_BUCKET_NAME = `--undefined-bucket-name-${v4()}`; 32 | const _DEFAULT_REGION = "us-east-1"; 33 | const config = { 34 | accessKeyId: `${accessKeyId}`, 35 | secretAccessKey: `${secretAccessKey}`, 36 | region: `${region || _DEFAULT_REGION}`, 37 | bucket: `${bucket || _DEFAULT_BUCKET_NAME}` 38 | }; 39 | return config; 40 | } 41 | 42 | function getS3Client(config) { 43 | let client = new S3Client({ 44 | credentials: { 45 | accessKeyId: config.accessKeyId, 46 | secretAccessKey: config.secretAccessKey 47 | }, 48 | region: config.region 49 | }); 50 | return client; 51 | } 52 | const getS3ClientNative = () => { 53 | const params = { 54 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 55 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 56 | region: process.env.AWS_REGION, 57 | bucket: process.env.S3_BUCKET_NAME 58 | }; 59 | let config = getConfig(params); 60 | return getS3Client(config); 61 | }; 62 | 63 | function validateStringInput(input) { 64 | if (!input) { 65 | throw new Error("Input cannot be empty"); 66 | } else if (Array.isArray(input)) { 67 | return input.join(","); 68 | } else { 69 | return input; 70 | } 71 | } 72 | 73 | async function apiSignS3Url(filename, filetype) { 74 | const client = getS3ClientNative(); 75 | filename = validateStringInput(filename); 76 | filetype = validateStringInput(filetype); 77 | const objectKey = `${filename}-${Date.now()}`; 78 | const params = { 79 | Bucket: process.env.S3_BUCKET_NAME, 80 | Key: objectKey, 81 | ContentType: filetype, 82 | CacheControl: "max-age=630720000", 83 | Metadata: { 84 | metadata1: "value12", 85 | metadata2: "value2" 86 | } 87 | }; 88 | const command = new PutObjectCommand(params); 89 | const signed_url = await getSignedUrl(client, command, { expiresIn: 3600 }); 90 | return { signed_url, params, command }; 91 | } 92 | 93 | function FileUploadInput({ handleUpload }) { 94 | const handleFileChange = (e) => { 95 | e.preventDefault(); 96 | const file = e.target.files?.[0]; 97 | if (!file) { 98 | return alert("No file selected."); 99 | } else { 100 | handleUpload(file); 101 | } 102 | }; 103 | return /* @__PURE__ */ React.createElement( 104 | "input", 105 | { 106 | type: "file", 107 | accept: "image/png, image/jpeg", 108 | onChange: handleFileChange 109 | } 110 | ); 111 | } 112 | 113 | function validateS3SignedUrl(url) { 114 | const regex = new RegExp(/^(ftp|http|https):\/\/[^ "]+$/); 115 | if (!regex.test(url)) { 116 | throw new Error("[UploadSection/uploadFile] Invalid URL"); 117 | } 118 | } 119 | async function fetchPresignedUrl(filename, filetype) { 120 | const res = await fetch( 121 | `/api/upload-presigned?filename=${filename}&filetype=${encodeURIComponent( 122 | filetype 123 | )}` 124 | ); 125 | const signed_url = (await res.json()).signed_url; 126 | validateS3SignedUrl(signed_url); 127 | return signed_url; 128 | } 129 | async function uploadFile(file) { 130 | if (!file) { 131 | throw new Error("[UploadSection/uploadFile] NO FILE"); 132 | } 133 | const signed_url = await fetchPresignedUrl( 134 | file.name || "unknown-front-end-filename", 135 | file.type 136 | ); 137 | try { 138 | const upload_response = await axios.put(signed_url, file); 139 | return upload_response; 140 | } catch (err) { 141 | console.error({ err }); 142 | return err; 143 | } 144 | } 145 | const useFileUpload = () => ({ 146 | FileUploadInput, 147 | uploadFile 148 | }); 149 | 150 | function demo(a, b) { 151 | console.log("The package is correctly compiled and distributed!"); 152 | return a + b; 153 | } 154 | 155 | export { apiSignS3Url, demo, fetchPresignedUrl, getConfig, getS3Client, getS3ClientNative, uploadFile, useFileUpload }; 156 | //# sourceMappingURL=index.mjs.map 157 | -------------------------------------------------------------------------------- /client/dist/index.mjs.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.mjs","sources":["../src/util/config.ts","../src/util/s3-client.ts","../src/util/helpers.ts","../src/nextjs/s3-presigned-api.ts","../src/nextjs/components/FileUploadInput.tsx","../src/nextjs/file-upload-hook.ts","../src/client.ts"],"sourcesContent":["import { v4 as uuidv4 } from \"uuid\";\n\nexport type TS3Config = {\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n region: string;\n};\n\nexport type TGetConfigParams = {\n accessKeyId: string | undefined;\n secretAccessKey: string | undefined;\n bucket: string | undefined;\n region: string | undefined;\n};\n\nfunction validateParams(params: TGetConfigParams) {\n const { accessKeyId, secretAccessKey, bucket, region } = params;\n\n if (!accessKeyId) {\n throw new Error(\"Missing AWS_ACCESS_KEY_ID environment variable\");\n }\n\n if (!secretAccessKey) {\n throw new Error(\"Missing AWS_SECRET_ACCESS_KEY environment variable\");\n }\n if (!bucket) {\n console.warn(\n \"\\x1b[41m\\x1b[31m%s\\x1b[0m\",\n \"Missing BUCKET_NAME environment variable\"\n );\n }\n\n if (!region) {\n console.warn(\n \"\\x1b[41m\\x1b[31m%s\\x1b[0m\",\n \"Missing AWS_REGION environment variable\"\n );\n }\n}\n\nexport function getConfig(params: TGetConfigParams): TS3Config {\n validateParams(params);\n\n const { accessKeyId, secretAccessKey, bucket, region } = params;\n\n // If bucket or region are not defined, we use a default value\n const _DEFAULT_BUCKET_NAME = `--undefined-bucket-name-${uuidv4()}`;\n const _DEFAULT_REGION = \"us-east-1\";\n\n const config: TS3Config = {\n accessKeyId: `${accessKeyId}`,\n secretAccessKey: `${secretAccessKey}`,\n region: `${region || _DEFAULT_REGION}`,\n bucket: `${bucket || _DEFAULT_BUCKET_NAME}`,\n };\n\n return config;\n}\n","import { S3Client } from \"@aws-sdk/client-s3\";\nimport { TS3Config, getConfig } from \"./config\";\n\nexport function getS3Client(config: TS3Config) {\n let client = new S3Client({\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n region: config.region,\n });\n\n return client;\n}\n\nexport const getS3ClientNative = () => {\n // this version of the function gets the config from the environment variables\n const params = {\n accessKeyId: process.env.AWS_ACCESS_KEY_ID,\n secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\n region: process.env.AWS_REGION,\n bucket: process.env.S3_BUCKET_NAME,\n };\n\n let config = getConfig(params);\n\n return getS3Client(config);\n};\n","export function validateStringInput(input: string | string[]): string {\n // convert string of arrays or string to string / used in apiSignS3Url\n if (!input) {\n throw new Error(\"Input cannot be empty\");\n } else if (Array.isArray(input)) {\n return input.join(\",\");\n } else {\n return input;\n }\n}\n","import { PutObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport { getS3ClientNative } from \"../util\";\nimport { validateStringInput } from \"../util/helpers\";\n\ninterface S3UploadResponse {\n signed_url: string;\n params: any;\n command: any;\n}\n\nexport async function apiSignS3Url(\n filename: string | string[],\n filetype: string | string[]\n): Promise {\n const client = getS3ClientNative();\n filename = validateStringInput(filename);\n filetype = validateStringInput(filetype);\n\n const objectKey = `${filename}-${Date.now()}`;\n const params = {\n Bucket: process.env.S3_BUCKET_NAME,\n Key: objectKey,\n ContentType: filetype,\n CacheControl: \"max-age=630720000\",\n Metadata: {\n metadata1: \"value12\",\n metadata2: \"value2\",\n },\n };\n\n const command = new PutObjectCommand(params);\n const signed_url = await getSignedUrl(client, command, { expiresIn: 3600 });\n\n return { signed_url, params, command };\n}\n","import React from \"react\";\n\nexport function FileUploadInput({ handleUpload }: { handleUpload: any }) {\n const handleFileChange = (e: React.ChangeEvent) => {\n e.preventDefault();\n const file = e.target.files?.[0]!;\n if (!file) {\n return alert(\"No file selected.\");\n } else {\n handleUpload(file);\n }\n };\n\n return (\n \n );\n}\n","import axios from \"axios\";\nimport { FileUploadInput } from \"./components/FileUploadInput\";\n\nfunction validateS3SignedUrl(url: string) {\n const regex = new RegExp(/^(ftp|http|https):\\/\\/[^ \"]+$/);\n\n if (!regex.test(url)) {\n throw new Error(\"[UploadSection/uploadFile] Invalid URL\");\n }\n}\n\nexport async function fetchPresignedUrl(\n filename: string,\n filetype: string\n): Promise {\n const res = await fetch(\n `/api/upload-presigned?filename=${filename}&filetype=${encodeURIComponent(\n filetype\n )}`\n );\n const signed_url = (await res.json()).signed_url;\n validateS3SignedUrl(signed_url);\n return signed_url;\n}\n\nexport async function uploadFile(file: File): Promise {\n if (!file) {\n throw new Error(\"[UploadSection/uploadFile] NO FILE\");\n }\n\n const signed_url = await fetchPresignedUrl(\n file.name || \"unknown-front-end-filename\",\n file.type\n );\n\n try {\n const upload_response = await axios.put(signed_url, file);\n return upload_response;\n } catch (err) {\n console.error({ err });\n return err;\n }\n}\n\nexport const useFileUpload = () => ({\n FileUploadInput,\n uploadFile,\n});\n","export * from \"./util/s3-client\";\nexport * from \"./util/config\";\nexport * from \"./nextjs/s3-presigned-api\";\nexport * from \"./nextjs/file-upload-hook\";\n\n// for testing purposes should be removed at some point\nexport function demo(a: number, b: number): number {\n console.log(\"The package is correctly compiled and distributed!\");\n return a + b;\n}\n"],"names":["uuidv4"],"mappings":"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"} -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storengine/client", 3 | "version": "0.0.4", 4 | "description": "netrunner storengine SDK for Typescript and JavaScript", 5 | "main": "./dist/index.cjs", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/index.mjs", 11 | "require": "./dist/index.cjs", 12 | "types": "./dist/index.d.ts" 13 | } 14 | }, 15 | "scripts": { 16 | "test": "node test/main.test.mjs", 17 | "build": "rimraf dist && rollup -c && pnpm test" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/davincios/netrunner.git#main" 22 | }, 23 | "author": "Vincent Hus", 24 | "license": "Apache-2.0", 25 | "bugs": { 26 | "url": "https://github.com/davincios/netrunner/issues" 27 | }, 28 | "homepage": "https://github.com/davincios/netrunner/tree/main#readme", 29 | "browser": { 30 | "child_process": false 31 | }, 32 | "peerDependencies": { 33 | "typescript": "^=5.0.4" 34 | }, 35 | "dependencies": { 36 | "@aws-sdk/client-s3": "^3.405.0", 37 | "@aws-sdk/s3-request-presigner": "^3.405.0", 38 | "react": "^18.2.0", 39 | "uuid": "^9.0.0" 40 | }, 41 | "devDependencies": { 42 | "@rollup/plugin-alias": "^5.0.0", 43 | "@types/node": "^18.16.2", 44 | "@types/react": "^18.2.0", 45 | "@types/uuid": "^9.0.1", 46 | "rimraf": "^5.0.0", 47 | "rollup": "^3.21.0", 48 | "rollup-plugin-dts": "^5.3.0", 49 | "rollup-plugin-esbuild": "^5.0.0", 50 | "rollup-plugin-strip-code": "^0.2.7", 51 | "typescript": "^5.0.4" 52 | }, 53 | "directories": { 54 | "test": "test" 55 | } 56 | } -------------------------------------------------------------------------------- /client/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import dts from "rollup-plugin-dts"; 2 | import esbuild from "rollup-plugin-esbuild"; 3 | import stripCode from "rollup-plugin-strip-code"; 4 | import alias from "@rollup/plugin-alias"; 5 | 6 | export default [ 7 | { 8 | input: "src/index.ts", 9 | plugins: [ 10 | stripCode({ 11 | start_comment: "REMOVE_CJS_BUNDLE_START", 12 | end_comment: "REMOVE_CJS_BUNDLE_END", 13 | }), 14 | esbuild(), 15 | ], 16 | output: { 17 | file: `dist/index.cjs`, 18 | format: "cjs", 19 | sourcemap: true, 20 | }, 21 | }, 22 | { 23 | input: "src/index.ts", 24 | plugins: [ 25 | stripCode({ 26 | start_comment: "REMOVE_ESM_BUNDLE_START", 27 | end_comment: "REMOVE_ESM_BUNDLE_END", 28 | }), 29 | esbuild(), 30 | ], 31 | output: { 32 | file: `dist/index.mjs`, 33 | format: "es", 34 | sourcemap: true, 35 | }, 36 | }, 37 | { 38 | input: "src/index.ts", 39 | plugins: [ 40 | dts(), 41 | alias({ 42 | entries: [{ find: "aws-crt", replacement: "node_modules/aws-crt" }], 43 | // "aws-crt": path.resolve(__dirname, "node_modules/aws-crt"), 44 | }), 45 | ], 46 | 47 | output: { 48 | file: `dist/index.d.ts`, 49 | format: "es", 50 | }, 51 | }, 52 | ]; 53 | -------------------------------------------------------------------------------- /client/src/client.ts: -------------------------------------------------------------------------------- 1 | export * from "./util/s3-client"; 2 | export * from "./util/config"; 3 | export * from "./nextjs/s3-presigned-api"; 4 | export * from "./nextjs/file-upload-hook"; 5 | 6 | // for testing purposes should be removed at some point 7 | export function demo(a: number, b: number): number { 8 | console.log("The package is correctly compiled and distributed!"); 9 | return a + b; 10 | } 11 | -------------------------------------------------------------------------------- /client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | -------------------------------------------------------------------------------- /client/src/nextjs/components/Demo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useFileUpload } from "../file-upload-hook"; 3 | 4 | export function DemoUploadComponent() { 5 | // This is the component as we show it in the demo 6 | const { FileUploadInput, uploadFile } = useFileUpload(); 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/nextjs/components/FileUploadInput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export function FileUploadInput({ handleUpload }: { handleUpload: any }) { 4 | const handleFileChange = (e: React.ChangeEvent) => { 5 | e.preventDefault(); 6 | const file = e.target.files?.[0]!; 7 | if (!file) { 8 | return alert("No file selected."); 9 | } else { 10 | handleUpload(file); 11 | } 12 | }; 13 | 14 | return ( 15 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /client/src/nextjs/file-upload-hook.ts: -------------------------------------------------------------------------------- 1 | import { FileUploadInput } from "./components/FileUploadInput"; 2 | 3 | function validateS3SignedUrl(url: string) { 4 | const regex = new RegExp(/^(ftp|http|https):\/\/[^ "]+$/); 5 | 6 | if (!regex.test(url)) { 7 | throw new Error("[UploadSection/uploadFile] Invalid URL"); 8 | } 9 | } 10 | 11 | export async function fetchPresignedUrl( 12 | filename: string, 13 | filetype: string 14 | ): Promise { 15 | const res = await fetch( 16 | `/api/upload-presigned?filename=${filename}&filetype=${encodeURIComponent( 17 | filetype 18 | )}` 19 | ); 20 | const signed_url = (await res.json()).signed_url; 21 | validateS3SignedUrl(signed_url); 22 | return signed_url; 23 | } 24 | 25 | export async function uploadFile(file: File): Promise { 26 | if (!file) { 27 | throw new Error("[UploadSection/uploadFile] NO FILE"); 28 | } 29 | 30 | const signed_url = await fetchPresignedUrl( 31 | file.name || "unknown-front-end-filename", 32 | file.type 33 | ); 34 | 35 | try { 36 | const upload_response = await fetch(signed_url, { 37 | method: "PUT", 38 | body: file, 39 | headers: { 40 | "Content-Type": file.type, 41 | }, 42 | }); 43 | 44 | if (!upload_response.ok) { 45 | throw new Error(`HTTP error! Status: ${upload_response.status}`); 46 | } 47 | 48 | return await upload_response.json(); 49 | } catch (err) { 50 | console.error({ err }); 51 | return err; 52 | } 53 | } 54 | 55 | export const useFileUpload = () => ({ 56 | FileUploadInput, 57 | uploadFile, 58 | }); 59 | -------------------------------------------------------------------------------- /client/src/nextjs/s3-presigned-api.ts: -------------------------------------------------------------------------------- 1 | import { PutObjectCommand } from "@aws-sdk/client-s3"; 2 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 3 | import { getS3ClientNative } from "../util"; 4 | import { validateStringInput } from "../util/helpers"; 5 | 6 | interface S3UploadResponse { 7 | signed_url: string; 8 | params: any; 9 | command: any; 10 | } 11 | 12 | export async function apiSignS3Url( 13 | filename: string | string[], 14 | filetype: string | string[] 15 | ): Promise { 16 | const client = getS3ClientNative(); 17 | filename = validateStringInput(filename); 18 | filetype = validateStringInput(filetype); 19 | 20 | const objectKey = `${filename}-${Date.now()}`; 21 | const params = { 22 | Bucket: process.env.S3_BUCKET_NAME, 23 | Key: objectKey, 24 | ContentType: filetype, 25 | CacheControl: "max-age=630720000", 26 | Metadata: { 27 | metadata1: "value12", 28 | metadata2: "value2", 29 | }, 30 | }; 31 | 32 | const command = new PutObjectCommand(params); 33 | const signed_url = await getSignedUrl(client, command, { expiresIn: 3600 }); 34 | 35 | return { signed_url, params, command }; 36 | } 37 | -------------------------------------------------------------------------------- /client/src/util/config.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | 3 | export type TS3Config = { 4 | accessKeyId: string; 5 | secretAccessKey: string; 6 | bucket: string; 7 | region: string; 8 | }; 9 | 10 | export type TGetConfigParams = { 11 | accessKeyId: string | undefined; 12 | secretAccessKey: string | undefined; 13 | bucket: string | undefined; 14 | region: string | undefined; 15 | }; 16 | 17 | function validateParams(params: TGetConfigParams) { 18 | const { accessKeyId, secretAccessKey, bucket, region } = params; 19 | 20 | if (!accessKeyId) { 21 | throw new Error("Missing AWS_ACCESS_KEY_ID environment variable"); 22 | } 23 | 24 | if (!secretAccessKey) { 25 | throw new Error("Missing AWS_SECRET_ACCESS_KEY environment variable"); 26 | } 27 | if (!bucket) { 28 | console.warn( 29 | "\x1b[41m\x1b[31m%s\x1b[0m", 30 | "Missing BUCKET_NAME environment variable" 31 | ); 32 | } 33 | 34 | if (!region) { 35 | console.warn( 36 | "\x1b[41m\x1b[31m%s\x1b[0m", 37 | "Missing AWS_REGION environment variable" 38 | ); 39 | } 40 | } 41 | 42 | export function getConfig(params: TGetConfigParams): TS3Config { 43 | validateParams(params); 44 | 45 | const { accessKeyId, secretAccessKey, bucket, region } = params; 46 | 47 | // If bucket or region are not defined, we use a default value 48 | const _DEFAULT_BUCKET_NAME = `--undefined-bucket-name-${uuidv4()}`; 49 | const _DEFAULT_REGION = "us-east-1"; 50 | 51 | const config: TS3Config = { 52 | accessKeyId: `${accessKeyId}`, 53 | secretAccessKey: `${secretAccessKey}`, 54 | region: `${region || _DEFAULT_REGION}`, 55 | bucket: `${bucket || _DEFAULT_BUCKET_NAME}`, 56 | }; 57 | 58 | return config; 59 | } 60 | -------------------------------------------------------------------------------- /client/src/util/helpers.ts: -------------------------------------------------------------------------------- 1 | export function validateStringInput(input: string | string[]): string { 2 | // convert string of arrays or string to string / used in apiSignS3Url 3 | if (!input) { 4 | throw new Error("Input cannot be empty"); 5 | } else if (Array.isArray(input)) { 6 | return input.join(","); 7 | } else { 8 | return input; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./config"; 2 | export * from "./s3-client"; 3 | -------------------------------------------------------------------------------- /client/src/util/s3-client.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | import { TS3Config, getConfig } from "./config"; 3 | 4 | export function getS3Client(config: TS3Config) { 5 | let client = new S3Client({ 6 | credentials: { 7 | accessKeyId: config.accessKeyId, 8 | secretAccessKey: config.secretAccessKey, 9 | }, 10 | region: config.region, 11 | }); 12 | 13 | return client; 14 | } 15 | 16 | export const getS3ClientNative = () => { 17 | // this version of the function gets the config from the environment variables 18 | const params = { 19 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 20 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 21 | region: process.env.AWS_REGION, 22 | bucket: process.env.S3_BUCKET_NAME, 23 | }; 24 | 25 | let config = getConfig(params); 26 | 27 | return getS3Client(config); 28 | }; 29 | -------------------------------------------------------------------------------- /client/test/main.test.mjs: -------------------------------------------------------------------------------- 1 | import { demo } from "../dist/index.mjs"; 2 | 3 | const successMessage = () => 4 | console.log("\x1b[32m", "All tests passed!", "\x1b[0m"); 5 | 6 | function testDemo() { 7 | const result = demo(1, 2); 8 | const expected = 3; 9 | if (result !== expected) { 10 | throw new Error(`${result} is not equal to ${expected}`); 11 | } else { 12 | return true; 13 | } 14 | } 15 | 16 | function main() { 17 | const t1 = testDemo(); 18 | const t2 = true; 19 | 20 | if (t1 && t2) return successMessage(); 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["esnext", "dom"], 5 | "jsx": "react", 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "strictNullChecks": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "es2020", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": false, 19 | "outDir": "dist", 20 | "declaration": true, 21 | "types": ["node"] 22 | }, 23 | "include": ["src"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /docs/NR_PLATFORM.md: -------------------------------------------------------------------------------- 1 |

2 | 🛜🏃🦾 Turn your AWS account into a personalised Firebase with Netrunner 🛜🏃🦾 3 |

4 |

5 | Netrunner Logo 6 |

7 |

8 | Node version 9 | English 10 |

11 |

12 | 13 | 14 | 🕸️ Website 15 |   •   16 | 🤝 Contribute 17 |

18 | 19 | ## Introducing Netrunner for Next.js and AWS S3 20 | 21 | Netrunner streamlines the process of integrating AWS S3 storage into your Next.js application. It is a storage development tool that creates and configures AWS S3 buckets for you, and provides you with instant APIs and code snippets for file uploads. 22 | 23 | And it doesn't stop at just configuration - Netrunner also supplies you with file previews for any media and easy-to-use security policy configurations, so that you don't have to reinvent the wheel. 24 | 25 | ## ✨ UI Screenshot 26 | 27 |
28 |

29 | Screenshot 30 |

31 |
32 | 33 | ## 🎉 Features 34 | 35 | - [x] ☁️ AWS S3 bucket creation and secure configuration for file uploads 36 | - [x] 🦾 Ready-to-use code snippets using AWS v3 or Netrunner SDKs 37 | - [x] 🔗 Easy storage, organization, and serving of large files 38 | - [x] 🏞️ Media previews, including videos and images 39 | - [ ] 🔓 Security policy creation from the UI (coming soon) 40 | - [ ] 🧠 Log streaming & OpenAI GPT-4 automated bug fixing (coming soon) 41 | - [ ] 🪐 Lambda functions and database provisioning (coming soon) 42 | 43 | Netrunner is currently in closed beta. You can sign up for beta access on the Netrunner [website](https://netrunnerhq.com). 44 | 45 | ## 👨‍🚀 Getting Started 46 | 47 | The easiest way to get started with Netrunner is the automatic setup CLI available on [npm](https://www.npmjs.com/package/@storengine/client). The cli sets up the following for Netrunner: 48 | 49 | - Environment variables. 50 | - Example code repository 51 | - A quickstart S3 bucket in your cloud account 52 | 53 | Before you get started, please make sure you have the following installed: 54 | 55 | - Netrunner API keys (available after signup) 56 | - AWS SDK and CLI 57 | - An OpenAI API key 58 | - Node.js 59 | - Visual studio code 60 | 61 | ## 🚀 Tech Stack 62 | 63 | - ✅ Framework: Nextjs 13 + Typescript + FastAPI 64 | - ✅ Auth: Auth0.js 65 | - ✅ Database: MongoDB. 66 | - ✅ Styling: TailwindCSS + RadixUI. 67 | - ✅ Infrasturcture as a code: Terraform + CloudFormation 68 | - ✅ Cloud platform: AWS 69 | 70 | ## 🦾 About Netrunner 71 | 72 | We are on a mission to enable JavaScript software engineers to transform their AWS cloud account into an AI-powered, personalised Firebase developer platform. Learn more by visiting our [website](https://netrunnerhq.com). 73 | -------------------------------------------------------------------------------- /docs/logo/file-upload-completed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/file-upload-completed.png -------------------------------------------------------------------------------- /docs/logo/gifs/bucket-creation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/gifs/bucket-creation.gif -------------------------------------------------------------------------------- /docs/logo/gifs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/gifs/demo.gif -------------------------------------------------------------------------------- /docs/logo/gifs/file-upload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/gifs/file-upload.gif -------------------------------------------------------------------------------- /docs/logo/gifs/hero-gif-full-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/gifs/hero-gif-full-demo.gif -------------------------------------------------------------------------------- /docs/logo/netrunner-main-character.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/netrunner-main-character.png -------------------------------------------------------------------------------- /docs/logo/screenshot-10-july.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/screenshot-10-july.png -------------------------------------------------------------------------------- /docs/logo/screenshot-22-july.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/screenshot-22-july.png -------------------------------------------------------------------------------- /docs/logo/screenshot-25-july.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netrunnerhq/nextjs-aws-s3/bbd46bc850ea6bd487d0775832d65d033fe1c6f4/docs/logo/screenshot-25-july.png -------------------------------------------------------------------------------- /examples/app-router-example/.env.template: -------------------------------------------------------------------------------- 1 | # template file for .env 2 | # go to the AWS console to get your credentials 3 | S3_BUCKET_NAME=XXXXXXX 4 | AWS_REGION=XXXXXX 5 | 6 | 7 | # aws credentials 8 | AWS_ACCESS_KEY_ID=xxxxx 9 | AWS_SECRET_ACCESS_KEY=xxxxx 10 | -------------------------------------------------------------------------------- /examples/app-router-example/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@next/next/no-img-element": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/app-router-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # local env files 4 | .env*.local 5 | .env 6 | 7 | # dependencies 8 | /node_modules 9 | /.pnp 10 | .pnp.js 11 | 12 | # testing 13 | /coverage 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | 19 | # production 20 | /build 21 | 22 | # misc 23 | .DS_Store 24 | *.pem 25 | 26 | # debug 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /examples/app-router-example/README.md: -------------------------------------------------------------------------------- 1 | # Next JS App Router Example 2 | 3 | ## QuickStart 4 | 5 | 1. Create an S3 bucket in the Netrunner dashboard: https://netrunnerhq.com/ 6 | 2. copy the template .env file in your terminal: `cp .env.template .env.local` and paste your S3 bucket name and AWS credentials 7 | 3. `pnpm dev` 8 | 4. Upload an example file. 9 | -------------------------------------------------------------------------------- /examples/app-router-example/app/api/upload/helper.ts: -------------------------------------------------------------------------------- 1 | // app/api/upload.ts 2 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 3 | import { 4 | GetObjectCommand, 5 | PutObjectCommand, 6 | S3Client, 7 | } from "@aws-sdk/client-s3"; 8 | 9 | export function getAwsConsoleUrl( 10 | bucketName: string, 11 | region: string, 12 | objectKey: string 13 | ): string { 14 | return `https://s3.console.aws.amazon.com/s3/object/${bucketName}?region=${region}&prefix=${objectKey}`; 15 | } 16 | 17 | export async function getPresignedUrlS3({ 18 | bucketName, 19 | objectKey, 20 | client, 21 | requestType, 22 | }: { 23 | bucketName: string; 24 | objectKey: string; 25 | client?: S3Client; 26 | requestType?: "put" | "get"; 27 | }): Promise { 28 | if (!client) throw new Error("[presigned-url.ts] client is required"); 29 | if (!bucketName) throw new Error("[presigned-url.ts] bucketName is required"); 30 | if (!objectKey) throw new Error("[presigned-url.ts] objectKey is required"); 31 | if (!requestType) 32 | throw new Error("[presigned-url.ts] requestType is required"); 33 | 34 | try { 35 | let command; 36 | 37 | switch (requestType) { 38 | case "get": 39 | command = new GetObjectCommand({ 40 | Bucket: bucketName, 41 | Key: objectKey, 42 | }); 43 | break; 44 | case "put": 45 | command = new PutObjectCommand({ 46 | Bucket: bucketName, 47 | Key: objectKey, 48 | }); 49 | break; 50 | default: 51 | throw new Error( 52 | "[presigned-url.ts] Invalid request type. It should be either 'put' or 'get'." 53 | ); 54 | } 55 | 56 | const signedUrl = await getSignedUrl(client as any, command as any, { 57 | expiresIn: 3600, 58 | }); 59 | 60 | return signedUrl; 61 | } catch (err) { 62 | console.error("Error generating presigned URL:", err); 63 | throw err; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/app-router-example/app/api/upload/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/upload.ts 2 | import { S3Client } from "@aws-sdk/client-s3"; 3 | import { NextResponse } from "next/server"; 4 | import { getAwsConsoleUrl, getPresignedUrlS3 } from "./helper"; 5 | 6 | const s3Client = new S3Client({ 7 | region: `${process.env.AWS_REGION}`, 8 | credentials: { 9 | accessKeyId: `${process.env.AWS_ACCESS_KEY_ID}`, 10 | secretAccessKey: `${process.env.AWS_SECRET_ACCESS_KEY}`, 11 | }, 12 | }); 13 | 14 | export async function POST(req: Request) { 15 | const body = await req.json(); 16 | const { objectKey, requestType } = body; 17 | 18 | if (!objectKey) { 19 | return NextResponse.json( 20 | { error: "[api/upload/route.ts] Object key is required" }, 21 | { status: 400 } 22 | ); 23 | } 24 | 25 | if (!requestType) { 26 | return NextResponse.json( 27 | { error: "[api/upload/route.ts] Request type is required" }, 28 | { status: 400 } 29 | ); 30 | } 31 | 32 | try { 33 | const signedUrl = await getPresignedUrlS3({ 34 | bucketName: `${process.env.S3_BUCKET_NAME}`, 35 | objectKey, 36 | requestType, 37 | client: s3Client, 38 | }); 39 | 40 | const awsConsoleUrl = getAwsConsoleUrl( 41 | `${process.env.S3_BUCKET_NAME}`, 42 | `${process.env.AWS_REGION}`, 43 | objectKey 44 | ); 45 | 46 | return NextResponse.json({ signedUrl, awsConsoleUrl }); 47 | } catch (err) { 48 | console.error("API Error:", err); 49 | return NextResponse.json( 50 | { error: "Failed to generate presigned URL." }, 51 | { status: 500 } 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/app-router-example/app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import "../styles/globals.css"; 4 | import { Footer, Header } from "@/utils/components/Common"; 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | 14 |
15 | {children} 16 |