diff --git a/.gitignore b/.gitignore index 2c8f1c2..0fd94b3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +public/uploads \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index d91b842..ea3f31f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,11 +1,20 @@ import { fileURLToPath } from "node:url"; import createJiti from "jiti"; + const jiti = createJiti(fileURLToPath(import.meta.url)); // Import env here to validate during build. Using jiti we can import .ts files :) jiti("./src/env.ts"); /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + remotePatterns: [ + { + hostname: "online-store-dev-bkt.s3.us-east-1.amazonaws.com", + }, + ], + }, +}; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 751b5cc..afa5aaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,22 @@ { - "name": "next-accept-payment", + "name": "online-store-nextjs", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "next-accept-payment", + "name": "online-store-nextjs", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@auth/prisma-adapter": "^2.1.0", + "@aws-sdk/client-s3": "^3.577.0", "@aws-sdk/client-ses": "^3.577.0", + "@aws-sdk/s3-request-presigner": "^3.582.0", "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@hookform/resolvers": "^3.4.2", "@mui/material": "^5.15.18", "@mui/material-nextjs": "^5.15.11", "@prisma/client": "^5.14.0", @@ -27,21 +31,27 @@ "@trpc/next": "^11.0.0-rc.373", "@trpc/react-query": "^11.0.0-rc.373", "@trpc/server": "^11.0.0-rc.373", + "bcryptjs": "^2.4.3", "clsx": "^2.1.1", "next": "14.2.3", "next-auth": "^4.24.7", "postcss-import": "^16.1.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.5", "react-icons": "^5.2.1", "stripe": "^15.7.0", "tailwind-merge": "^2.3.0", - "zod": "^3.23.8" + "uuid": "^9.0.1", + "zod": "^3.23.8", + "zod-form-data": "^2.0.2" }, "devDependencies": { + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/uuid": "^9.0.8", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", @@ -139,6 +149,36 @@ "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5" } }, + "node_modules/@aws-crypto/crc32": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", + "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@aws-crypto/crc32c": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-3.0.0.tgz", + "integrity": "sha512-ENNPPManmnVJ4BTXlOjAgD7URidbAznURqD0KvfREyc4o20DPYdEldU1f5cQ7Jbj0CJJSPaMIk/9ZshdB3210w==", + "dependencies": { + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/crc32c/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@aws-crypto/ie11-detection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", @@ -152,6 +192,25 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-3.0.0.tgz", + "integrity": "sha512-NJth5c997GLHs6nOYTzFKTbYdMNA6/1XlKVgnZoaZcQ7z7UJlOgj2JdbHE8tiYLS3fzXNCguct77SPGat2raSw==", + "dependencies": { + "@aws-crypto/ie11-detection": "^3.0.0", + "@aws-crypto/supports-web-crypto": "^3.0.0", + "@aws-crypto/util": "^3.0.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, "node_modules/@aws-crypto/sha256-browser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", @@ -215,6 +274,320 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.582.0.tgz", + "integrity": "sha512-yp3oIN48sQSJ01JF707KcOLAb7+UxcU6uYH0J48AG61z18tJ0SdE7KG2QPEFbK1RRyYXdHd8VLkbTVP+iwCLmw==", + "dependencies": { + "@aws-crypto/sha1-browser": "3.0.0", + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sso-oidc": "3.582.0", + "@aws-sdk/client-sts": "3.582.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.582.0", + "@aws-sdk/middleware-bucket-endpoint": "3.577.0", + "@aws-sdk/middleware-expect-continue": "3.577.0", + "@aws-sdk/middleware-flexible-checksums": "3.577.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-location-constraint": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-sdk-s3": "3.582.0", + "@aws-sdk/middleware-signing": "3.577.0", + "@aws-sdk/middleware-ssec": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/signature-v4-multi-region": "3.582.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@aws-sdk/xml-builder": "3.575.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/eventstream-serde-browser": "^3.0.0", + "@smithy/eventstream-serde-config-resolver": "^3.0.0", + "@smithy/eventstream-serde-node": "^3.0.0", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-blob-browser": "^3.0.0", + "@smithy/hash-node": "^3.0.0", + "@smithy/hash-stream-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/md5-js": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-stream": "^3.0.1", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.582.0.tgz", + "integrity": "sha512-C6G2vNREANe5uUCYrTs8vvGhIrrS1GRoTjr0f5qmkZDuAtuBsQNoTF6Rt+0mDwXXBYW3FcNhZntaNCGVhXlugA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.582.0.tgz", + "integrity": "sha512-g4uiD4GUR03CqY6LwdocJxO+fHSBk/KNXBGJv1ENCcPmK3jpEI8xBggIQOQl3NWjDeP07bpIb8+UhgSoYAYtkg==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sts": "3.582.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.582.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sts": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.582.0.tgz", + "integrity": "sha512-3gaYyQkt8iTSStnjv6kJoPGDJUaPbhcgBOrXhUNbWUgAlgw7Y1aI1MYt3JqvVN4jtiCLwjuiAQATU/8elbqPdQ==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/client-sso-oidc": "3.582.0", + "@aws-sdk/core": "3.582.0", + "@aws-sdk/credential-provider-node": "3.582.0", + "@aws-sdk/middleware-host-header": "3.577.0", + "@aws-sdk/middleware-logger": "3.577.0", + "@aws-sdk/middleware-recursion-detection": "3.577.0", + "@aws-sdk/middleware-user-agent": "3.577.0", + "@aws-sdk/region-config-resolver": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-endpoints": "3.577.0", + "@aws-sdk/util-user-agent-browser": "3.577.0", + "@aws-sdk/util-user-agent-node": "3.577.0", + "@smithy/config-resolver": "^3.0.0", + "@smithy/core": "^2.0.1", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/hash-node": "^3.0.0", + "@smithy/invalid-dependency": "^3.0.0", + "@smithy/middleware-content-length": "^3.0.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/middleware-retry": "^3.0.1", + "@smithy/middleware-serde": "^3.0.0", + "@smithy/middleware-stack": "^3.0.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/url-parser": "^3.0.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.1", + "@smithy/util-defaults-mode-node": "^3.0.1", + "@smithy/util-endpoints": "^2.0.0", + "@smithy/util-middleware": "^3.0.0", + "@smithy/util-retry": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.582.0.tgz", + "integrity": "sha512-ofmD96IQc9g1dbyqlCyxu5fCG7kIl9p1NoN5+vGBUyLdbmPCV3Pdg99nRHYEJuv2MgGx5AUFGDPMHcqbJpnZIw==", + "dependencies": { + "@smithy/core": "^2.0.1", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "fast-xml-parser": "4.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.582.0.tgz", + "integrity": "sha512-kGOUKw5ryPkDIYB69PjK3SicVLTbWB06ouFN2W1EvqUJpkQGPAUGzYcomKtt3mJaCTf/1kfoaHwARAl6KKSP8Q==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/fetch-http-handler": "^3.0.1", + "@smithy/node-http-handler": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/util-stream": "^3.0.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.582.0.tgz", + "integrity": "sha512-GWcjHx6ErcZAi5GZ7kItX7E6ygYmklm9tD9dbCWdsnis7IiWfYZNMXFQEwKCubUmhT61zjGZGDUiRcqVeZu1Aw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.577.0", + "@aws-sdk/credential-provider-process": "3.577.0", + "@aws-sdk/credential-provider-sso": "3.582.0", + "@aws-sdk/credential-provider-web-identity": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@smithy/credential-provider-imds": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.582.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.582.0.tgz", + "integrity": "sha512-T8OLA/2xayRMT8z2eIZgo8tBAamTsBn7HWc8mL1a9yzv5OCPYvucNmbO915DY8u4cNbMl2dcB9frfVxIrahCXw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.577.0", + "@aws-sdk/credential-provider-http": "3.582.0", + "@aws-sdk/credential-provider-ini": "3.582.0", + "@aws-sdk/credential-provider-process": "3.577.0", + "@aws-sdk/credential-provider-sso": "3.582.0", + "@aws-sdk/credential-provider-web-identity": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@smithy/credential-provider-imds": "^3.0.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.582.0.tgz", + "integrity": "sha512-PSiBX6YvJaodGSVg6dReWfeYgK5Tl4fUi0GMuD9WXo/ckfxAPdDFtIfVR6VkSPUrkZj26uw1Pwqeefp2H5phag==", + "dependencies": { + "@aws-sdk/client-sso": "3.582.0", + "@aws-sdk/token-providers": "3.577.0", + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/shared-ini-file-loader": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-ses": { "version": "3.577.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.577.0.tgz", @@ -559,6 +932,55 @@ "@aws-sdk/client-sts": "^3.577.0" } }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.577.0.tgz", + "integrity": "sha512-twlkNX2VofM6kHXzDEiJOiYCc9tVABe5cbyxMArRWscIsCWG9mamPhC77ezG4XsN9dFEwVdxEYD5Crpm/5EUiw==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-config-provider": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.577.0.tgz", + "integrity": "sha512-6dPp8Tv4F0of4un5IAyG6q++GrRrNQQ4P2NAMB1W0VO4JoEu1C8GievbbDLi88TFIFmtKpnHB0ODCzwnoe8JsA==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.577.0.tgz", + "integrity": "sha512-IHAUEipIfagjw92LV8SOSBiCF7ZnqfHcw14IkcZW2/mfrCy1Fh/k40MoS/t3Tro2tQ91rgQPwUoSgB/QCi2Org==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@aws-crypto/crc32c": "3.0.0", + "@aws-sdk/types": "3.577.0", + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.577.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.577.0.tgz", @@ -573,6 +995,19 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.577.0.tgz", + "integrity": "sha512-DKPTD2D2s+t2QUo/IXYtVa/6Un8GZ+phSTBkyBNx2kfZz4Kwavhl/JJzSqTV3GfCXkVdFu7CrjoX7BZ6qWeTUA==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/middleware-logger": { "version": "3.577.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.577.0.tgz", @@ -600,6 +1035,55 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.582.0.tgz", + "integrity": "sha512-PJqQpLoLaZPRI4L/XZUeHkd9UVK8VAr9R38wv0osGeMTvzD9iwzzk0I2TtBqFda/5xEB1YgVYZwyqvmStXmttg==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/node-config-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "@smithy/util-config-provider": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.577.0.tgz", + "integrity": "sha512-QS/dh3+NqZbXtY0j/DZ867ogP413pG5cFGqBy9OeOhDMsolcwLrQbi0S0c621dc1QNq+er9ffaMhZ/aPkyXXIg==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/property-provider": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-middleware": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.577.0.tgz", + "integrity": "sha512-i2BPJR+rp8xmRVIGc0h1kDRFcM2J9GnClqqpc+NLSjmYadlcg4mPklisz9HzwFVcRPJ5XcGf3U4BYs5G8+iTyg==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.577.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.577.0.tgz", @@ -631,6 +1115,40 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/s3-request-presigner": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.582.0.tgz", + "integrity": "sha512-h2tn0IjJ3Tsnh0Ep8FUqYwAJIjursK68gegrWEUpf7oeJlJer5gaNlD5CXCeRHwyhNiA1uzHaX4BjjyeKHl0Kw==", + "dependencies": { + "@aws-sdk/signature-v4-multi-region": "3.582.0", + "@aws-sdk/types": "3.577.0", + "@aws-sdk/util-format-url": "3.577.0", + "@smithy/middleware-endpoint": "^3.0.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/smithy-client": "^3.0.1", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.582.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.582.0.tgz", + "integrity": "sha512-aFCOjjNqEX2l+V8QjOWy5F7CtHIC/RlYdBuv3No6yxn+pMvVUUe6zdMk2yHWcudVpHWsyvcZzAUBliAPeFLPsQ==", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.582.0", + "@aws-sdk/types": "3.577.0", + "@smithy/protocol-http": "^4.0.0", + "@smithy/signature-v4": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/token-providers": { "version": "3.577.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.577.0.tgz", @@ -661,6 +1179,17 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.568.0.tgz", + "integrity": "sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.577.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.577.0.tgz", @@ -675,6 +1204,20 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.577.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.577.0.tgz", + "integrity": "sha512-SyEGC2J+y/krFRuPgiF02FmMYhqbiIkOjDE6k4nYLJQRyS6XEAGxZoG+OHeOVEM+bsDgbxokXZiM3XKGu6qFIg==", + "dependencies": { + "@aws-sdk/types": "3.577.0", + "@smithy/querystring-builder": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/util-locate-window": { "version": "3.568.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", @@ -727,6 +1270,18 @@ "tslib": "^2.3.1" } }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.575.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.575.0.tgz", + "integrity": "sha512-cWgAwmbFYNCFzPwxL705+lWps0F3ZvOckufd2KKoEZUmtpVw9/txUXNrPySUXSmRTSRhoatIMABNfStWR043bQ==", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.24.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", @@ -1091,6 +1646,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" }, + "node_modules/@hookform/resolvers": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.2.tgz", + "integrity": "sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -2081,6 +2644,23 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-3.0.0.tgz", + "integrity": "sha512-sbnURCwjF0gSToGlsBiAmd1lRCmSn72nu9axfJu5lIx6RUEgHu6GwTMbqCdhQSi0Pumcm5vFxsi9XWXb2mTaoA==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.0.tgz", + "integrity": "sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg==", + "dependencies": { + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, "node_modules/@smithy/config-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.0.tgz", @@ -2129,6 +2709,68 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/eventstream-codec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.0.0.tgz", + "integrity": "sha512-PUtyEA0Oik50SaEFCZ0WPVtF9tz/teze2fDptW6WRXl+RrEenH8UbEjudOz8iakiMl3lE3lCVqYf2Y+znL8QFQ==", + "dependencies": { + "@aws-crypto/crc32": "3.0.0", + "@smithy/types": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.0.tgz", + "integrity": "sha512-NB7AFiPN4NxP/YCAnrvYR18z2/ZsiHiF7VtG30gshO9GbFrIb1rC8ep4NGpJSWrz6P64uhPXeo4M0UsCLnZKqw==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.0.tgz", + "integrity": "sha512-RUQG3vQ3LX7peqqHAbmayhgrF5aTilPnazinaSGF1P0+tgM3vvIRWPHmlLIz2qFqB9LqFIxditxc8O2Z6psrRw==", + "dependencies": { + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.0.tgz", + "integrity": "sha512-baRPdMBDMBExZXIUAoPGm/hntixjt/VFpU6+VmCyiYJYzRHRxoaI1MN+5XE+hIS8AJ2GCHLMFEIOLzq9xx1EgQ==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.0.tgz", + "integrity": "sha512-HNFfShmotWGeAoW4ujP8meV9BZavcpmerDbPIjkJbxKbN8RsUcpRQ/2OyIxWNxXNH2GWCAxuSB7ynmIGJlQ3Dw==", + "dependencies": { + "@smithy/eventstream-codec": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@smithy/fetch-http-handler": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.0.1.tgz", @@ -2141,6 +2783,17 @@ "tslib": "^2.6.2" } }, + "node_modules/@smithy/hash-blob-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.0.0.tgz", + "integrity": "sha512-/Wbpdg+bwJvW7lxR/zpWAc1/x/YkcqguuF2bAzkJrvXriZu1vm8r+PUdE4syiVwQg7PPR2dXpi3CLBb9qRDaVQ==", + "dependencies": { + "@smithy/chunked-blob-reader": "^3.0.0", + "@smithy/chunked-blob-reader-native": "^3.0.0", + "@smithy/types": "^3.0.0", + "tslib": "^2.6.2" + } + }, "node_modules/@smithy/hash-node": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.0.tgz", @@ -2155,6 +2808,19 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/hash-stream-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.0.0.tgz", + "integrity": "sha512-J0i7de+EgXDEGITD4fxzmMX8CyCNETTIRXlxjMiNUvvu76Xn3GJ31wQR85ynlPk2wI1lqoknAFJaD1fiNDlbIA==", + "dependencies": { + "@smithy/types": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@smithy/invalid-dependency": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.0.tgz", @@ -2175,6 +2841,16 @@ "node": ">=16.0.0" } }, + "node_modules/@smithy/md5-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.0.tgz", + "integrity": "sha512-Tm0vrrVzjlD+6RCQTx7D3Ls58S3FUH1ZCtU1MIh/qQmaOo1H9lMN2as6CikcEwgattnA9SURSdoJJ27xMcEfMA==", + "dependencies": { + "@smithy/types": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + } + }, "node_modules/@smithy/middleware-content-length": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.0.tgz", @@ -2742,6 +3418,12 @@ "https://trpc.io/sponsor" ] }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -2797,6 +3479,12 @@ "@types/react": "*" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@typescript-eslint/parser": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", @@ -3263,6 +3951,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -6557,6 +7250,21 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.51.5", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz", + "integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-icons": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", @@ -7739,6 +8447,14 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-form-data": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/zod-form-data/-/zod-form-data-2.0.2.tgz", + "integrity": "sha512-sKTi+k0fvkxdakD0V5rq+9WVJA3cuTQUfEmNqvHrTzPLvjfLmkkBLfR0ed3qOi9MScJXTHIDH/jUNnEJ3CBX4g==", + "peerDependencies": { + "zod": ">= 3.11.0" + } } } } diff --git a/package.json b/package.json index fc0fdeb..6007769 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,13 @@ }, "dependencies": { "@auth/prisma-adapter": "^2.1.0", + "@aws-sdk/client-s3": "^3.577.0", "@aws-sdk/client-ses": "^3.577.0", + "@aws-sdk/s3-request-presigner": "^3.582.0", "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.5", + "@hookform/resolvers": "^3.4.2", "@mui/material": "^5.15.18", "@mui/material-nextjs": "^5.15.11", "@prisma/client": "^5.14.0", @@ -30,21 +33,27 @@ "@trpc/next": "^11.0.0-rc.373", "@trpc/react-query": "^11.0.0-rc.373", "@trpc/server": "^11.0.0-rc.373", + "bcryptjs": "^2.4.3", "clsx": "^2.1.1", "next": "14.2.3", "next-auth": "^4.24.7", "postcss-import": "^16.1.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.5", "react-icons": "^5.2.1", "stripe": "^15.7.0", "tailwind-merge": "^2.3.0", - "zod": "^3.23.8" + "uuid": "^9.0.1", + "zod": "^3.23.8", + "zod-form-data": "^2.0.2" }, "devDependencies": { + "@types/bcryptjs": "^2.4.6", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@types/uuid": "^9.0.8", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", diff --git a/prisma/migrations/20240522220953_added_to_product/migration.sql b/prisma/migrations/20240522220953_added_to_product/migration.sql new file mode 100644 index 0000000..90e6883 --- /dev/null +++ b/prisma/migrations/20240522220953_added_to_product/migration.sql @@ -0,0 +1,38 @@ +/* + Warnings: + + - You are about to drop the column `cartId` on the `Product` table. All the data in the column will be lost. + - Added the required column `quantity` to the `Product` table without a default value. This is not possible if the table is not empty. + - Added the required column `status` to the `Product` table without a default value. This is not possible if the table is not empty. + - Added the required column `suk` to the `Product` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "Status" AS ENUM ('Active', 'InActive'); + +-- DropForeignKey +ALTER TABLE "Product" DROP CONSTRAINT "Product_cartId_fkey"; + +-- AlterTable +ALTER TABLE "Product" DROP COLUMN "cartId", +ADD COLUMN "quantity" INTEGER NOT NULL, +ADD COLUMN "status" "Status" NOT NULL, +ADD COLUMN "suk" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "_CartToProduct" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_CartToProduct_AB_unique" ON "_CartToProduct"("A", "B"); + +-- CreateIndex +CREATE INDEX "_CartToProduct_B_index" ON "_CartToProduct"("B"); + +-- AddForeignKey +ALTER TABLE "_CartToProduct" ADD CONSTRAINT "_CartToProduct_A_fkey" FOREIGN KEY ("A") REFERENCES "Cart"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CartToProduct" ADD CONSTRAINT "_CartToProduct_B_fkey" FOREIGN KEY ("B") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240523190640_changed_price_type/migration.sql b/prisma/migrations/20240523190640_changed_price_type/migration.sql new file mode 100644 index 0000000..e07e4fb --- /dev/null +++ b/prisma/migrations/20240523190640_changed_price_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Product" ALTER COLUMN "price" SET DATA TYPE DOUBLE PRECISION; diff --git a/prisma/migrations/20240523211558_added_user_password/migration.sql b/prisma/migrations/20240523211558_added_user_password/migration.sql new file mode 100644 index 0000000..2f77ec7 --- /dev/null +++ b/prisma/migrations/20240523211558_added_user_password/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "password" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c744b65..1bb41b8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { email String @unique emailVerified DateTime? image String? + password String? accounts Account[] sessions Session[] // Optional for WebAuthn support @@ -27,7 +28,7 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - Cart Cart? + Cart Cart[] } model Account { @@ -85,13 +86,20 @@ model Authenticator { @@id([userId, credentialID]) } +enum Status { + Active + InActive +} + model Product { - id String @id @default(cuid()) - name String - price Int - image String - Cart Cart? @relation(fields: [cartId], references: [id]) - cartId String? + id String @id @default(cuid()) + name String + price Float + image String + status Status + suk String + quantity Int + Cart Cart[] } model Cart { diff --git a/public/sign-bg-image.jpg b/public/sign-bg-image.jpg new file mode 100644 index 0000000..bf4b1ab Binary files /dev/null and b/public/sign-bg-image.jpg differ diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 54093ba..22b9879 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,3 +1,5 @@ +"use client"; + import OrdersIcon from "@/components/icons/orders"; import ProductIcon from "@/components/icons/product"; import React from "react"; diff --git a/src/app/admin/product/components/product-table.tsx b/src/app/admin/product/components/product-table.tsx index f530f8a..fc18c53 100644 --- a/src/app/admin/product/components/product-table.tsx +++ b/src/app/admin/product/components/product-table.tsx @@ -1,6 +1,18 @@ -import Image from "next/image"; +"use client"; +import { trpc } from "@/utils/trpc"; +import TableBottom from "./table-bottom"; const ProductTable = () => { + const products = trpc.product.getProducts.useQuery(); + + if (!products.data) { + return

