From feb911aea06bacf58ea933d2803a2a89fe36e57b Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Sat, 9 Apr 2022 09:28:08 +0200 Subject: [PATCH] feat(s3): implement non-ephemeral storage through S3/DO Spaces --- .env.example | 6 + CHANGELOG.md | 2 - client/next.config.js | 2 +- docker-compose.yml | 6 + docs/docs/source-code/env-vars.mdx | 28 + package.json | 2 +- pnpm-lock.yaml | 864 +++++++++++++++++++++++++ server/package.json | 1 + server/src/config/config.module.ts | 11 +- server/src/config/storage.config.ts | 10 + server/src/resume/resume.controller.ts | 2 +- server/src/resume/resume.module.ts | 24 +- server/src/resume/resume.service.ts | 53 +- 13 files changed, 973 insertions(+), 38 deletions(-) create mode 100644 server/src/config/storage.config.ts diff --git a/.env.example b/.env.example index 299ae0db..a20dae65 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,12 @@ SENDGRID_API_KEY= SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID= SENDGRID_FROM_NAME= SENDGRID_FROM_EMAIL= +STORAGE_BUCKET= +STORAGE_REGION= +STORAGE_ENDPOINT= +STORAGE_URL_PREFIX= +STORAGE_ACCESS_KEY= +STORAGE_SECRET_KEY= # Flags PUBLIC_FLAG_DISABLE_SIGNUPS=false \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e1f7d1..56b960de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,6 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. -### [3.3.3](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.2...v3.3.3) (2022-04-09) - ### [3.3.2](https://github.com/AmruthPillai/Reactive-Resume/compare/v3.3.1...v3.3.2) (2022-04-08) diff --git a/client/next.config.js b/client/next.config.js index 6c7e4615..d82dd7e8 100644 --- a/client/next.config.js +++ b/client/next.config.js @@ -12,7 +12,7 @@ const nextConfig = { }, images: { - domains: ['www.gravatar.com'], + domains: ['cdn.rxresu.me', 'www.gravatar.com'], }, async rewrites() { diff --git a/docker-compose.yml b/docker-compose.yml index fcfff0f7..0d5a4553 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,12 @@ x-env-server: &env-server - SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID= - SENDGRID_FROM_NAME= - SENDGRID_FROM_EMAIL= + - STORAGE_BUCKET= + - STORAGE_REGION= + - STORAGE_ENDPOINT= + - STORAGE_URL_PREFIX= + - STORAGE_ACCESS_KEY= + - STORAGE_SECRET_KEY= x-env-flags: &env-flags environment: diff --git a/docs/docs/source-code/env-vars.mdx b/docs/docs/source-code/env-vars.mdx index 84717d70..cd4bc10e 100644 --- a/docs/docs/source-code/env-vars.mdx +++ b/docs/docs/source-code/env-vars.mdx @@ -161,3 +161,31 @@ You can get your own key here: https://docs.sendgrid.com/ui/account-and-settings **Required**: `no` **Description:** Sender's Email Address + +## Storage + +You can either use S3 or any S3-compliant service such as DigitalOcean Spaces to store profile pictures uploaded by users of the platform. + +### `STORAGE_BUCKET` + +**Required**: `yes` + +### `STORAGE_REGION` + +**Required**: `yes` + +### `STORAGE_ENDPOINT` + +**Required**: `yes` + +### `STORAGE_URL_PREFIX` + +**Required**: `yes` + +### `STORAGE_ACCESS_KEY` + +**Required**: `yes` + +### `STORAGE_SECRET_KEY` + +**Required**: `yes` diff --git a/package.json b/package.json index 883f5de3..bb3be29a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactive-resume", - "version": "3.3.3", + "version": "3.3.2", "private": true, "workspaces": [ "schema", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f47c89eb..e343ce6d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -205,6 +205,7 @@ importers: server: specifiers: + '@aws-sdk/client-s3': ^3.67.0 '@nestjs/axios': ^0.0.7 '@nestjs/cli': ^8.2.5 '@nestjs/common': ^8.4.4 @@ -260,6 +261,7 @@ importers: uuid: ^8.3.2 webpack: ^5.72.0 dependencies: + '@aws-sdk/client-s3': 3.67.0 '@nestjs/axios': 0.0.7_7670df531e7a1503cdbe395599f41db7 '@nestjs/common': 8.4.4_ea528571bae7e3437ac6e354bb00c0f4 '@nestjs/config': 2.0.0_7670df531e7a1503cdbe395599f41db7 @@ -515,6 +517,859 @@ packages: - chokidar dev: true + /@aws-crypto/crc32/2.0.0: + resolution: {integrity: sha512-TvE1r2CUueyXOuHdEigYjIZVesInd9KN+K/TFFNfkkxRThiNxO6i4ZqqAVMoEjAamZZ1AA8WXJkjCz7YShHPQA==} + dependencies: + '@aws-crypto/util': 2.0.1 + '@aws-sdk/types': 3.55.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/crc32c/2.0.0: + resolution: {integrity: sha512-vF0eMdMHx3O3MoOXUfBZry8Y4ZDtcuskjjKgJz8YfIDjLStxTZrYXk+kZqtl6A0uCmmiN/Eb/JbC/CndTV1MHg==} + dependencies: + '@aws-crypto/util': 2.0.1 + '@aws-sdk/types': 3.55.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/ie11-detection/2.0.0: + resolution: {integrity: sha512-pkVXf/dq6PITJ0jzYZ69VhL8VFOFoPZLZqtU/12SGnzYuJOOGNfF41q9GxdI1yqC8R13Rq3jOLKDFpUJFT5eTA==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha1-browser/2.0.0: + resolution: {integrity: sha512-3fIVRjPFY8EG5HWXR+ZJZMdWNRpwbxGzJ9IH9q93FpbgCH8u8GHRi46mZXp3cYD7gealmyqpm3ThZwLKJjWJhA==} + dependencies: + '@aws-crypto/ie11-detection': 2.0.0 + '@aws-crypto/supports-web-crypto': 2.0.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-locate-window': 3.55.0 + '@aws-sdk/util-utf8-browser': 3.55.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-browser/2.0.0: + resolution: {integrity: sha512-rYXOQ8BFOaqMEHJrLHul/25ckWH6GTJtdLSajhlqGMx0PmSueAuvboCuZCTqEKlxR8CQOwRarxYMZZSYlhRA1A==} + dependencies: + '@aws-crypto/ie11-detection': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-crypto/supports-web-crypto': 2.0.0 + '@aws-crypto/util': 2.0.1 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-locate-window': 3.55.0 + '@aws-sdk/util-utf8-browser': 3.55.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-js/2.0.0: + resolution: {integrity: sha512-VZY+mCY4Nmrs5WGfitmNqXzaE873fcIZDu54cbaDaaamsaTOP1DBImV9F4pICc3EHjQXujyE8jig+PFCaew9ig==} + dependencies: + '@aws-crypto/util': 2.0.1 + '@aws-sdk/types': 3.55.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/supports-web-crypto/2.0.0: + resolution: {integrity: sha512-Ge7WQ3E0OC7FHYprsZV3h0QIcpdyJLvIeg+uTuHqRYm8D6qCFJoiC+edSzSyFiHtZf+NOQDJ1q46qxjtzIY2nA==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/util/2.0.1: + resolution: {integrity: sha512-JJmFFwvbm08lULw4Nm5QOLg8+lAQeC8aCXK5xrtxntYzYXCGfHwUJ4Is3770Q7HmICsXthGQ+ZsDL7C2uH3yBQ==} + dependencies: + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-utf8-browser': 3.55.0 + tslib: 1.14.1 + dev: false + + /@aws-sdk/abort-controller/3.55.0: + resolution: {integrity: sha512-rCcTxJDEFnmvo/PgbhCRv24/Uv03lEGfRslKZq7SjaMcOubflS/ZXYaMEgsjYHgAT0zlpSsyCIkJXmhFaM7H7w==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/chunked-blob-reader-native/3.58.0: + resolution: {integrity: sha512-+D3xnPD5985iphgAqgUerBDs371a2WzzoEVi7eHJUMMsP/gEnSTdSH0HNxsqhYv6CW4EdKtvDAQdAwA1VtCf2A==} + dependencies: + '@aws-sdk/util-base64-browser': 3.58.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/chunked-blob-reader/3.55.0: + resolution: {integrity: sha512-o/xjMCq81opAjSBjt7YdHJwIJcGVG5XIV9+C2KXcY5QwVimkOKPybWTv0mXPvSwSilSx+EhpLNhkcJuXdzhw4w==} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/client-s3/3.67.0: + resolution: {integrity: sha512-+wSWhkxXG8mY5rlsTXdDaYULpzq8AZ3B2TZKVGEXb6zs1txedyWSYeZr6ENpBoxtJepmlwXDC7oRm39eVoRixw==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-crypto/sha1-browser': 2.0.0 + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/client-sts': 3.67.0 + '@aws-sdk/config-resolver': 3.58.0 + '@aws-sdk/credential-provider-node': 3.67.0 + '@aws-sdk/eventstream-serde-browser': 3.58.0 + '@aws-sdk/eventstream-serde-config-resolver': 3.55.0 + '@aws-sdk/eventstream-serde-node': 3.58.0 + '@aws-sdk/fetch-http-handler': 3.58.0 + '@aws-sdk/hash-blob-browser': 3.58.0 + '@aws-sdk/hash-node': 3.55.0 + '@aws-sdk/hash-stream-node': 3.58.0 + '@aws-sdk/invalid-dependency': 3.55.0 + '@aws-sdk/md5-js': 3.58.0 + '@aws-sdk/middleware-bucket-endpoint': 3.58.0 + '@aws-sdk/middleware-content-length': 3.58.0 + '@aws-sdk/middleware-expect-continue': 3.58.0 + '@aws-sdk/middleware-flexible-checksums': 3.58.0 + '@aws-sdk/middleware-host-header': 3.58.0 + '@aws-sdk/middleware-location-constraint': 3.55.0 + '@aws-sdk/middleware-logger': 3.55.0 + '@aws-sdk/middleware-retry': 3.58.0 + '@aws-sdk/middleware-sdk-s3': 3.66.0 + '@aws-sdk/middleware-serde': 3.55.0 + '@aws-sdk/middleware-signing': 3.58.0 + '@aws-sdk/middleware-ssec': 3.55.0 + '@aws-sdk/middleware-stack': 3.55.0 + '@aws-sdk/middleware-user-agent': 3.58.0 + '@aws-sdk/node-config-provider': 3.58.0 + '@aws-sdk/node-http-handler': 3.58.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/signature-v4-multi-region': 3.66.0 + '@aws-sdk/smithy-client': 3.55.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/url-parser': 3.55.0 + '@aws-sdk/util-base64-browser': 3.58.0 + '@aws-sdk/util-base64-node': 3.55.0 + '@aws-sdk/util-body-length-browser': 3.55.0 + '@aws-sdk/util-body-length-node': 3.55.0 + '@aws-sdk/util-defaults-mode-browser': 3.55.0 + '@aws-sdk/util-defaults-mode-node': 3.58.0 + '@aws-sdk/util-stream-browser': 3.55.0 + '@aws-sdk/util-stream-node': 3.55.0 + '@aws-sdk/util-user-agent-browser': 3.58.0 + '@aws-sdk/util-user-agent-node': 3.58.0 + '@aws-sdk/util-utf8-browser': 3.55.0 + '@aws-sdk/util-utf8-node': 3.55.0 + '@aws-sdk/util-waiter': 3.55.0 + '@aws-sdk/xml-builder': 3.55.0 + entities: 2.2.0 + fast-xml-parser: 3.19.0 + tslib: 2.3.1 + transitivePeerDependencies: + - '@aws-sdk/signature-v4-crt' + dev: false + + /@aws-sdk/client-sso/3.67.0: + resolution: {integrity: sha512-njBLSqX2+4eTjeptODxdYgBC8cYwwNE3VwrIFgf+nosIo8Ll07evZgHhfZsyURou0cUUpxFInDf1KaAotH9lBQ==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/config-resolver': 3.58.0 + '@aws-sdk/fetch-http-handler': 3.58.0 + '@aws-sdk/hash-node': 3.55.0 + '@aws-sdk/invalid-dependency': 3.55.0 + '@aws-sdk/middleware-content-length': 3.58.0 + '@aws-sdk/middleware-host-header': 3.58.0 + '@aws-sdk/middleware-logger': 3.55.0 + '@aws-sdk/middleware-retry': 3.58.0 + '@aws-sdk/middleware-serde': 3.55.0 + '@aws-sdk/middleware-stack': 3.55.0 + '@aws-sdk/middleware-user-agent': 3.58.0 + '@aws-sdk/node-config-provider': 3.58.0 + '@aws-sdk/node-http-handler': 3.58.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/smithy-client': 3.55.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/url-parser': 3.55.0 + '@aws-sdk/util-base64-browser': 3.58.0 + '@aws-sdk/util-base64-node': 3.55.0 + '@aws-sdk/util-body-length-browser': 3.55.0 + '@aws-sdk/util-body-length-node': 3.55.0 + '@aws-sdk/util-defaults-mode-browser': 3.55.0 + '@aws-sdk/util-defaults-mode-node': 3.58.0 + '@aws-sdk/util-user-agent-browser': 3.58.0 + '@aws-sdk/util-user-agent-node': 3.58.0 + '@aws-sdk/util-utf8-browser': 3.55.0 + '@aws-sdk/util-utf8-node': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/client-sts/3.67.0: + resolution: {integrity: sha512-gTWQU/4tPQLMJpLXgSbUss5s5dsyxpXJW2gWosvLLtX5QlchGBzWEawoA3QIxFRkNJrXqLyaVDBdFFqB+l0mVQ==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 2.0.0 + '@aws-crypto/sha256-js': 2.0.0 + '@aws-sdk/config-resolver': 3.58.0 + '@aws-sdk/credential-provider-node': 3.67.0 + '@aws-sdk/fetch-http-handler': 3.58.0 + '@aws-sdk/hash-node': 3.55.0 + '@aws-sdk/invalid-dependency': 3.55.0 + '@aws-sdk/middleware-content-length': 3.58.0 + '@aws-sdk/middleware-host-header': 3.58.0 + '@aws-sdk/middleware-logger': 3.55.0 + '@aws-sdk/middleware-retry': 3.58.0 + '@aws-sdk/middleware-sdk-sts': 3.58.0 + '@aws-sdk/middleware-serde': 3.55.0 + '@aws-sdk/middleware-signing': 3.58.0 + '@aws-sdk/middleware-stack': 3.55.0 + '@aws-sdk/middleware-user-agent': 3.58.0 + '@aws-sdk/node-config-provider': 3.58.0 + '@aws-sdk/node-http-handler': 3.58.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/smithy-client': 3.55.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/url-parser': 3.55.0 + '@aws-sdk/util-base64-browser': 3.58.0 + '@aws-sdk/util-base64-node': 3.55.0 + '@aws-sdk/util-body-length-browser': 3.55.0 + '@aws-sdk/util-body-length-node': 3.55.0 + '@aws-sdk/util-defaults-mode-browser': 3.55.0 + '@aws-sdk/util-defaults-mode-node': 3.58.0 + '@aws-sdk/util-user-agent-browser': 3.58.0 + '@aws-sdk/util-user-agent-node': 3.58.0 + '@aws-sdk/util-utf8-browser': 3.55.0 + '@aws-sdk/util-utf8-node': 3.55.0 + entities: 2.2.0 + fast-xml-parser: 3.19.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/config-resolver/3.58.0: + resolution: {integrity: sha512-NXEwYw0JrXcvenu42QpNMQXK+6pgZ+6bDGfCgOfCC0FmyI+w/CuF36lApwm7InHvHazOaDlwArXm2pfntErKoA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/signature-v4': 3.58.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-config-provider': 3.55.0 + '@aws-sdk/util-middleware': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/credential-provider-env/3.55.0: + resolution: {integrity: sha512-4AIIXEdvinLlWNFtrUbUgoB7dkuV04RTcTruVWI4Ub4WSsuSCa72ZU1vqyvcEAOgGGLBmcSaGTWByjiD2sGcGA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/credential-provider-imds/3.58.0: + resolution: {integrity: sha512-CdtnTQ9zqLx1FbXdbgjijLbMcIWOyQM03TFaLSCjI3FNbUwyt3T7StBU9tj/LtbypHhSdXyQBpzUtXTOMWCEhg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/node-config-provider': 3.58.0 + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/url-parser': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/credential-provider-ini/3.67.0: + resolution: {integrity: sha512-47uNhLsd2eKWOa+alsyoT48TbtIfYtIJmC85APunHczqfpUC1YHOyHZ3pCzByGrgx4khjnJ323gfq9kreEHCvA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.55.0 + '@aws-sdk/credential-provider-imds': 3.58.0 + '@aws-sdk/credential-provider-sso': 3.67.0 + '@aws-sdk/credential-provider-web-identity': 3.55.0 + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/shared-ini-file-loader': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/credential-provider-node/3.67.0: + resolution: {integrity: sha512-nxBUJ4rANue9MWQyhrmkZwuKVVpwbUXEqyMwJD1uxEZnYXksmAoVxKW/m4vxy1nh/65wAFCCipeLmqYzwJ8m0A==} + engines: {node: '>=12.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.55.0 + '@aws-sdk/credential-provider-imds': 3.58.0 + '@aws-sdk/credential-provider-ini': 3.67.0 + '@aws-sdk/credential-provider-process': 3.58.0 + '@aws-sdk/credential-provider-sso': 3.67.0 + '@aws-sdk/credential-provider-web-identity': 3.55.0 + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/shared-ini-file-loader': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/credential-provider-process/3.58.0: + resolution: {integrity: sha512-npgFqPUjMhUamf1FvJjBYUdpbWx8XWkKCwJsX73I7IYQAvAi2atCOkdtKq+4rds0VWAYu6vzlaI1tXgFxjOPNQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/shared-ini-file-loader': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/credential-provider-sso/3.67.0: + resolution: {integrity: sha512-AwS7tvA++2li0+yZkiCL1zk56EC3N6fDXWDqPEG/lKuviwyv1B+up6r/6ks7ADWbXuOKzrmslS7rn3DT2ZUkig==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.67.0 + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/shared-ini-file-loader': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/credential-provider-web-identity/3.55.0: + resolution: {integrity: sha512-aKnXfZNGohTuF9rCGYLg4JEIOvWIZ/sb66XMq7bOUrx13KRPDwL/eUQL8quS5jGRLpjXVNvrS17AFf65GbdUBg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/eventstream-marshaller/3.58.0: + resolution: {integrity: sha512-vTdVFLIHGZTx/Anp9GpkTXVuvwSCNOecTutU5Py4i6fATgefWiSutc5Xc/FLujBSc0EhAXDGZIcTMpZC7jUpeg==} + dependencies: + '@aws-crypto/crc32': 2.0.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-hex-encoding': 3.58.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/eventstream-serde-browser/3.58.0: + resolution: {integrity: sha512-oR5yoOoJrTSUKwbxZSt37bZgMXUUSsOub96E6SOb8wh8TMq2f0wvqeO8A+aaxY487gKpzuVUClp7jSQ9LgiVcw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/eventstream-marshaller': 3.58.0 + '@aws-sdk/eventstream-serde-universal': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/eventstream-serde-config-resolver/3.55.0: + resolution: {integrity: sha512-NTJHLq1sbXyXAaJucKvcdN3Svr/fM2TjHEC3l8P/torFjIsX1+Ykpi8tZt8KsX8RjoUTTfKylh41AjJq0K9X4Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/eventstream-serde-node/3.58.0: + resolution: {integrity: sha512-U1DnRVfvKOXty+Bei6oqhRWFzGWzxl0OFHtev9GzC7BE/E6s4Gn695o+NO+9IwQgjOlc/JsGyAcWevq3MDxymg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/eventstream-marshaller': 3.58.0 + '@aws-sdk/eventstream-serde-universal': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/eventstream-serde-universal/3.58.0: + resolution: {integrity: sha512-w7czmMNvCCspJi8Ij0lTByCiuYBhyNzYTM1wv33vtF7dL+FJgi4W4c5WFAOtvpsPulobY013TWCjPJG+V0IPGQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/eventstream-marshaller': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/fetch-http-handler/3.58.0: + resolution: {integrity: sha512-timF3FjPV5Bd+Kgph83LIKVlPCFObVYzious1a6doeLAT6YFwZpRrWbfP/HzS+DCoYiwUsH69oVJ91BoV66oyA==} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/querystring-builder': 3.55.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-base64-browser': 3.58.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/hash-blob-browser/3.58.0: + resolution: {integrity: sha512-fdp12BqypRxwvevbJSl/sUhXJRi4Ghv6JKEXAHI1klkR6xY1GRORO5SHWltVY/xl373ERMol5o/n+ra/7jcx/g==} + dependencies: + '@aws-sdk/chunked-blob-reader': 3.55.0 + '@aws-sdk/chunked-blob-reader-native': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/hash-node/3.55.0: + resolution: {integrity: sha512-2UdYwY/++AlzWEAFaK9wOed2QSxbzV527vmqKjReLHpPKPrSIlooUxlTH3LU6Y6WVDAzDRtLK43KUVXTLgGK1A==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-buffer-from': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/hash-stream-node/3.58.0: + resolution: {integrity: sha512-y7HEeC3OiuXCRqsHnKDn5yef8UAbnegD9r+OM9bdD+3e6FLAL8Rq7hQTOpwIAiPXuD7HKx8h98s9JLvkwTOBkg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/invalid-dependency/3.55.0: + resolution: {integrity: sha512-delH0lV+78fdD/8MXIt9kTLS6IwHvdhqq9dw/ow5VjTUw+xBwUlfPfZplaai+3hKTKWh6a2WZCeDasNItBv9aA==} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/is-array-buffer/3.55.0: + resolution: {integrity: sha512-NbiPHVYuPxdqdFd6FxzzN3H1BQn/iWA3ri3Ry7AyLeP/tGs1yzEWMwf8BN8TSMALI0GXT6Sh0GDWy3Ok5xB6DA==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/md5-js/3.58.0: + resolution: {integrity: sha512-V5f4Re+CLn3aDF1nrmDqdUtcqBHCyxxD2s2Ot+hZ2JFit+OtJggo1cI03ldTrQpG79rwHG+bHqL2VvNQP7Aj9A==} + dependencies: + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-utf8-browser': 3.55.0 + '@aws-sdk/util-utf8-node': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-bucket-endpoint/3.58.0: + resolution: {integrity: sha512-zocLfFzj+NQjXLGZKPJBAYWWldAKBJkGzGVpTfrYx9bxxHTA70Gu+3sx+Xe+iOu8dtQT0OAnIX0wGudOPnTGNg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-arn-parser': 3.55.0 + '@aws-sdk/util-config-provider': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-content-length/3.58.0: + resolution: {integrity: sha512-h/BypPkhjv2CpCUbXA8Fa2s7V2GPiz9l11XhYK+sKSuQvQ7Lbq6VhaKaLqfeD3gLVZHgJZSLGl2btdHV1qHNNA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-expect-continue/3.58.0: + resolution: {integrity: sha512-nx6X6qLPwvbJrGoPxXSu4tsOek2eRnnjk78hhRUDfxFewpHJQLSPlyNKkXAo+C3syVALe6RJRmUYu5bShY6FfA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/middleware-header-default': 3.58.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-flexible-checksums/3.58.0: + resolution: {integrity: sha512-R8S3U1boaIb7+kYhLJBks7rv/eaGj7I5T/2CgmcGY1BJBUU0h0arjPC7eeA/5wV29EHapoxVYQvJda//706rCw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-crypto/crc32': 2.0.0 + '@aws-crypto/crc32c': 2.0.0 + '@aws-sdk/is-array-buffer': 3.55.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-header-default/3.58.0: + resolution: {integrity: sha512-7F+CdLLauMmNbwFGYrE2pKsgTKY8G2PgazHmaE9s3FySEFcGPWmiEAG8sVImfZooj8gxGFQMLr97nanWjhSq2Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-host-header/3.58.0: + resolution: {integrity: sha512-q/UKGcanm9e6DBRNN6UKhVqLvpRRdZWbmmPCeDNr4HqhCmgT6i1OvWdhAMOnT++hvCX8DpTsIXzNSlY6zWAxBg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-location-constraint/3.55.0: + resolution: {integrity: sha512-OvCKwBFbl8Gbfk0HGX00pkdORJN8BPuH/O5l3+mOBWuwILPuckRP5WGnL+1HT/gu4hHS6h1lpxUrPxUOoeKIAg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-logger/3.55.0: + resolution: {integrity: sha512-PtRbVrxEzDmeV9prBIP4/9or7R5Dj66mjbFSvNRGZ0n+UBfBFfVRfNrhQPNzQpfV9A3KVl9YyWCVXDSW+/rk9Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-retry/3.58.0: + resolution: {integrity: sha512-sfSq+t0Yy47DQwrWGpA8iOx9sd26l4l1JDVTwHNi7+OKD4ClRPVCEdw3bTbbyYz/PV4f9AEfAZ6jwtSff4wkGw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/service-error-classification': 3.55.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-middleware': 3.55.0 + tslib: 2.3.1 + uuid: 8.3.2 + dev: false + + /@aws-sdk/middleware-sdk-s3/3.66.0: + resolution: {integrity: sha512-4ACAdKAZkIjEK99UwoaKTrTGhS7qGqyLmjiGHlzR0ggMUUVmlep7EtcluImFtT6pi+ANVLDzuZGa+95MwGY/Qg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-arn-parser': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-sdk-sts/3.58.0: + resolution: {integrity: sha512-HUz7MhcsSDDTGygOwL61l4voc0pZco06J3z06JjTX19D5XxcQ7hSCtkHHHz0oMb9M1himVSiEon2tjhjsnB99g==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.58.0 + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/signature-v4': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-serde/3.55.0: + resolution: {integrity: sha512-NkEbTDrSZcC2NhuvfjXHKJEl0xgI2B5tMAwi/rMOq/TEnARwVUL9qAy+5lgeiPCqebiNllWatARrFgAaYf0VeA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-signing/3.58.0: + resolution: {integrity: sha512-4FXubHB66GbhyZUlo6YPQoWpYfED15GNbEmHbJLSONzrVzZR3IkViSPLasDngVm1a050JqKuqNkFYGJBP4No/Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/signature-v4': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-ssec/3.55.0: + resolution: {integrity: sha512-HTdA23hksOphQe0TmYORsa/kMNnKRGbdh0VJcsDGHQScJXzJ+C//THwfcoklff0XZfC+vGh93PECBWqixMELZw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-stack/3.55.0: + resolution: {integrity: sha512-ouD+wFz8W2R0ZQ8HrbhgN8tg1jyINEg9lPEEXY79w1Q5sf94LJ90XKAMVk02rw3dJalUWjLHf0OQe1/qxZfHyA==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/middleware-user-agent/3.58.0: + resolution: {integrity: sha512-1c69bIWM63JwXijXvb9IWwcwQ/gViKMZ1lhxv52NvdG5VSxWXXsFJ2jETEXZoAypwT97Hmf3xo9SYuaHcKoq+g==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/node-config-provider/3.58.0: + resolution: {integrity: sha512-AMcPqPhKxo/3/yOMS9PsKlI0GWp2/8eD6gSlhzdBpznPCKplyqXOSnSX7wS814Cyh373hFSjCaOrCOA9/EYtDg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/shared-ini-file-loader': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/node-http-handler/3.58.0: + resolution: {integrity: sha512-D9xVZG2nfo4GbPsby3JuBiAhpqXTFk1+CfuQU0AZv0gQvE3fFTCnB3za83jo7JV/pyRPU+s+/LHIpxCWUHzStg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.55.0 + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/querystring-builder': 3.55.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/property-provider/3.55.0: + resolution: {integrity: sha512-o7cKFJSHq5WOhwPsspYrzNto35oKKZvESZuWDtLxaZKSI6l7zpA366BI4kDG6Tc9i2+teV553MbxyZ9eya5A8g==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/protocol-http/3.58.0: + resolution: {integrity: sha512-0yFFRPbR+CCa9eOQBBQ2qtrIDLYqSMN0y7G4iqVM8wQdIw7n3QK1PsTI3RNPGJ3Oi2krFTw5uUKqQQZPZEBuVQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/querystring-builder/3.55.0: + resolution: {integrity: sha512-/ZAXNipt9nRR8k+eowwukE/YjXnQ49p5w/MkaQxsBk3IuIf7MAcgVg8glHr0igH84GfUQ7ZVP8v+G2S3tKUG+Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-uri-escape': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/querystring-parser/3.55.0: + resolution: {integrity: sha512-e+2FLgo+eDx7oh7ap5HngN9XSVMxredAVztLHxCcSN0lFHHHzMa8b2SpXbaowUxQHh7ziymSqvOrPYFQ71Filg==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/service-error-classification/3.55.0: + resolution: {integrity: sha512-HdjnDyarsa1Avq1MJurkLyEe9c3eRa76dPmK4TmRGgwJ+tInEzGHL0rBW7V8xBK+PDF+fJQ71hvm8jPYmzvBwQ==} + engines: {node: '>= 12.0.0'} + dev: false + + /@aws-sdk/shared-ini-file-loader/3.58.0: + resolution: {integrity: sha512-ARDKQerIzgNs/MFNdCEuK2lgRJ1lneAaJw0p9O1LkJUvcSibvkSATwny7vwJMueOf+ae1Pf+8+54OMNIt0nTkQ==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/signature-v4-multi-region/3.66.0: + resolution: {integrity: sha512-Akvc8G9Del2+umg0R/5Gc/PWgQwbxxTXdnm6YTHtDzvyPPiYWBs6au6WqJQqcqk07gcQV67MLVqFFhnFuLlcVg==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@aws-sdk/signature-v4-crt': ^3.66.0 + peerDependenciesMeta: + '@aws-sdk/signature-v4-crt': + optional: true + dependencies: + '@aws-sdk/protocol-http': 3.58.0 + '@aws-sdk/signature-v4': 3.58.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-arn-parser': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/signature-v4/3.58.0: + resolution: {integrity: sha512-flEo8p3XkzWoBDqnIUQre4jLuT5aLnmfQNI8c2uSjyJ3OBxpJ0iS1cDu3E++d1/pN6Q8o0KOmr2ypHeiyBOujw==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.55.0 + '@aws-sdk/types': 3.55.0 + '@aws-sdk/util-hex-encoding': 3.58.0 + '@aws-sdk/util-middleware': 3.55.0 + '@aws-sdk/util-uri-escape': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/smithy-client/3.55.0: + resolution: {integrity: sha512-YgBpqg6R3Qg8CH9biOP1N1lYTvh8VLGD6AoDGgy/R1dQSqRQuxgKANLl3DOVcZnIZLsw4TdB0m7U+ZPtirPR1Q==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/middleware-stack': 3.55.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/types/3.55.0: + resolution: {integrity: sha512-wrDZjuy1CVAYxDCbm3bWQIKMGfNs7XXmG0eG4858Ixgqmq2avsIn5TORy8ynBxcXn9aekV/+tGEQ7BBSYzIVNQ==} + engines: {node: '>= 12.0.0'} + dev: false + + /@aws-sdk/url-parser/3.55.0: + resolution: {integrity: sha512-qrTwN5xIgTLreqLnZ+x3cAudjNKfxi6srW1H/px2mk4lb2U9B4fpGjZ6VU+XV8U2kR+YlT8J6Jo5iwuVGfC91A==} + dependencies: + '@aws-sdk/querystring-parser': 3.55.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-arn-parser/3.55.0: + resolution: {integrity: sha512-76KJxp4MRWufHYWys7DFl64znr5yeJ3AIQNAPCKKw1sP0hzO7p6Kx0PaJnw9x+CPSzOrT4NbuApL6/srYhKDGg==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-base64-browser/3.58.0: + resolution: {integrity: sha512-0ebsXIZNpu/fup9OgsFPnRKfCFbuuI9PPRzvP6twzLxUB0c/aix6Co7LGHFKcRKHZdaykoJMXArf8eHj2Nzv1Q==} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-base64-node/3.55.0: + resolution: {integrity: sha512-UQ/ZuNoAc8CFMpSiRYmevaTsuRKzLwulZTnM8LNlIt9Wx1tpNvqp80cfvVj7yySKROtEi20wq29h31dZf1eYNQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-body-length-browser/3.55.0: + resolution: {integrity: sha512-Ei2OCzXQw5N6ZkTMZbamUzc1z+z1R1Ja5tMEagz5BxuX4vWdBObT+uGlSzL8yvTbjoPjnxWA2aXyEqaUP3JS8Q==} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-body-length-node/3.55.0: + resolution: {integrity: sha512-lU1d4I+9wJwydduXs0SxSfd+mHKjxeyd39VwOv6i2KSwWkPbji9UQqpflKLKw+r45jL7+xU/zfeTUg5Tt/3Gew==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-buffer-from/3.55.0: + resolution: {integrity: sha512-uVzKG1UgvnV7XX2FPTylBujYMKBPBaq/qFBxfl0LVNfrty7YjpfieQxAe6yRLD+T0Kir/WDQwGvYC+tOYG3IGA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/is-array-buffer': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-config-provider/3.55.0: + resolution: {integrity: sha512-30dzofQQfx6tp1jVZkZ0DGRsT0wwC15nEysKRiAcjncM64A0Cm6sra77d0os3vbKiKoPCI/lMsFr4o3533+qvQ==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-defaults-mode-browser/3.55.0: + resolution: {integrity: sha512-OS3gAwR84bHz7ObhjsSJM+grfeaBq3leGrj7xiX4BH3C8J+c10GMo3fqx1pV8Fq5F+9lMmhHpfOocD63SN5Q8A==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/types': 3.55.0 + bowser: 2.11.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-defaults-mode-node/3.58.0: + resolution: {integrity: sha512-KNUCp0MXI+z3Z3pQCKDkx3Stdy1TXDjcUB+ZJFxRTJGIuBYwX4fV6G8s/zeFJi5Qv1ztR3CJ9fWJGsrx9mQ5EA==} + engines: {node: '>= 10.0.0'} + dependencies: + '@aws-sdk/config-resolver': 3.58.0 + '@aws-sdk/credential-provider-imds': 3.58.0 + '@aws-sdk/node-config-provider': 3.58.0 + '@aws-sdk/property-provider': 3.55.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-hex-encoding/3.58.0: + resolution: {integrity: sha512-Rl+jXUzk/FJkOLYfUVYPhKa2aUmTpeobRP31l8IatQltSzDgLyRHO35f6UEs7Ztn5s1jbu/POatLAZ2WjbgVyg==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-locate-window/3.55.0: + resolution: {integrity: sha512-0sPmK2JaJE2BbTcnvybzob/VrFKCXKfN4CUKcvn0yGg/me7Bz+vtzQRB3Xp+YSx+7OtWxzv63wsvHoAnXvgxgg==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-middleware/3.55.0: + resolution: {integrity: sha512-82fW2XV+rUalv8lkd4VlhpPp6xnXO5n9sckMp1N+TrQ+p8eqxqT0+o8n1/6s9Qsnkw64Y3m6+EfCdc8/uFOY2g==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-stream-browser/3.55.0: + resolution: {integrity: sha512-3f/zQsAqexJpKssCL0adTjG8WO+NPQ63E3TingyKpnCnHQPEnqPdya5I5OLGzZ0WR0iUWRtpuW0MtuDabyLDWw==} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-stream-node/3.55.0: + resolution: {integrity: sha512-brCK3iENvXEL7BK5eDAdkZ2VuBSvXj7DH9EQezxl4Ntrj1lvb+McOk9WoU/o7yzE7A/bzEJEoNQAPi+VPNbb/w==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-uri-escape/3.55.0: + resolution: {integrity: sha512-mmdDLUpFCN2nkfwlLdOM54lTD528GiGSPN1qb8XtGLgZsJUmg3uJSFIN2lPeSbEwJB3NFjVas/rnQC48i7mV8w==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-user-agent-browser/3.58.0: + resolution: {integrity: sha512-aJpqCvT09giJRg5xFTBDBRAVF0k0yq3OEf6UTuiOVf5azlL2MGp6PJ/xkJp9Z06PuQQkwBJ/2nIQZemo02a5Sw==} + dependencies: + '@aws-sdk/types': 3.55.0 + bowser: 2.11.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-user-agent-node/3.58.0: + resolution: {integrity: sha512-VlbY/nzWdN2pfLUHqKvnlGBQ6tEeV4jyK9ggAD2Szjj0bkYvaaKwpBKswQmuJpi5/J2v7Bo4ayBLnqDL7PgzLA==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/node-config-provider': 3.58.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-utf8-browser/3.55.0: + resolution: {integrity: sha512-ljzqJcyjfJpEVSIAxwtIS8xMRUly84BdjlBXyp6cu4G8TUufgjNS31LWdhyGhgmW5vYBNr+LTz0Kwf6J+ou7Ug==} + dependencies: + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-utf8-node/3.55.0: + resolution: {integrity: sha512-FsFm7GFaC7j0tlPEm/ri8bU2QCwFW5WKjxUg8lm1oWaxplCpKGUsmcfPJ4sw58GIoyoGu4QXBK60oCWosZYYdQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/util-buffer-from': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/util-waiter/3.55.0: + resolution: {integrity: sha512-Do34MKPFSC/+zVN6vY+FZ+0WN61hzga4nPoAC590AOjs8rW6/H6sDN6Gz1KAZbPnuQUZfvsIJjMxN7lblXHJkQ==} + engines: {node: '>= 12.0.0'} + dependencies: + '@aws-sdk/abort-controller': 3.55.0 + '@aws-sdk/types': 3.55.0 + tslib: 2.3.1 + dev: false + + /@aws-sdk/xml-builder/3.55.0: + resolution: {integrity: sha512-BH+i5S2FLprmfSeIuGy3UbNtEoJPVjh8arl5+LV3i2KY/+TmrS4yT8JtztDlDxHF0cMtNLZNO0KEPtsACS6SOg==} + engines: {node: '>= 12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + /@babel/code-frame/7.16.7: resolution: {integrity: sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==} engines: {node: '>=6.9.0'} @@ -5151,6 +6006,10 @@ packages: resolution: {integrity: sha1-aN/1++YMUes3cl6p4+0xDcwed24=} dev: false + /bowser/2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + /boxen/5.1.2: resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} engines: {node: '>=10'} @@ -7459,6 +8318,11 @@ packages: punycode: 1.4.1 dev: false + /fast-xml-parser/3.19.0: + resolution: {integrity: sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==} + hasBin: true + dev: false + /fastq/1.13.0: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} dependencies: diff --git a/server/package.json b/server/package.json index 76da7488..8bd6296d 100644 --- a/server/package.json +++ b/server/package.json @@ -10,6 +10,7 @@ "lint": "eslint --fix --ext .ts ./src" }, "dependencies": { + "@aws-sdk/client-s3": "^3.67.0", "@nestjs/axios": "^0.0.7", "@nestjs/common": "^8.4.4", "@nestjs/config": "^2.0.0", diff --git a/server/src/config/config.module.ts b/server/src/config/config.module.ts index 4e05ea62..0eabd1a7 100644 --- a/server/src/config/config.module.ts +++ b/server/src/config/config.module.ts @@ -7,6 +7,7 @@ import authConfig from './auth.config'; import databaseConfig from './database.config'; import googleConfig from './google.config'; import sendgridConfig from './sendgrid.config'; +import storageConfig from './storage.config'; const validationSchema = Joi.object({ // App @@ -40,12 +41,20 @@ const validationSchema = Joi.object({ SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID: Joi.string().allow(''), SENDGRID_FROM_NAME: Joi.string().allow(''), SENDGRID_FROM_EMAIL: Joi.string().allow(''), + + // Storage + STORAGE_BUCKET: Joi.string().allow(''), + STORAGE_REGION: Joi.string().allow(''), + STORAGE_ENDPOINT: Joi.string().allow(''), + STORAGE_URL_PREFIX: Joi.string().allow(''), + STORAGE_ACCESS_KEY: Joi.string().allow(''), + STORAGE_SECRET_KEY: Joi.string().allow(''), }); @Module({ imports: [ NestConfigModule.forRoot({ - load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig], + load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig, storageConfig], validationSchema: validationSchema, }), ], diff --git a/server/src/config/storage.config.ts b/server/src/config/storage.config.ts new file mode 100644 index 00000000..2ab05170 --- /dev/null +++ b/server/src/config/storage.config.ts @@ -0,0 +1,10 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs('storage', () => ({ + bucket: process.env.STORAGE_BUCKET, + region: process.env.STORAGE_REGION, + endpoint: process.env.STORAGE_ENDPOINT, + urlPrefix: process.env.STORAGE_URL_PREFIX, + accessKey: process.env.STORAGE_ACCESS_KEY, + secretKey: process.env.STORAGE_SECRET_KEY, +})); diff --git a/server/src/resume/resume.controller.ts b/server/src/resume/resume.controller.ts index decd18e0..7f44762a 100644 --- a/server/src/resume/resume.controller.ts +++ b/server/src/resume/resume.controller.ts @@ -99,7 +99,7 @@ export class ResumeController { @Put(':id/photo') @UseInterceptors(FileInterceptor('file')) async uploadPhoto(@Param('id') id: string, @User('id') userId: number, @UploadedFile() file: Express.Multer.File) { - return this.resumeService.uploadPhoto(+id, userId, file.filename); + return this.resumeService.uploadPhoto(+id, userId, file); } @UseGuards(JwtAuthGuard) diff --git a/server/src/resume/resume.module.ts b/server/src/resume/resume.module.ts index b4c50baf..f501ef1c 100644 --- a/server/src/resume/resume.module.ts +++ b/server/src/resume/resume.module.ts @@ -2,12 +2,9 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { MulterModule } from '@nestjs/platform-express'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { mkdir } from 'fs/promises'; -import { diskStorage } from 'multer'; -import { extname, join } from 'path'; +import { memoryStorage } from 'multer'; import { AuthModule } from '@/auth/auth.module'; -import { User } from '@/users/entities/user.entity'; import { UsersModule } from '@/users/users.module'; import { Resume } from './entities/resume.entity'; @@ -18,24 +15,7 @@ import { ResumeService } from './resume.service'; imports: [ ConfigModule, TypeOrmModule.forFeature([Resume]), - MulterModule.register({ - storage: diskStorage({ - destination: async (req, _, cb) => { - const userId = (req.user as User).id; - const resumeId = +req.params.id; - const destination = join(__dirname, '..', `assets/uploads/${userId}/${resumeId}`); - - await mkdir(destination, { recursive: true }); - - cb(null, destination); - }, - filename: (_, file, cb) => { - const filename = new Date().getTime() + extname(file.originalname); - - cb(null, filename); - }, - }), - }), + MulterModule.register({ storage: memoryStorage() }), AuthModule, UsersModule, ], diff --git a/server/src/resume/resume.service.ts b/server/src/resume/resume.service.ts index 016ed152..8231422f 100644 --- a/server/src/resume/resume.service.ts +++ b/server/src/resume/resume.service.ts @@ -1,11 +1,11 @@ +import { DeleteObjectCommand, PutObjectCommand, S3, S3Client } from '@aws-sdk/client-s3'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Resume as ResumeSchema } from '@reactive-resume/schema'; -import { unlink } from 'fs/promises'; import { pick, sample, set } from 'lodash'; import { nanoid } from 'nanoid'; -import { join } from 'path'; +import { extname } from 'path'; import { Repository } from 'typeorm'; import { PostgresErrorCode } from '@/database/errorCodes.enum'; @@ -22,11 +22,22 @@ export const SHORT_ID_LENGTH = 8; @Injectable() export class ResumeService { + private s3Client: S3Client; + constructor( @InjectRepository(Resume) private resumeRepository: Repository, private configService: ConfigService, private usersService: UsersService - ) {} + ) { + this.s3Client = new S3({ + endpoint: configService.get('storage.endpoint'), + region: configService.get('storage.region'), + credentials: { + accessKeyId: configService.get('storage.accessKey'), + secretAccessKey: configService.get('storage.secretKey'), + }, + }); + } async create(createResumeDto: CreateResumeDto, userId: number) { try { @@ -216,22 +227,44 @@ export class ResumeService { return this.resumeRepository.update(id, nextResume); } - async uploadPhoto(id: number, userId: number, filename: string) { + async uploadPhoto(id: number, userId: number, file: Express.Multer.File) { const resume = await this.findOne(id, userId); - const url = `/api/assets/uploads/${userId}/${id}/${filename}`; - const updatedResume = set(resume, 'basics.photo.url', url); + const urlPrefix = this.configService.get('storage.urlPrefix'); + const filename = new Date().getTime() + extname(file.originalname); + const key = `uploads/${userId}/${id}/${filename}`; + + await this.s3Client.send( + new PutObjectCommand({ + Bucket: this.configService.get('storage.bucket'), + Key: key, + Body: file.buffer, + ACL: 'public-read', + }) + ); + + const publicUrl = urlPrefix + key; + + const updatedResume = set(resume, 'basics.photo.url', publicUrl); return this.resumeRepository.save(updatedResume); } async deletePhoto(id: number, userId: number) { const resume = await this.findOne(id, userId); - const filepath = new URL(resume.basics.photo.url).pathname; - const photoPath = join(__dirname, '..', `assets/${filepath}`); - const updatedResume = set(resume, 'basics.photo.url', ''); - await unlink(photoPath); + const urlPrefix = this.configService.get('storage.urlPrefix'); + const publicUrl = resume.basics.photo.url; + const key = publicUrl.replace(urlPrefix, ''); + + await this.s3Client.send( + new DeleteObjectCommand({ + Bucket: this.configService.get('storage.bucket'), + Key: key, + }) + ); + + const updatedResume = set(resume, 'basics.photo.url', ''); return this.resumeRepository.save(updatedResume); }