Loading

; + } + + if (!products.data.length) { + return

No Products

; + } + return ( @@ -44,76 +56,9 @@ const ProductTable = () => { - - - - - - - - + {products.data.map((product) => { + return ; + })}
- - - - Whitetails Women's Open Sky - - - - #479063DR - 37 - $171.00 - - - Active - - -
- - - -
-
); diff --git a/src/app/admin/product/components/table-bottom.tsx b/src/app/admin/product/components/table-bottom.tsx new file mode 100644 index 0000000..30c5121 --- /dev/null +++ b/src/app/admin/product/components/table-bottom.tsx @@ -0,0 +1,87 @@ +import React from "react"; +import Image from "next/image"; +import { TProduct } from "@/type"; + +interface TableBottomProps { + product: TProduct; +} + +const TableBottom = (props: TableBottomProps) => { + const { product } = props; + return ( + + + + + + {product.name} + + + + + {product.suk} + + + {product.quantity} + + + ${product.price} + + + + {product.status} + + + +
+ + + +
+ + + ); +}; + +export default TableBottom; diff --git a/src/app/admin/product/[product_id]/page.tsx b/src/app/admin/product/edit/[product_id]/page.tsx similarity index 100% rename from src/app/admin/product/[product_id]/page.tsx rename to src/app/admin/product/edit/[product_id]/page.tsx diff --git a/src/app/admin/product/new/components/new-product-form.tsx b/src/app/admin/product/new/components/new-product-form.tsx new file mode 100644 index 0000000..fa651e7 --- /dev/null +++ b/src/app/admin/product/new/components/new-product-form.tsx @@ -0,0 +1,163 @@ +"use client"; +import { useForm, Controller } from "react-hook-form"; +import { TProduct, ZProduct } from "@/type"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { trpc } from "@/utils/trpc"; +import Input from "@/components/ui/input"; +import UploadImageInput from "./upload-image-input"; +import { useRouter } from "next/navigation"; +import useToast from "@/hooks/useToast"; + +type statusOption = { + label: string; + value: string; +}; + +const NewProductForm = () => { + const statusOptions: statusOption[] = [ + { label: "Active", value: "Active" }, + { label: "In Active", value: "InActive" }, + ]; + + const router = useRouter(); + const toast = useToast(); + + const { + register, + handleSubmit, + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(ZProduct), + defaultValues: { + price: 0, + quantity: 0, + }, + }); + + const mutation = trpc.product.createProduct.useMutation({ + onSuccess() { + toast({ + type: "open", + message: "Successfully created a product", + variant: "success", + }); + router.push("/admin/product"); + }, + onError() { + toast({ + type: "open", + message: "There was an error while trying to create a product", + variant: "error", + }); + }, + }); + + const onSubmit = async (data: TProduct, event: any) => { + let formData = new FormData(event?.target); + await mutation.mutateAsync(formData); + }; + + return ( + <> +
+
+
+
+

General

+ +
+

+ Product Name * +

+ + + A product name is required and recommended to be unique. + +
+
+

Status

+ +
+
+

+ Product Price * +

+ +
+
+

+ SUK * +

+ +
+
+

+ Quantity * +

+ +
+
+
+ { + return ( + + ); + }} + /> +
+ + +
+ + ); +}; + +export default NewProductForm; diff --git a/src/app/admin/product/new/components/upload-image-input.tsx b/src/app/admin/product/new/components/upload-image-input.tsx new file mode 100644 index 0000000..8a24466 --- /dev/null +++ b/src/app/admin/product/new/components/upload-image-input.tsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; + +const UploadImageInput = ({ + name, + onChange, + errors, +}: { + name: string; + onChange: (e: any) => void; + errors: any; +}) => { + const [image, setImage] = useState(null); + + return ( +
+
+

Upload Image

+
+

{image && `${image}`}

+ + {errors.image && `${errors.image.message}`} + +
+ + Image size must be less than 5Mb + +
+ { + setImage(e.target.value); + onChange(e.target.files); + }} + /> + +
+
+
+ ); +}; + +export default UploadImageInput; diff --git a/src/app/admin/product/new/page.tsx b/src/app/admin/product/new/page.tsx index 5c470b7..50ae0be 100644 --- a/src/app/admin/product/new/page.tsx +++ b/src/app/admin/product/new/page.tsx @@ -1,109 +1,14 @@ import React from "react"; import TopBar from "../../components/top-bar"; - -type statusOption = { - label: string; - value: string; -}; +import NewProductForm from "./components/new-product-form"; const NewProuct = () => { - const statusOptions: statusOption[] = [ - { label: "Active", value: "active" }, - { label: "In Active", value: "in-active" }, - ]; return ( <>
-
-
-
-
-

General

- -
-

- Product Name * -

- - - A product name is required and recommended to be unique. - -
-
-

Status

- -
-
-

- Product Price * -

- -
-
-

- SUK * -

- -
-
-

- Quantity * -

- -
-
-
-
-
-

Upload Image

-
-

Hello

-
- - Image size must be less than 5Mb - -
- - -
-
-
-
- -
+
diff --git a/src/app/auth/components/wrapper.tsx b/src/app/auth/components/wrapper.tsx index 3effc48..f29e353 100644 --- a/src/app/auth/components/wrapper.tsx +++ b/src/app/auth/components/wrapper.tsx @@ -1,6 +1,7 @@ import React from "react"; import GoogleButton from "./google-button"; import Link from "next/link"; +import Image from "next/image"; const Header = ({ type }: { type: "signin" | "signup" }) => { return ( @@ -49,10 +50,15 @@ const Wrapper = ({
-
+
+ Signup image +
diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index bed07ad..bf1e037 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -1,21 +1,63 @@ +"use client"; import Input from "@/components/ui/input"; import Wrapper from "../components/wrapper"; -import FeildErrorMessage from "@/components/forms/field-error-message"; import FieldLabel from "@/components/forms/field-label"; import FormControl from "@/components/forms/form-control"; +import { useForm } from "react-hook-form"; +import { TSigninFormState, ZSigninFormState } from "@/type"; +import { signIn } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import useToast from "@/hooks/useToast"; +import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; const Signin = () => { + const { register, handleSubmit } = useForm({ + resolver: zodResolver(ZSigninFormState), + }); + const [logingIn, setLogingIn] = useState(false); + const router = useRouter(); + const toast = useToast(); + + const onSubmit = async (data: TSigninFormState) => { + setLogingIn(true); + const res = await signIn("credentials", { + ...data, + redirect: false, + }); + + if (!res) { + throw new Error("there was no signin response"); + } + + if (res.ok) { + router.push("/store"); + return; + } + + toast({ + type: "open", + message: "Can not log a user with these credentials", + variant: "error", + }); + setLogingIn(false); + }; + return ( -
+ Email - + Password - +
@@ -27,7 +69,7 @@ const Signin = () => {
- diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index b8e6070..0459efa 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -1,41 +1,116 @@ +"use client"; + +import Input from "@/components/ui/input"; import Wrapper from "../components/wrapper"; +import FieldLabel from "@/components/forms/field-label"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { ZSignupFormState, TSignupFormState } from "@/type"; +import FeildErrorMessage from "@/components/forms/field-error-message"; +import { trpc } from "@/utils/trpc"; +import useToast from "@/hooks/useToast"; +import { useRouter } from "next/navigation"; const Signup = () => { + const toast = useToast(); + const router = useRouter(); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(ZSignupFormState), + }); + const mutation = trpc.user.createUser.useMutation({ + onSuccess({ name }) { + toast({ + type: "open", + message: `Created the account for ${name}`, + variant: "success", + }); + router.push("/auth/signin"); + }, + onError(error, variables) { + if (error.message === "user already exist") { + toast({ + type: "open", + message: `Email: ${variables.email} is already`, + variant: "error", + }); + } else { + toast({ + type: "open", + message: `Error while creating an account`, + variant: "error", + }); + } + }, + }); + + const onSubmit = (data: TSignupFormState) => { + mutation.mutate({ + name: data.username, + email: data.email, + password: data.password, + }); + }; + return ( -
+ +
+ Username + + {errors.username && ( + + )} +
-

- Your Email * -

- Email + + {errors.email && }
-

- Password * -

- Password + + {errors.password && ( + + )}
-

- Confirm Password * -

- Confirm Password + + {errors.confirm_password && ( + + )}
-
diff --git a/src/app/cart/page.tsx b/src/app/cart/page.tsx new file mode 100644 index 0000000..316dc0d --- /dev/null +++ b/src/app/cart/page.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const Cart = () => { + return
Cart
; +}; + +export default Cart; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5b2f360..23b66a6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import StripeProvider from "@/providers/stripe-provider"; import { SessionProvider } from "@/providers/session-provider"; import { getServerSession } from "next-auth/next"; import { authOptions } from "@/authOptions"; +import ToastProvider from "@/providers/toast-provider"; export const metadata: Metadata = { title: "Create Next App", @@ -28,7 +29,9 @@ export default async function RootLayout({ - {children} + + {children} + diff --git a/src/authOptions.ts b/src/authOptions.ts index 31132ed..bd334aa 100644 --- a/src/authOptions.ts +++ b/src/authOptions.ts @@ -4,6 +4,8 @@ import { PrismaAdapter } from "@auth/prisma-adapter"; import prisma from "@/db"; import { env } from "@/env"; import { type Adapter } from "next-auth/adapters"; +import CredentialsProvider from "next-auth/providers/credentials"; +import bcrypt from "bcryptjs"; export const authOptions: NextAuthOptions = { session: { @@ -15,6 +17,52 @@ export const authOptions: NextAuthOptions = { clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }), + CredentialsProvider({ + // The name to display on the sign in form (e.g. "Sign in with...") + name: "Credentials", + // `credentials` is used to generate a form on the sign in page. + // You can specify which fields should be submitted, by adding keys to the `credentials` object. + // e.g. domain, username, password, 2FA token, etc. + // You can pass any HTML attribute to the tag through the object. + credentials: { + email: { + label: "Email", + type: "text", + placeholder: "jsmith@gamil.com", + }, + password: { label: "Password", type: "password" }, + }, + + async authorize(credentials, req) { + // Add logic here to look up the user from the credentials supplied + const dbUser = await prisma.user.findUnique({ + where: { email: credentials?.email }, + }); + + if (!dbUser?.password || !credentials?.password) { + throw new Error("No password from db"); + } + + const match = bcrypt.compareSync( + credentials?.password, + dbUser?.password + ); + + if (match) { + const user = { + id: dbUser.id, + name: dbUser.name, + email: dbUser.email, + }; + return user; + } else { + // If you return null then an error will be displayed advising the user to check their details. + return null; + + // You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter + } + }, + }), ], pages: { signIn: "/auth/signin", diff --git a/src/components/forms/field-error-message.tsx b/src/components/forms/field-error-message.tsx index c913adc..9944a42 100644 --- a/src/components/forms/field-error-message.tsx +++ b/src/components/forms/field-error-message.tsx @@ -1,7 +1,5 @@ -import React from "react"; - -const FeildErrorMessage = ({ children }: { children: React.ReactNode }) => { - return

{children}

; +const FeildErrorMessage = ({ message }: { message?: string }) => { + return

{message}

; }; export default FeildErrorMessage; diff --git a/src/components/shared/navbar.tsx b/src/components/shared/navbar.tsx index 9271531..77511f0 100644 --- a/src/components/shared/navbar.tsx +++ b/src/components/shared/navbar.tsx @@ -1,10 +1,8 @@ -import React from "react"; -import Link from "next/link"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/authOptions"; +"use client"; +import { signOut, useSession } from "next-auth/react"; -const Navbar = async () => { - const session = await getServerSession(authOptions); +const Navbar = () => { + const { data: session } = useSession(); return (
@@ -33,7 +31,9 @@ const Navbar = async () => { {session?.user?.name} - Logout +
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index efebe47..838ae04 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,20 +1,28 @@ import React from "react"; import { cn } from "@/utils"; -interface InputProps extends React.InputHTMLAttributes {} +interface InputProps extends React.InputHTMLAttributes { + isInvalid?: boolean; +} -const Input = (props: InputProps) => { - const { className } = props; +const Input = React.forwardRef( + (props: InputProps, ref) => { + const { className, isInvalid, ...rest } = props; + return ( + + ); + } +); - return ( - - ); -}; +Input.displayName = "Input"; export default Input; diff --git a/src/env.ts b/src/env.ts index 9b4218b..1a6a262 100644 --- a/src/env.ts +++ b/src/env.ts @@ -10,6 +10,10 @@ export const env = createEnv({ STRIPE_SECRET_KEY: z.string(), WEBHOOK_SECRET: z.string(), AWS_SES_REGION: z.string(), + AWS_BUCKET_REGION: z.string(), + AWS_ACCESS_KEY_ID: z.string(), + AWS_SECRET_ACCESS_KEY: z.string(), + AWS_BUCKET_NAME: z.string(), NEXTAUTH_URL: process.env.NODE_ENV === "production" ? z.string() @@ -31,5 +35,9 @@ export const env = createEnv({ WEBHOOK_SECRET: process.env.WEBHOOK_SECRET, AWS_SES_REGION: process.env.AWS_SES_REGION, NEXTAUTH_URL: process.env.NEXTAUTH_URL, + AWS_BUCKET_REGION: process.env.AWS_BUCKET_REGION, + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, + AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME, }, }); diff --git a/src/hooks/useToast.tsx b/src/hooks/useToast.tsx new file mode 100644 index 0000000..0edfa7a --- /dev/null +++ b/src/hooks/useToast.tsx @@ -0,0 +1,10 @@ +"use client"; +import { ToastDispatchContext, actionType } from "@/providers/toast-provider"; +import { useContext } from "react"; + +const useToast = () => { + const dispatch = useContext(ToastDispatchContext); + return (params: actionType) => dispatch(params); +}; + +export default useToast; diff --git a/src/providers/toast-provider.tsx b/src/providers/toast-provider.tsx new file mode 100644 index 0000000..26256f2 --- /dev/null +++ b/src/providers/toast-provider.tsx @@ -0,0 +1,73 @@ +"use client"; +import { Snackbar } from "@mui/material"; +import Alert from "@mui/material/Alert"; +import React, { useReducer } from "react"; +import { createContext } from "react"; + +export type actionType = { + type: "open" | "close"; + message: string; + variant: "success" | "error"; +}; + +export type stateType = { + open: boolean; + message: string; + variant: "success" | "error"; +}; + +export const ToastContext = createContext(null); +export const ToastDispatchContext = createContext(null); + +function reducer(state: stateType, action: actionType) { + if (action.type === "open") { + return { + open: true, + message: action.message, + variant: action.variant, + }; + } else if (action.type === "close") { + return { + open: false, + message: action.message, + variant: action.variant, + }; + } + throw new Error("Unknown action"); +} + +const ToastProvider = ({ children }: { children: React.ReactNode }) => { + const [state, dispatch] = useReducer(reducer, { open: false } as stateType); + + const handleClose = (event: any, reason: any) => { + if (reason === "clickaway") { + return; + } + dispatch({ type: "close", message: "error", variant: "success" }); + }; + + return ( + <> + + + + + {state.message} + + + {children} + + + + ); +}; + +export default ToastProvider; diff --git a/src/providers/trpc-provider.tsx b/src/providers/trpc-provider.tsx index b9dec89..f5a6168 100644 --- a/src/providers/trpc-provider.tsx +++ b/src/providers/trpc-provider.tsx @@ -1,6 +1,11 @@ "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { httpBatchLink } from "@trpc/client"; +import { + httpLink, + httpBatchLink, + splitLink, + isNonJsonSerializable, +} from "@trpc/client"; import { useState } from "react"; import { trpc } from "@/utils/trpc"; import { getBaseUrl } from "@/utils"; @@ -10,13 +15,22 @@ export function TrpcProvider({ children }: { children: React.ReactNode }) { const [trpcClient] = useState(() => trpc.createClient({ links: [ - httpBatchLink({ - url: `${getBaseUrl()}/api/trpc`, - - // You can pass any HTTP headers you wish here - async headers() { - return {}; + splitLink({ + condition: (op) => { + return isNonJsonSerializable(op.input); }, + true: httpLink({ + url: `${getBaseUrl()}/api/trpc`, + + // You can pass any HTTP headers you wish here + async headers() { + return {}; + }, + }), + + false: httpBatchLink({ + url: `${getBaseUrl()}/api/trpc`, + }), }), ], }) diff --git a/src/server/routers/index.ts b/src/server/routers/index.ts index 06a311c..b9cc3cf 100644 --- a/src/server/routers/index.ts +++ b/src/server/routers/index.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { procedure, router } from "../trpc"; import stripe from "@/utils/stripe/config"; import productRouter from "./product"; +import { userRouter } from "./user"; export const appRouter = router({ createPaymentIntent: procedure @@ -15,6 +16,7 @@ export const appRouter = router({ return { clientSecret: paymentIntent.client_secret }; }), product: productRouter, + user: userRouter, }); // export type definition of API diff --git a/src/server/routers/product.ts b/src/server/routers/product.ts index 77aa0d3..92063b3 100644 --- a/src/server/routers/product.ts +++ b/src/server/routers/product.ts @@ -1,9 +1,43 @@ import { procedure, router } from "../trpc"; import prisma from "@/db"; +import { env } from "@/env"; +import { ZFProuct } from "@/type"; +import { s3 } from "@/utils/s3/config"; +import { v4 as uuidv4 } from "uuid"; +import { createPresignedUrlWithClient } from "@/utils/s3/utils"; const productRouter = router({ getProducts: procedure.query(() => { - return; + const products = prisma.product.findMany(); + return products; + }), + + createProduct: procedure.input(ZFProuct).mutation(async ({ input }) => { + const signedUrl = await createPresignedUrlWithClient( + s3, + env.AWS_BUCKET_NAME, + uuidv4() + ); + + const res = await fetch(signedUrl, { + method: "PUT", + body: input.image, + headers: { + "Content-Type": input.image.type, + }, + }); + + if (!res.ok) { + throw new Error("Could not PUT file in S3 Bucket"); + } + + const product = prisma.product.create({ + data: { + ...input, + image: signedUrl.split("?")[0], + }, + }); + return product; }), }); diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts new file mode 100644 index 0000000..9275a9f --- /dev/null +++ b/src/server/routers/user.ts @@ -0,0 +1,54 @@ +import { ZUser } from "@/type"; +import { procedure, router } from "../trpc"; +import prisma from "@/db"; +import { v4 as uuidv4 } from "uuid"; +import bcrypt from "bcryptjs"; +import { User } from "@prisma/client"; +import { Prisma } from "@prisma/client"; +import { TRPCError } from "@trpc/server"; + +export const userRouter = router({ + createUser: procedure.input(ZUser).mutation(async ({ input }) => { + const salt = bcrypt.genSaltSync(10); + const hash = bcrypt.hashSync(input.password, salt); + + let user: User; + + try { + user = await prisma.user.create({ + data: { + ...input, + password: hash, + }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError) { + // The .code property can be accessed in a type-safe manner + if (e.code === "P2002") { + if (e.meta) { + if (e.meta.modelName === "User") { + const error: TRPCError = { + name: "TRPCError", + code: "BAD_REQUEST", + message: "user already exist", + }; + throw error; + } + } + } + } + throw e; + } + + await prisma.account.create({ + data: { + userId: user.id, + type: "credentials", + provider: "email", + providerAccountId: uuidv4(), + }, + }); + + return { name: user.name, email: user.email }; + }), +}); diff --git a/src/type.ts b/src/type.ts index 22e70f2..2e6ffa7 100644 --- a/src/type.ts +++ b/src/type.ts @@ -1,7 +1,18 @@ import { z } from "zod"; +import { zfd } from "zod-form-data"; + +const ACCEPTED_IMAGE_TYPES = [ + "image/jpeg", + "image/jpg", + "image/png", + "image/webp", +]; + +const statusValues = ["Active", "InActive"] as const; export const ZSignupFormState = z .object({ + username: z.string().min(2), email: z.string().email(), password: z.string().min(4), confirm_password: z.string(), @@ -26,9 +37,21 @@ export const ZSigninFormState = z.object({ export type TSigninFormState = z.infer; export const ZProduct = z.object({ - name: z.string(), - price: z.number(), - image: z.string().url(), + name: z.string().min(2), + price: z.coerce.number(), + image: z + .any() + .refine((file) => { + return !!file; + }, "Image is required") + .refine((file) => { + if (!!file) { + return ACCEPTED_IMAGE_TYPES.includes(file[0].type); + } + }, "Only .jpg, .jpeg, .png and .webp formats are supported."), + status: z.enum(statusValues), + suk: z.string().min(2), + quantity: z.coerce.number(), }); export type TProduct = z.infer; @@ -38,3 +61,20 @@ export const ZCart = z.object({ }); export type TCart = z.infer; + +export const ZFProuct = zfd.formData({ + name: zfd.text(), + price: zfd.numeric(), + image: zfd.file(), + suk: zfd.text(), + status: zfd.text(z.enum(statusValues)), + quantity: zfd.numeric(), +}); + +export const ZUser = z.object({ + name: z.string(), + email: z.string().email(), + password: z.string(), +}); + +export type TUser = z.infer; diff --git a/src/utils/s3/config.ts b/src/utils/s3/config.ts new file mode 100644 index 0000000..5740747 --- /dev/null +++ b/src/utils/s3/config.ts @@ -0,0 +1,10 @@ +import { S3Client } from "@aws-sdk/client-s3"; +import { env } from "@/env"; + +export const s3 = new S3Client({ + region: env.AWS_BUCKET_REGION, + credentials: { + accessKeyId: env.AWS_ACCESS_KEY_ID, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY, + }, +}); diff --git a/src/utils/s3/utils.ts b/src/utils/s3/utils.ts new file mode 100644 index 0000000..80dbc8b --- /dev/null +++ b/src/utils/s3/utils.ts @@ -0,0 +1,11 @@ +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; + +export const createPresignedUrlWithClient = ( + client: S3Client, + bucket: string, + key: string +) => { + const command = new PutObjectCommand({ Bucket: bucket, Key: key }); + return getSignedUrl(client, command, { expiresIn: 3600 }); +};