diff --git a/package.json b/package.json index 94adac4..ccffaf4 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "seed:run": "mikro-orm seeder:run --config=database/mikro-orm.config.ts" }, "dependencies": { + "@aws-sdk/client-s3": "^3.850.0", + "@aws-sdk/s3-request-presigner": "^3.850.0", "@fastify/static": "^8.2.0", "@keyv/redis": "^4.5.0", "@mikro-orm/cli": "^6.4.16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5aa69a..b5e6548 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.850.0 + version: 3.850.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.850.0 + version: 3.850.0 '@fastify/static': specifier: ^8.2.0 version: 8.2.0 @@ -272,6 +278,165 @@ packages: resolution: {integrity: sha512-QsmFuYdAyeCyg9WF/AJBhFXDUfCwmDFTEbsv5t5KPSP6slhk0GoLNZApniiFytU2siRlSxVNpve2uATyYuAYkQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.850.0': + resolution: {integrity: sha512-tX5bUfqiLOh6jtAlaiAuOUKFYh8KDG9k9zFLUdgGplC5TP47AYTreUEg+deCTHo4DD3YCvrLuyZ8tIDgKu7neQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/client-sso@3.848.0': + resolution: {integrity: sha512-mD+gOwoeZQvbecVLGoCmY6pS7kg02BHesbtIxUj+PeBqYoZV5uLvjUOmuGfw1SfoSobKvS11urxC9S7zxU/Maw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/core@3.846.0': + resolution: {integrity: sha512-7CX0pM906r4WSS68fCTNMTtBCSkTtf3Wggssmx13gD40gcWEZXsU00KzPp1bYheNRyPlAq3rE22xt4wLPXbuxA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-env@3.846.0': + resolution: {integrity: sha512-QuCQZET9enja7AWVISY+mpFrEIeHzvkx/JEEbHYzHhUkxcnC2Kq2c0bB7hDihGD0AZd3Xsm653hk1O97qu69zg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-http@3.846.0': + resolution: {integrity: sha512-Jh1iKUuepdmtreMYozV2ePsPcOF5W9p3U4tWhi3v6nDvz0GsBjzjAROW+BW8XMz9vAD3I9R+8VC3/aq63p5nlw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-ini@3.848.0': + resolution: {integrity: sha512-r6KWOG+En2xujuMhgZu7dzOZV3/M5U/5+PXrG8dLQ3rdPRB3vgp5tc56KMqLwm/EXKRzAOSuw/UE4HfNOAB8Hw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-node@3.848.0': + resolution: {integrity: sha512-AblNesOqdzrfyASBCo1xW3uweiSro4Kft9/htdxLeCVU1KVOnFWA5P937MNahViRmIQm2sPBCqL8ZG0u9lnh5g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-process@3.846.0': + resolution: {integrity: sha512-mEpwDYarJSH+CIXnnHN0QOe0MXI+HuPStD6gsv3z/7Q6ESl8KRWon3weFZCDnqpiJMUVavlDR0PPlAFg2MQoPg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-sso@3.848.0': + resolution: {integrity: sha512-pozlDXOwJZL0e7w+dqXLgzVDB7oCx4WvtY0sk6l4i07uFliWF/exupb6pIehFWvTUcOvn5aFTTqcQaEzAD5Wsg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.848.0': + resolution: {integrity: sha512-D1fRpwPxtVDhcSc/D71exa2gYweV+ocp4D3brF0PgFd//JR3XahZ9W24rVnTQwYEcK9auiBZB89Ltv+WbWN8qw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-bucket-endpoint@3.840.0': + resolution: {integrity: sha512-+gkQNtPwcSMmlwBHFd4saVVS11In6ID1HczNzpM3MXKXRBfSlbZJbCt6wN//AZ8HMklZEik4tcEOG0qa9UY8SQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-expect-continue@3.840.0': + resolution: {integrity: sha512-iJg2r6FKsKKvdiU4oCOuCf7Ro/YE0Q2BT/QyEZN3/Rt8Nr4SAZiQOlcBXOCpGvuIKOEAhvDOUnW3aDHL01PdVw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.846.0': + resolution: {integrity: sha512-CdkeVfkwt3+bDLhmOwBxvkUf6oY9iUhvosaUnqkoPsOqIiUEN54yTGOnO8A0wLz6mMsZ6aBlfFrQhFnxt3c+yw==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-host-header@3.840.0': + resolution: {integrity: sha512-ub+hXJAbAje94+Ya6c6eL7sYujoE8D4Bumu1NUI8TXjUhVVn0HzVWQjpRLshdLsUp1AW7XyeJaxyajRaJQ8+Xg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-location-constraint@3.840.0': + resolution: {integrity: sha512-KVLD0u0YMF3aQkVF8bdyHAGWSUY6N1Du89htTLgqCcIhSxxAJ9qifrosVZ9jkAzqRW99hcufyt2LylcVU2yoKQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-logger@3.840.0': + resolution: {integrity: sha512-lSV8FvjpdllpGaRspywss4CtXV8M7NNNH+2/j86vMH+YCOZ6fu2T/TyFd/tHwZ92vDfHctWkRbQxg0bagqwovA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.840.0': + resolution: {integrity: sha512-Gu7lGDyfddyhIkj1Z1JtrY5NHb5+x/CRiB87GjaSrKxkDaydtX2CU977JIABtt69l9wLbcGDIQ+W0uJ5xPof7g==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.846.0': + resolution: {integrity: sha512-jP9x+2Q87J5l8FOP+jlAd7vGLn0cC6G9QGmf386e5OslBPqxXKcl3RjqGLIOKKos2mVItY3ApP5xdXQx7jGTVA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-ssec@3.840.0': + resolution: {integrity: sha512-CBZP9t1QbjDFGOrtnUEHL1oAvmnCUUm7p0aPNbIdSzNtH42TNKjPRN3TuEIJDGjkrqpL3MXyDSmNayDcw/XW7Q==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/middleware-user-agent@3.848.0': + resolution: {integrity: sha512-rjMuqSWJEf169/ByxvBqfdei1iaduAnfolTshsZxwcmLIUtbYrFUmts0HrLQqsAG8feGPpDLHA272oPl+NTCCA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/nested-clients@3.848.0': + resolution: {integrity: sha512-joLsyyo9u61jnZuyYzo1z7kmS7VgWRAkzSGESVzQHfOA1H2PYeUFek6vLT4+c9xMGrX/Z6B0tkRdzfdOPiatLg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/region-config-resolver@3.840.0': + resolution: {integrity: sha512-Qjnxd/yDv9KpIMWr90ZDPtRj0v75AqGC92Lm9+oHXZ8p1MjG5JE2CW0HL8JRgK9iKzgKBL7pPQRXI8FkvEVfrA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/s3-request-presigner@3.850.0': + resolution: {integrity: sha512-eFvMUCJXoVTkAxkqHKn125mLMGtNa76+oD3wV97ScXUZuL5liaj+kAN9nSqRiQ5vaCz5gsOeB9t/ba/cTGATjg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.846.0': + resolution: {integrity: sha512-ZMfIMxUljqZzPJGOcraC6erwq/z1puNMU35cO1a/WdhB+LdYknMn1lr7SJuH754QwNzzIlZbEgg4hoHw50+DpQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/token-providers@3.848.0': + resolution: {integrity: sha512-oNPyM4+Di2Umu0JJRFSxDcKQ35+Chl/rAwD47/bS0cDPI8yrao83mLXLeDqpRPHyQW4sXlP763FZcuAibC0+mg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/types@3.840.0': + resolution: {integrity: sha512-xliuHaUFZxEx1NSXeLLZ9Dyu6+EJVQKEoD+yM+zqUo3YDZ7medKJWY6fIOKiPX/N7XbLdBYwajb15Q7IL8KkeA==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-arn-parser@3.804.0': + resolution: {integrity: sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-endpoints@3.848.0': + resolution: {integrity: sha512-fY/NuFFCq/78liHvRyFKr+aqq1aA/uuVSANjzr5Ym8c+9Z3HRPE9OrExAHoMrZ6zC8tHerQwlsXYYH5XZ7H+ww==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-format-url@3.840.0': + resolution: {integrity: sha512-VB1PWyI1TQPiPvg4w7tgUGGQER1xxXPNUqfh3baxUSFi1Oh8wHrDnFywkxLm3NMmgDmnLnSZ5Q326qAoyqKLSg==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-locate-window@3.804.0': + resolution: {integrity: sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==} + engines: {node: '>=18.0.0'} + + '@aws-sdk/util-user-agent-browser@3.840.0': + resolution: {integrity: sha512-JdyZM3EhhL4PqwFpttZu1afDpPJCCc3eyZOLi+srpX11LsGj6sThf47TYQN75HT1CarZ7cCdQHGzP2uy3/xHfQ==} + + '@aws-sdk/util-user-agent-node@3.848.0': + resolution: {integrity: sha512-Zz1ft9NiLqbzNj/M0jVNxaoxI2F4tGXN0ZbZIj+KJ+PbJo+w5+Jo6d0UDAtbj3AEd79pjcCaP4OA9NTVzItUdw==} + engines: {node: '>=18.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.821.0': + resolution: {integrity: sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -1404,6 +1569,218 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@smithy/abort-controller@4.0.4': + resolution: {integrity: sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader-native@4.0.0': + resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==} + engines: {node: '>=18.0.0'} + + '@smithy/chunked-blob-reader@5.0.0': + resolution: {integrity: sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==} + engines: {node: '>=18.0.0'} + + '@smithy/config-resolver@4.1.4': + resolution: {integrity: sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.7.2': + resolution: {integrity: sha512-JoLw59sT5Bm8SAjFCYZyuCGxK8y3vovmoVbZWLDPTH5XpPEIwpFd9m90jjVMwoypDuB/SdVgje5Y4T7w50lJaw==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.0.6': + resolution: {integrity: sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.0.4': + resolution: {integrity: sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-browser@4.0.4': + resolution: {integrity: sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.1.2': + resolution: {integrity: sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.0.4': + resolution: {integrity: sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-universal@4.0.4': + resolution: {integrity: sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.1.0': + resolution: {integrity: sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-blob-browser@4.0.4': + resolution: {integrity: sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.0.4': + resolution: {integrity: sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-stream-node@4.0.4': + resolution: {integrity: sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.0.4': + resolution: {integrity: sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.0.0': + resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.0.4': + resolution: {integrity: sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.0.4': + resolution: {integrity: sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.1.17': + resolution: {integrity: sha512-S3hSGLKmHG1m35p/MObQCBCdRsrpbPU8B129BVzRqRfDvQqPMQ14iO4LyRw+7LNizYc605COYAcjqgawqi+6jA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.1.18': + resolution: {integrity: sha512-bYLZ4DkoxSsPxpdmeapvAKy7rM5+25gR7PGxq2iMiecmbrRGBHj9s75N74Ylg+aBiw9i5jIowC/cLU2NR0qH8w==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.0.8': + resolution: {integrity: sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.0.4': + resolution: {integrity: sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.1.3': + resolution: {integrity: sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.1.0': + resolution: {integrity: sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.0.4': + resolution: {integrity: sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.1.2': + resolution: {integrity: sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.0.4': + resolution: {integrity: sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.0.4': + resolution: {integrity: sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.0.6': + resolution: {integrity: sha512-RRoTDL//7xi4tn5FrN2NzH17jbgmnKidUqd4KvquT0954/i6CXXkh1884jBiunq24g9cGtPBEXlU40W6EpNOOg==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.0.4': + resolution: {integrity: sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.1.2': + resolution: {integrity: sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.4.9': + resolution: {integrity: sha512-mbMg8mIUAWwMmb74LoYiArP04zWElPzDoA1jVOp3or0cjlDMgoS6WTC3QXK0Vxoc9I4zdrX0tq6qsOmaIoTWEQ==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.3.1': + resolution: {integrity: sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.0.4': + resolution: {integrity: sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.0.0': + resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.0.0': + resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.0.0': + resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.0.0': + resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.0.0': + resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.0.25': + resolution: {integrity: sha512-pxEWsxIsOPLfKNXvpgFHBGFC3pKYKUFhrud1kyooO9CJai6aaKDHfT10Mi5iiipPXN/JhKAu3qX9o75+X85OdQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.0.25': + resolution: {integrity: sha512-+w4n4hKFayeCyELZLfsSQG5mCC3TwSkmRHv4+el5CzFU8ToQpYGhpV7mrRzqlwKkntlPilT1HJy1TVeEvEjWOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.0.6': + resolution: {integrity: sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.0.0': + resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.0.4': + resolution: {integrity: sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.0.6': + resolution: {integrity: sha512-+YekoF2CaSMv6zKrA6iI/N9yva3Gzn4L6n35Luydweu5MMPYpiGZlWqehPHDHyNbnyaYlz/WJyYAZnC+loBDZg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.2.3': + resolution: {integrity: sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.0.0': + resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.0.0': + resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==} + engines: {node: '>=18.0.0'} + + '@smithy/util-waiter@4.0.6': + resolution: {integrity: sha512-slcr1wdRbX7NFphXZOxtxRNA7hXAAtJAXJDE/wdoMAos27SIquVCKiSqfB6/28YzQ8FCsB5NKkhdM5gMADbqxg==} + engines: {node: '>=18.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -1654,6 +2031,9 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/validator@13.15.2': resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==} @@ -2179,6 +2559,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -3076,6 +3459,10 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + fast-xml-parser@5.2.5: + resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==} + hasBin: true + fastify-multer@2.0.3: resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==} engines: {node: '>=10.17.0'} @@ -5402,6 +5789,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@2.1.1: + resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==} + strtok3@10.3.1: resolution: {integrity: sha512-3JWEZM6mfix/GCJBBUrkA8p2Id2pBkyTkVCJKto55w080QBKZ+8R171fGrbiSp+yMO/u6F8/yUh7K4V9K+YCnw==} engines: {node: '>=18'} @@ -5971,6 +6361,490 @@ snapshots: transitivePeerDependencies: - chokidar + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.840.0 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.840.0 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-locate-window': 3.804.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-locate-window': 3.804.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.840.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.850.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.846.0 + '@aws-sdk/credential-provider-node': 3.848.0 + '@aws-sdk/middleware-bucket-endpoint': 3.840.0 + '@aws-sdk/middleware-expect-continue': 3.840.0 + '@aws-sdk/middleware-flexible-checksums': 3.846.0 + '@aws-sdk/middleware-host-header': 3.840.0 + '@aws-sdk/middleware-location-constraint': 3.840.0 + '@aws-sdk/middleware-logger': 3.840.0 + '@aws-sdk/middleware-recursion-detection': 3.840.0 + '@aws-sdk/middleware-sdk-s3': 3.846.0 + '@aws-sdk/middleware-ssec': 3.840.0 + '@aws-sdk/middleware-user-agent': 3.848.0 + '@aws-sdk/region-config-resolver': 3.840.0 + '@aws-sdk/signature-v4-multi-region': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.848.0 + '@aws-sdk/util-user-agent-browser': 3.840.0 + '@aws-sdk/util-user-agent-node': 3.848.0 + '@aws-sdk/xml-builder': 3.821.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.7.2 + '@smithy/eventstream-serde-browser': 4.0.4 + '@smithy/eventstream-serde-config-resolver': 4.1.2 + '@smithy/eventstream-serde-node': 4.0.4 + '@smithy/fetch-http-handler': 5.1.0 + '@smithy/hash-blob-browser': 4.0.4 + '@smithy/hash-node': 4.0.4 + '@smithy/hash-stream-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/md5-js': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.17 + '@smithy/middleware-retry': 4.1.18 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.1.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.25 + '@smithy/util-defaults-mode-node': 4.0.25 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-stream': 4.2.3 + '@smithy/util-utf8': 4.0.0 + '@smithy/util-waiter': 4.0.6 + '@types/uuid': 9.0.8 + tslib: 2.8.1 + uuid: 9.0.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.848.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.846.0 + '@aws-sdk/middleware-host-header': 3.840.0 + '@aws-sdk/middleware-logger': 3.840.0 + '@aws-sdk/middleware-recursion-detection': 3.840.0 + '@aws-sdk/middleware-user-agent': 3.848.0 + '@aws-sdk/region-config-resolver': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.848.0 + '@aws-sdk/util-user-agent-browser': 3.840.0 + '@aws-sdk/util-user-agent-node': 3.848.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.7.2 + '@smithy/fetch-http-handler': 5.1.0 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.17 + '@smithy/middleware-retry': 4.1.18 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.1.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.25 + '@smithy/util-defaults-mode-node': 4.0.25 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.846.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@aws-sdk/xml-builder': 3.821.0 + '@smithy/core': 3.7.2 + '@smithy/node-config-provider': 4.1.3 + '@smithy/property-provider': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/signature-v4': 5.1.2 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-utf8': 4.0.0 + fast-xml-parser: 5.2.5 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.846.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.846.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@smithy/fetch-http-handler': 5.1.0 + '@smithy/node-http-handler': 4.1.0 + '@smithy/property-provider': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + '@smithy/util-stream': 4.2.3 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.848.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/credential-provider-env': 3.846.0 + '@aws-sdk/credential-provider-http': 3.846.0 + '@aws-sdk/credential-provider-process': 3.846.0 + '@aws-sdk/credential-provider-sso': 3.848.0 + '@aws-sdk/credential-provider-web-identity': 3.848.0 + '@aws-sdk/nested-clients': 3.848.0 + '@aws-sdk/types': 3.840.0 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.848.0': + dependencies: + '@aws-sdk/credential-provider-env': 3.846.0 + '@aws-sdk/credential-provider-http': 3.846.0 + '@aws-sdk/credential-provider-ini': 3.848.0 + '@aws-sdk/credential-provider-process': 3.846.0 + '@aws-sdk/credential-provider-sso': 3.848.0 + '@aws-sdk/credential-provider-web-identity': 3.848.0 + '@aws-sdk/types': 3.840.0 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.846.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.848.0': + dependencies: + '@aws-sdk/client-sso': 3.848.0 + '@aws-sdk/core': 3.846.0 + '@aws-sdk/token-providers': 3.848.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.848.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/nested-clients': 3.848.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-bucket-endpoint@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-arn-parser': 3.804.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-config-provider': 4.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.846.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@smithy/is-array-buffer': 4.0.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-stream': 4.2.3 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.846.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-arn-parser': 3.804.0 + '@smithy/core': 3.7.2 + '@smithy/node-config-provider': 4.1.3 + '@smithy/protocol-http': 5.1.2 + '@smithy/signature-v4': 5.1.2 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-stream': 4.2.3 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.848.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.848.0 + '@smithy/core': 3.7.2 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.848.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.846.0 + '@aws-sdk/middleware-host-header': 3.840.0 + '@aws-sdk/middleware-logger': 3.840.0 + '@aws-sdk/middleware-recursion-detection': 3.840.0 + '@aws-sdk/middleware-user-agent': 3.848.0 + '@aws-sdk/region-config-resolver': 3.840.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-endpoints': 3.848.0 + '@aws-sdk/util-user-agent-browser': 3.840.0 + '@aws-sdk/util-user-agent-node': 3.848.0 + '@smithy/config-resolver': 4.1.4 + '@smithy/core': 3.7.2 + '@smithy/fetch-http-handler': 5.1.0 + '@smithy/hash-node': 4.0.4 + '@smithy/invalid-dependency': 4.0.4 + '@smithy/middleware-content-length': 4.0.4 + '@smithy/middleware-endpoint': 4.1.17 + '@smithy/middleware-retry': 4.1.18 + '@smithy/middleware-serde': 4.0.8 + '@smithy/middleware-stack': 4.0.4 + '@smithy/node-config-provider': 4.1.3 + '@smithy/node-http-handler': 4.1.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-body-length-node': 4.0.0 + '@smithy/util-defaults-mode-browser': 4.0.25 + '@smithy/util-defaults-mode-node': 4.0.25 + '@smithy/util-endpoints': 3.0.6 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.4 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.850.0': + dependencies: + '@aws-sdk/signature-v4-multi-region': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@aws-sdk/util-format-url': 3.840.0 + '@smithy/middleware-endpoint': 4.1.17 + '@smithy/protocol-http': 5.1.2 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.846.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.846.0 + '@aws-sdk/types': 3.840.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/signature-v4': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.848.0': + dependencies: + '@aws-sdk/core': 3.846.0 + '@aws-sdk/nested-clients': 3.848.0 + '@aws-sdk/types': 3.840.0 + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.840.0': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.804.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.848.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-endpoints': 3.0.6 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/querystring-builder': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.804.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.840.0': + dependencies: + '@aws-sdk/types': 3.840.0 + '@smithy/types': 4.3.1 + bowser: 2.11.0 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.848.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.848.0 + '@aws-sdk/types': 3.840.0 + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.821.0': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -7303,6 +8177,339 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@smithy/abort-controller@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@4.0.0': + dependencies: + '@smithy/util-base64': 4.0.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@5.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@4.1.4': + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + '@smithy/util-config-provider': 4.0.0 + '@smithy/util-middleware': 4.0.4 + tslib: 2.8.1 + + '@smithy/core@3.7.2': + dependencies: + '@smithy/middleware-serde': 4.0.8 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-body-length-browser': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-stream': 4.2.3 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.0.6': + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/property-provider': 4.0.4 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + tslib: 2.8.1 + + '@smithy/eventstream-codec@4.0.4': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.3.1 + '@smithy/util-hex-encoding': 4.0.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@4.0.4': + dependencies: + '@smithy/eventstream-serde-universal': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.1.2': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.0.4': + dependencies: + '@smithy/eventstream-serde-universal': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@4.0.4': + dependencies: + '@smithy/eventstream-codec': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.1.0': + dependencies: + '@smithy/protocol-http': 5.1.2 + '@smithy/querystring-builder': 4.0.4 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@4.0.4': + dependencies: + '@smithy/chunked-blob-reader': 5.0.0 + '@smithy/chunked-blob-reader-native': 4.0.0 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/hash-node@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.0.4': + dependencies: + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.1.17': + dependencies: + '@smithy/core': 3.7.2 + '@smithy/middleware-serde': 4.0.8 + '@smithy/node-config-provider': 4.1.3 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + '@smithy/url-parser': 4.0.4 + '@smithy/util-middleware': 4.0.4 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.1.18': + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/protocol-http': 5.1.2 + '@smithy/service-error-classification': 4.0.6 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-retry': 4.0.6 + tslib: 2.8.1 + uuid: 9.0.1 + + '@smithy/middleware-serde@4.0.8': + dependencies: + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.1.3': + dependencies: + '@smithy/property-provider': 4.0.4 + '@smithy/shared-ini-file-loader': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.1.0': + dependencies: + '@smithy/abort-controller': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/querystring-builder': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/protocol-http@5.1.2': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + '@smithy/util-uri-escape': 4.0.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.0.6': + dependencies: + '@smithy/types': 4.3.1 + + '@smithy/shared-ini-file-loader@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.1.2': + dependencies: + '@smithy/is-array-buffer': 4.0.0 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-middleware': 4.0.4 + '@smithy/util-uri-escape': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/smithy-client@4.4.9': + dependencies: + '@smithy/core': 3.7.2 + '@smithy/middleware-endpoint': 4.1.17 + '@smithy/middleware-stack': 4.0.4 + '@smithy/protocol-http': 5.1.2 + '@smithy/types': 4.3.1 + '@smithy/util-stream': 4.2.3 + tslib: 2.8.1 + + '@smithy/types@4.3.1': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.0.4': + dependencies: + '@smithy/querystring-parser': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/util-base64@4.0.0': + dependencies: + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.0.0': + dependencies: + '@smithy/is-array-buffer': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.0.25': + dependencies: + '@smithy/property-provider': 4.0.4 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + bowser: 2.11.0 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.0.25': + dependencies: + '@smithy/config-resolver': 4.1.4 + '@smithy/credential-provider-imds': 4.0.6 + '@smithy/node-config-provider': 4.1.3 + '@smithy/property-provider': 4.0.4 + '@smithy/smithy-client': 4.4.9 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.0.6': + dependencies: + '@smithy/node-config-provider': 4.1.3 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.0.4': + dependencies: + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.0.6': + dependencies: + '@smithy/service-error-classification': 4.0.6 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + + '@smithy/util-stream@4.2.3': + dependencies: + '@smithy/fetch-http-handler': 5.1.0 + '@smithy/node-http-handler': 4.1.0 + '@smithy/types': 4.3.1 + '@smithy/util-base64': 4.0.0 + '@smithy/util-buffer-from': 4.0.0 + '@smithy/util-hex-encoding': 4.0.0 + '@smithy/util-utf8': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.0.0': + dependencies: + '@smithy/util-buffer-from': 4.0.0 + tslib: 2.8.1 + + '@smithy/util-waiter@4.0.6': + dependencies: + '@smithy/abort-controller': 4.0.4 + '@smithy/types': 4.3.1 + tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} '@swc/cli@0.6.0(@swc/core@1.12.9)(chokidar@4.0.3)': @@ -7575,6 +8782,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/uuid@9.0.8': {} + '@types/validator@13.15.2': {} '@types/yargs-parser@21.0.3': {} @@ -8191,6 +9400,8 @@ snapshots: boolbase@1.0.0: optional: true + bowser@2.11.0: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -9210,6 +10421,10 @@ snapshots: fast-uri@3.0.6: {} + fast-xml-parser@5.2.5: + dependencies: + strnum: 2.1.1 + fastify-multer@2.0.3: dependencies: '@fastify/busboy': 1.2.1 @@ -12110,6 +13325,8 @@ snapshots: strip-json-comments@3.1.1: {} + strnum@2.1.1: {} + strtok3@10.3.1: dependencies: '@tokenizer/token': 0.3.0 diff --git a/src/app.module.ts b/src/app.module.ts index f9b9a50..cf1e131 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -25,6 +25,7 @@ import { QuotaSyncModule } from "./modules/quota-sync/quota-sync.module"; import { SettingModule } from "./modules/settings/settings.module"; import { EmailSignaturesModule } from "./modules/signatures/email-signatures.module"; import { TemplatesModule } from "./modules/templates/templates.module"; +import { UploaderModule } from "./modules/uploader/uploader.module"; import { UsersModule } from "./modules/users/users.module"; import { UtilsModule } from "./modules/utils/utils.module"; @@ -50,6 +51,7 @@ import { UtilsModule } from "./modules/utils/utils.module"; TemplatesModule, SettingModule, NotificationModule, + UploaderModule, ], }) export class AppModule implements NestModule { diff --git a/src/common/decorators/auth-guard.decorator.ts b/src/common/decorators/auth-guard.decorator.ts index b5a40e4..7d398fa 100755 --- a/src/common/decorators/auth-guard.decorator.ts +++ b/src/common/decorators/auth-guard.decorator.ts @@ -1,9 +1,13 @@ import { UseGuards, applyDecorators } from "@nestjs/common"; -import { ApiBearerAuth } from "@nestjs/swagger"; +import { ApiBearerAuth, ApiHeader } from "@nestjs/swagger"; import { AdminRouteGuard } from "../../modules/auth/guards/admin.guard"; import { JwtAuthGuard } from "../../modules/auth/guards/auth.guard"; export function AuthGuards() { - return applyDecorators(UseGuards(JwtAuthGuard, AdminRouteGuard), ApiBearerAuth("authorization")); + return applyDecorators( + UseGuards(JwtAuthGuard, AdminRouteGuard), + ApiBearerAuth("authorization"), + ApiHeader({ name: "x-business-id", description: "Business ID" }), + ); } diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index ce9f779..e2a0ba1 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -107,6 +107,9 @@ export const enum UserMessage { FORWARDERS_EMAIL_VALID = "آدرس ایمیل فوروارد باید معتبر باشد", FORWARDERS_ARRAY = "آدرس های فوروارد باید یک آرایه باشد", FORWARDERS_STRING = "آدرس های فوروارد باید یک رشته باشد", + PROFILE_PICTURE_URL = "آدرس تصویر پروفایل باید یک آدرس اینترنتی معتبر باشد", + USER_UPDATED_SUCCESSFULLY = "کاربر با موفقیت به روز رسانی شد", + PROFILE_PICTURE_NOT_EMPTY = "تصویر پروفایل نمیتواند خالی باشد", } export const enum CommonMessage { diff --git a/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts b/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts index ef4530a..ecbe62e 100644 --- a/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts +++ b/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts @@ -1,105 +1,15 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from "class-validator"; - -import { EmailLength, EmailPurpose, WritingTone } from "../enums/ai.enum"; - -export class EmailContextDto { - @IsOptional() - @IsString({ message: "Original email content must be a string" }) - @ApiPropertyOptional({ - description: "Original email content (for replies/forwards)", - example: "Hi, I need to discuss the project timeline...", - }) - originalContent?: string; - - @IsOptional() - @IsString({ message: "Original sender must be a string" }) - @ApiPropertyOptional({ - description: "Original sender (for replies/forwards)", - example: "john.doe@company.com", - }) - originalSender?: string; - - @IsOptional() - @IsString({ message: "Original subject must be a string" }) - @ApiPropertyOptional({ - description: "Original subject (for replies/forwards)", - example: "Project Timeline Discussion", - }) - originalSubject?: string; -} +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; export class WriteEmailAssistanceDto { - @IsNotEmpty({ message: "Email purpose is required" }) - @IsEnum(EmailPurpose, { message: "Email purpose must be one of: reply, forward, new, follow_up" }) + @IsNotEmpty({ message: "Prompt is required" }) + @IsString({ message: "Prompt must be a string" }) @ApiProperty({ - description: "Purpose of the email", - enum: EmailPurpose, - example: EmailPurpose.REPLY, + description: "User prompt describing what kind of email they want to write", + example: + "Write a professional email to schedule a meeting with my team to discuss the Q4 project timeline. I want to address concerns about resource allocation and set a deadline for next Friday.", }) - purpose: EmailPurpose; - - @IsNotEmpty({ message: "Main message or intent is required" }) - @IsString({ message: "Main message must be a string" }) - @ApiProperty({ - description: "Main message or intent of the email", - example: "I want to schedule a meeting to discuss the project timeline and address any concerns.", - }) - mainMessage: string; - - @IsOptional() - @IsString({ message: "Recipient information must be a string" }) - @ApiPropertyOptional({ - description: "Recipient information (optional)", - example: "john.doe@company.com", - }) - recipient?: string; - - @IsOptional() - @IsEnum(WritingTone, { message: "Writing tone must be one of: formal, casual, professional, friendly, urgent" }) - @ApiPropertyOptional({ - description: "Desired tone of the email", - enum: WritingTone, - default: WritingTone.PROFESSIONAL, - }) - tone?: WritingTone; - - @IsOptional() - @IsEnum(EmailLength, { message: "Email length must be one of: short, medium, long" }) - @ApiPropertyOptional({ - description: "Desired length of the email", - enum: EmailLength, - default: EmailLength.MEDIUM, - }) - length?: EmailLength; - - @IsOptional() - @IsArray({ message: "Key points must be an array" }) - @IsString({ each: true, message: "Each key point must be a string" }) - @ApiPropertyOptional({ - description: "Key points to include in the email", - type: [String], - example: ["Discuss project timeline", "Address resource allocation", "Set next meeting date"], - }) - keyPoints?: string[]; - - @IsOptional() - @ValidateNested() - @Type(() => EmailContextDto) - @ApiPropertyOptional({ - description: "Context from original email (for replies/forwards)", - type: EmailContextDto, - }) - context?: EmailContextDto; - - @IsOptional() - @IsString({ message: "Additional instructions must be a string" }) - @ApiPropertyOptional({ - description: "Additional instructions or preferences", - example: "Please keep it concise and mention the deadline", - }) - additionalInstructions?: string; + prompt: string; } export class EmailSuggestionDto { @@ -122,25 +32,3 @@ export class EmailSuggestionDto { }) alternatives: string[]; } - -export class EmailWritingResponseDto { - @ApiProperty({ - description: "Primary email suggestion", - type: EmailSuggestionDto, - }) - suggestion: EmailSuggestionDto; - - @ApiProperty({ - description: "Writing tips and recommendations", - type: [String], - example: ["Consider adding a specific deadline", "You might want to include meeting agenda", "Don't forget to CC relevant stakeholders"], - }) - tips: string[]; - - @ApiProperty({ - description: "Suggested improvements for the original request", - type: [String], - example: ["Consider being more specific about the timeline", "Add context about project urgency"], - }) - improvements: string[]; -} diff --git a/src/modules/ai-assistant/ai-assistant.controller.ts b/src/modules/ai-assistant/ai-assistant.controller.ts index a4ad2b2..eaa97d7 100644 --- a/src/modules/ai-assistant/ai-assistant.controller.ts +++ b/src/modules/ai-assistant/ai-assistant.controller.ts @@ -8,8 +8,6 @@ import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; @Controller("ai-assistant") -// @ApiHeader({ name: "x-business-id", description: "Business ID" }) -// @UseInterceptors(BusinessInterceptor) @AuthGuards() export class AiAssistantController { constructor(private readonly aiAssistantService: AiAssistantService) {} diff --git a/src/modules/ai-assistant/interfaces/ai-service.interface.ts b/src/modules/ai-assistant/interfaces/ai-service.interface.ts index 4dc27e8..3eb4912 100644 --- a/src/modules/ai-assistant/interfaces/ai-service.interface.ts +++ b/src/modules/ai-assistant/interfaces/ai-service.interface.ts @@ -14,20 +14,6 @@ export interface SummarizationOptions { language?: "Persian" | "English"; } -export interface WritingAssistanceOptions { - purpose: "reply" | "forward" | "new" | "follow_up"; - tone: "formal" | "casual" | "professional" | "friendly" | "urgent"; - length: "short" | "medium" | "long"; - keyPoints?: string[]; - context?: { - originalContent?: string; - originalSender?: string; - originalSubject?: string; - }; - additionalInstructions?: string; - language?: "Persian" | "English"; -} - export interface AISummaryResponse { summary: string; keyPoints: string[]; @@ -46,5 +32,5 @@ export interface AIWritingResponse { export interface AIServiceInterface { summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise; - assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise; + assistEmailWriting(prompt: string): Promise; } diff --git a/src/modules/ai-assistant/services/ai-assistant.service.ts b/src/modules/ai-assistant/services/ai-assistant.service.ts index c3fd9a2..26f06ff 100644 --- a/src/modules/ai-assistant/services/ai-assistant.service.ts +++ b/src/modules/ai-assistant/services/ai-assistant.service.ts @@ -8,7 +8,7 @@ import { WildDuckMessagesService } from "../../mail-server/services/wildduck-mes import { SummarizeEmailDto } from "../DTO/summarize-email.dto"; import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto"; import { SummaryLength, SummaryTone } from "../enums/ai.enum"; -import { EmailData, SummarizationOptions, WritingAssistanceOptions } from "../interfaces/ai-service.interface"; +import { EmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; @Injectable() export class AiAssistantService { @@ -132,22 +132,7 @@ export class AiAssistantService { //************************************************* */ private async generateEmailWritingAssistance(writeAssistanceDto: WriteEmailAssistanceDto, _userId: string) { - const writingOptions: WritingAssistanceOptions = { - purpose: writeAssistanceDto.purpose.toLowerCase() as "reply" | "forward" | "new" | "follow_up", - tone: (writeAssistanceDto.tone?.toLowerCase() as "formal" | "casual" | "professional" | "friendly" | "urgent") || "professional", - length: (writeAssistanceDto.length?.toLowerCase() as "short" | "medium" | "long") || "medium", - keyPoints: writeAssistanceDto.keyPoints, - context: writeAssistanceDto.context - ? { - originalContent: writeAssistanceDto.context.originalContent, - originalSender: writeAssistanceDto.context.originalSender, - originalSubject: writeAssistanceDto.context.originalSubject, - } - : undefined, - additionalInstructions: writeAssistanceDto.additionalInstructions, - }; - - const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.mainMessage, writingOptions); + const aiResponse = await this.aiService.assistEmailWriting(writeAssistanceDto.prompt); return { suggestion: { diff --git a/src/modules/ai-assistant/services/ai.service.ts b/src/modules/ai-assistant/services/ai.service.ts index 5c8a834..aeca6bc 100644 --- a/src/modules/ai-assistant/services/ai.service.ts +++ b/src/modules/ai-assistant/services/ai.service.ts @@ -4,18 +4,15 @@ import OpenAI from "openai"; import { AI_CONFIG } from "../../../common/constants"; import { AiMessage } from "../../../common/enums/message.enum"; import { AIConfig } from "../../../configs/ai.config"; -import { - AIServiceInterface, - AISummaryResponse, - AIWritingResponse, - EmailData, - SummarizationOptions, - WritingAssistanceOptions, -} from "../interfaces/ai-service.interface"; +import { AIServiceInterface, AISummaryResponse, AIWritingResponse, EmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; @Injectable() export class AiService implements AIServiceInterface { private readonly openai: OpenAI; + private readonly language: "Persian" | "English" = "Persian"; + private readonly defaultTone: "formal" | "casual" | "professional" = "professional"; + private readonly defaultLength: "short" | "medium" | "detailed" = "medium"; + private readonly defaultPurpose: "general" | "reply" | "forward" | "new" | "follow_up" = "general"; constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) { this.openai = new OpenAI({ @@ -25,6 +22,7 @@ export class AiService implements AIServiceInterface { }); } + //************************************************* */ async summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise { const prompt = this.buildSummarizationPrompt(emailData, options); @@ -52,16 +50,15 @@ export class AiService implements AIServiceInterface { return parsedResponse; } - //************************************************* */ - async assistEmailWriting(mainMessage: string, options: WritingAssistanceOptions): Promise { - const prompt = this.buildWritingAssistancePrompt(mainMessage, options); + //************************************************* */ + async assistEmailWriting(prompt: string): Promise { const completion = await this.openai.chat.completions.create({ model: this.aiConfig.defaultModel, messages: [ { role: "system", - content: this.getWritingAssistanceSystemPrompt(options), + content: this.getWritingAssistanceSystemPrompt(), }, { role: "user", @@ -80,24 +77,23 @@ export class AiService implements AIServiceInterface { return parsedResponse; } - //************************************************* */ private getSummarizationSystemPrompt(options: SummarizationOptions): string { - const language = options.language || "Persian"; + const language = options.language || this.language; const template = this.aiConfig.farsiPrompts.summarization.systemPrompt; return template.replace("{tone}", options.tone).replace("{length}", options.length).replace("{language}", language); } //************************************************* */ - private getWritingAssistanceSystemPrompt(options: WritingAssistanceOptions): string { - const language = options.language || "Persian"; + private getWritingAssistanceSystemPrompt(): string { + const language = this.language; const template = this.aiConfig.farsiPrompts.writingAssistance.systemPrompt; return template - .replace("{tone}", options.tone) - .replace("{length}", options.length) - .replace("{purpose}", options.purpose) + .replace("{tone}", this.defaultTone) + .replace("{length}", this.defaultLength) + .replace("{purpose}", this.defaultPurpose) .replace("{language}", language); } @@ -105,7 +101,6 @@ export class AiService implements AIServiceInterface { private buildSummarizationPrompt(emailData: EmailData, options: SummarizationOptions): string { const template = this.aiConfig.farsiPrompts.summarization.promptTemplate; let prompt = `${template.intro}\n\n`; - console.log(emailData); prompt += `${template.subjectLabel} ${emailData.subject}\n`; prompt += `${template.fromLabel} ${emailData.from}\n`; @@ -123,44 +118,4 @@ export class AiService implements AIServiceInterface { return prompt; } - - //************************************************* */ - private buildWritingAssistancePrompt(mainMessage: string, options: WritingAssistanceOptions): string { - const template = this.aiConfig.farsiPrompts.writingAssistance.promptTemplate; - let prompt = `${template.intro}\n\n`; - - prompt += `${template.mainMessageLabel} ${mainMessage}\n\n`; - prompt += `${template.emailDetailsLabel}\n`; - prompt += `${template.purposeLabel} ${options.purpose}\n`; - prompt += `${template.toneLabel} ${options.tone}\n`; - prompt += `${template.lengthLabel} ${options.length}\n\n`; - - if (options.keyPoints && options.keyPoints.length > 0) { - prompt += `${template.keyPointsLabel}\n`; - options.keyPoints.forEach((point, index) => { - prompt += `${index + 1}. ${point}\n`; - }); - prompt += `\n`; - } - - if (options.context) { - prompt += `${template.contextLabel}\n`; - if (options.context.originalSubject) { - prompt += `${template.originalSubjectLabel} ${options.context.originalSubject}\n`; - } - if (options.context.originalSender) { - prompt += `${template.originalSenderLabel} ${options.context.originalSender}\n`; - } - if (options.context.originalContent) { - prompt += `${template.originalContentLabel} ${options.context.originalContent}\n`; - } - prompt += `\n`; - } - - if (options.additionalInstructions) { - prompt += `${template.additionalInstructionsLabel} ${options.additionalInstructions}\n\n`; - } - - return prompt; - } } diff --git a/src/modules/businesses/businesses.controller.ts b/src/modules/businesses/businesses.controller.ts index 9ef168d..d7bae5c 100644 --- a/src/modules/businesses/businesses.controller.ts +++ b/src/modules/businesses/businesses.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Get, Param, Patch, Post, UseInterceptors } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { BusinessQuotaResponseDto } from "./DTO/business-quota-response.dto"; import { BusinessSlugParamDto } from "./DTO/business-slug-param.dto"; @@ -12,7 +12,6 @@ import { BusinessDec } from "../../common/decorators/business.decorator"; import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; @Controller("business") -@ApiHeader({ name: "x-business-id", description: "Business ID" }) export class BusinessesController { constructor(private readonly businessesService: BusinessesService) {} diff --git a/src/modules/domains/domains.controller.ts b/src/modules/domains/domains.controller.ts index ba152da..60a52de 100644 --- a/src/modules/domains/domains.controller.ts +++ b/src/modules/domains/domains.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { CreateDomainDto } from "./DTO/create-domain.dto"; import { UpdateDomainDto } from "./DTO/update-domain.dto"; @@ -12,7 +12,6 @@ import { ParamDto } from "../../common/DTO/param.dto"; import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; @Controller("domains") -@ApiHeader({ name: "x-business-id", description: "Business ID" }) @UseInterceptors(BusinessInterceptor) @AuthGuards() export class DomainsController { diff --git a/src/modules/templates/templates.controller.ts b/src/modules/templates/templates.controller.ts index 4f2c79b..b879fbb 100644 --- a/src/modules/templates/templates.controller.ts +++ b/src/modules/templates/templates.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { CreateTemplateDto } from "./DTO/create-template.dto"; import { TemplateParamDto } from "./DTO/template-param.dto"; @@ -12,7 +12,6 @@ import { BusinessDec } from "../../common/decorators/business.decorator"; import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; @Controller("templates") -@ApiHeader({ name: "x-business-id", description: "Business ID" }) @UseInterceptors(BusinessInterceptor) @AuthGuards() export class TemplatesController { diff --git a/src/modules/uploader/DTO/upload-file.dto.ts b/src/modules/uploader/DTO/upload-file.dto.ts new file mode 100755 index 0000000..fb69ec9 --- /dev/null +++ b/src/modules/uploader/DTO/upload-file.dto.ts @@ -0,0 +1,19 @@ +import { File } from "@nest-lab/fastify-multer"; +import { ApiProperty } from "@nestjs/swagger"; + +export class UploadSingleFileDto { + @ApiProperty({ type: "string", format: "binary", nullable: false }) + file: File; +} + +export class UploadMultipleFileDto { + @ApiProperty({ + required: true, + type: "array", + items: { + type: "string", + format: "binary", + }, + }) + files: File[]; +} diff --git a/src/modules/uploader/interfaces/s3.interface.ts b/src/modules/uploader/interfaces/s3.interface.ts new file mode 100644 index 0000000..f510a4c --- /dev/null +++ b/src/modules/uploader/interfaces/s3.interface.ts @@ -0,0 +1,37 @@ +export interface S3UploadResult { + key: string; + url: string; + bucket: string; +} + +export interface S3UploadOptions { + contentType: string; + metadata?: Record; + acl?: "private" | "public-read" | "public-read-write"; +} + +export interface S3DownloadOptions { + expiresIn?: number; + responseContentType?: string; + responseContentDisposition?: string; +} + +export interface S3FileInfo { + key: string; + bucket: string; + size: number; + lastModified: Date; + contentType: string; + etag: string; +} + +export interface S3DeleteResult { + deleted: boolean; + key: string; +} + +export interface S3ListResult { + files: S3FileInfo[]; + continuationToken?: string; + isTruncated: boolean; +} diff --git a/src/modules/uploader/providers/s3.service.ts b/src/modules/uploader/providers/s3.service.ts new file mode 100644 index 0000000..4ece82e --- /dev/null +++ b/src/modules/uploader/providers/s3.service.ts @@ -0,0 +1,129 @@ +import { Readable } from "stream"; + +import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; + +import { S3UploadResult } from "../interfaces/s3.interface"; + +@Injectable() +export class S3Service { + private readonly logger = new Logger(S3Service.name); + private readonly s3Client: S3Client; + private readonly bucketName: string; + private readonly region: string; + private readonly url: string; + + constructor(private readonly configService: ConfigService) { + this.bucketName = this.configService.getOrThrow("BUCKET_NAME"); + this.region = this.configService.getOrThrow("BUCKET_REGION"); + this.url = this.configService.getOrThrow("BUCKET_URL"); + + this.s3Client = new S3Client({ + region: this.region, + endpoint: this.url, + credentials: { + accessKeyId: this.configService.getOrThrow("BUCKET_ACCESS_KEY"), + secretAccessKey: this.configService.getOrThrow("BUCKET_SECRET_KEY"), + }, + }); + } + + /** + * Upload file to S3 + */ + async uploadFile(buffer: Buffer, key: string, contentType: string, metadata?: Record): Promise { + try { + const command = new PutObjectCommand({ + Bucket: this.bucketName, + Key: key, + Body: buffer, + ACL: "public-read", + ContentType: contentType, + Metadata: metadata, + }); + + await this.s3Client.send(command); + + // const url = `https://${this.bucketName}.s3.${this.region}.amazonaws.com/${key}`; + const url = `https://${this.bucketName}.s3.ir-thr-at1.arvanstorage.ir/${key}`; + + this.logger.log(`File uploaded to S3: ${key}`); + + return { + key, + url, + bucket: this.bucketName, + }; + } catch (error: unknown) { + this.logger.error(`Failed to upload file to S3: ${(error as Error).message}`, (error as Error).stack); + throw error; + } + } + + /** + * Get file stream from S3 + */ + async getFileStream(key: string): Promise { + try { + const command = new GetObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + + const response = await this.s3Client.send(command); + return response.Body as Readable; + } catch (error: unknown) { + this.logger.error(`Failed to get file from S3: ${(error as Error).message}`, (error as Error).stack); + throw error; + } + } + + /** + * Generate presigned URL for file download + */ + async getPresignedUrl(key: string, expiresIn: number = 3600): Promise { + try { + const command = new GetObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + + return await getSignedUrl(this.s3Client, command, { expiresIn }); + } catch (error: unknown) { + this.logger.error(`Failed to generate presigned URL: ${(error as Error).message}`, (error as Error).stack); + throw error; + } + } + + /** + * Delete file from S3 + */ + async deleteFile(key: string): Promise { + try { + const command = new DeleteObjectCommand({ + Bucket: this.bucketName, + Key: key, + }); + + await this.s3Client.send(command); + this.logger.log(`File deleted from S3: ${key}`); + } catch (error: unknown) { + this.logger.error(`Failed to delete file from S3: ${(error as Error).message}`, (error as Error).stack); + throw error; + } + } + + /** + * Generate S3 key for file + */ + generateFileKey(originalName: string): string { + const timestamp = Date.now(); + const randomSuffix = Math.round(Math.random() * 1e9); + const extension = originalName.split(".").pop(); + + const basePath = `images`; + return `${basePath}/${timestamp}-${randomSuffix}.${extension}`; + } +} diff --git a/src/modules/uploader/providers/uploader.service.ts b/src/modules/uploader/providers/uploader.service.ts new file mode 100644 index 0000000..aee3393 --- /dev/null +++ b/src/modules/uploader/providers/uploader.service.ts @@ -0,0 +1,109 @@ +import { File } from "@nest-lab/fastify-multer"; +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; + +import { S3Service } from "./s3.service"; +import { UploaderMessage } from "../../../common/enums/message.enum"; + +@Injectable() +export class UploaderService { + private readonly logger = new Logger(UploaderService.name); + private readonly maxFileSize: number; + private readonly allowedMimeTypes: string[]; + + constructor( + private readonly configService: ConfigService, + private readonly s3Service: S3Service, + ) { + this.maxFileSize = this.configService.get("MAX_FILE_SIZE", 1024 * 1024 * 100); // 100MB + this.allowedMimeTypes = [ + "video/mp4", + "video/webm", + "video/ogg", + "video/avi", + "video/mov", + "video/mkv", + "audio/mp3", + "audio/wav", + "audio/ogg", + "audio/webm", + "audio/m4a", + "audio/aac", + "audio/flac", + "audio/m4a", + "image/jpeg", + "image/png", + "image/gif", + "image/webp", + "image/svg+xml", + "image/tiff", + "image/bmp", + ]; + } + + async uploadMultiple(files: File[]) { + return await Promise.all(files.map((file) => this.uploadFile(file))); + } + + /** + * Handle file upload + */ + async uploadFile(file: File) { + if (!file || !file.buffer || !file.size) { + throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID); + } + this.logger.log(`Uploading file: ${file.originalname}`); + + // Validate file + this.validateFile(file); + + // Determine file type + this.determineFileType(file.mimetype); + + // Generate S3 key + const s3Key = this.s3Service.generateFileKey(file.originalname); + + // Upload to S3 + const s3Result = await this.s3Service.uploadFile(file.buffer, s3Key, file.mimetype, { + originalName: file.originalname, + uploadedBy: "admin", + }); + + this.logger.log(`File uploaded successfully: `); + + return { + file: s3Result, + url: s3Result.url, + }; + } + + /** + * Validate uploaded file + */ + private validateFile(file: File): void { + if (!file) throw new BadRequestException(UploaderMessage.UPLOAD_FILE_INVALID); + + if (file.size && file.size > this.maxFileSize) throw new BadRequestException(`File too large. Maximum size is ${this.maxFileSize} bytes`); + + if (!this.allowedMimeTypes.includes(file.mimetype)) throw new BadRequestException(`File type not supported: ${file.mimetype}`); + } + + /** + * Determine file type based on mimetype + */ + private determineFileType(mimetype: string) { + if (mimetype.startsWith("video/")) { + return "video"; + } else if (mimetype.startsWith("audio/")) { + return "audio"; + } else if (mimetype.startsWith("image/")) { + return "image"; + } else if (mimetype.startsWith("application/")) { + return "application"; + } else if (mimetype.startsWith("text/")) { + return "text"; + } + // Default to video for unknown types + return "video"; + } +} diff --git a/src/modules/uploader/uploader.controller.ts b/src/modules/uploader/uploader.controller.ts new file mode 100644 index 0000000..93188e5 --- /dev/null +++ b/src/modules/uploader/uploader.controller.ts @@ -0,0 +1,31 @@ +import { File, FileInterceptor, FilesInterceptor } from "@nest-lab/fastify-multer"; +import { Controller, Post, UploadedFile, UploadedFiles, UseInterceptors } from "@nestjs/common"; +import { ApiBody, ApiConsumes, ApiOperation } from "@nestjs/swagger"; + +import { UploadMultipleFileDto, UploadSingleFileDto } from "./DTO/upload-file.dto"; +import { UploaderService } from "./providers/uploader.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; + +@Controller("uploader") +@AuthGuards() +export class UploaderController { + constructor(private readonly uploaderService: UploaderService) {} + + @ApiOperation({ summary: "Upload a file (admin)" }) + @ApiConsumes("multipart/form-data") + @UseInterceptors(FileInterceptor("file")) + @ApiBody({ type: UploadSingleFileDto }) + @Post("single-file") + async uploadFile(@UploadedFile() file: File) { + return this.uploaderService.uploadFile(file); + } + + @ApiOperation({ summary: "Uploads multiple files" }) + @ApiConsumes("multipart/form-data") + @UseInterceptors(FilesInterceptor("files")) + @ApiBody({ type: UploadMultipleFileDto }) + @Post("multi-file") + uploadFiles(@UploadedFiles() files: File[]) { + return this.uploaderService.uploadMultiple(files); + } +} diff --git a/src/modules/uploader/uploader.module.ts b/src/modules/uploader/uploader.module.ts new file mode 100644 index 0000000..f5b7d24 --- /dev/null +++ b/src/modules/uploader/uploader.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; + +import { S3Service } from "./providers/s3.service"; +import { UploaderService } from "./providers/uploader.service"; +import { UploaderController } from "./uploader.controller"; + +@Module({ + controllers: [UploaderController], + providers: [UploaderService, S3Service], + exports: [UploaderService], +}) +export class UploaderModule {} diff --git a/src/modules/users/DTO/update-user-profile.dto.ts b/src/modules/users/DTO/update-user-profile.dto.ts new file mode 100644 index 0000000..45ef79d --- /dev/null +++ b/src/modules/users/DTO/update-user-profile.dto.ts @@ -0,0 +1,12 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsUrl } from "class-validator"; + +import { UserMessage } from "../../../common/enums/message.enum"; + +export class UpdateUserProfileDto { + @IsOptional() + @IsNotEmpty({ message: UserMessage.PROFILE_PICTURE_NOT_EMPTY }) + @IsUrl({ protocols: ["https"] }, { message: UserMessage.PROFILE_PICTURE_URL }) + @ApiPropertyOptional({ description: "Profile picture URL", example: "https://example.com/profile.jpg" }) + profilePic?: string; +} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index fc440a1..b790230 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -20,6 +20,9 @@ export class User extends BaseEntity { @Property({ type: "varchar", length: 150, nullable: false }) password!: string; + @Property({ type: "varchar", length: 255, nullable: true }) + profilePic?: string; + @Property({ type: "varchar", length: 255, nullable: false }) emailAddress!: string; diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 4077752..e5e14dd 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -16,6 +16,7 @@ import { Template } from "../../templates/entities/template.entity"; import { PasswordService } from "../../utils/services/password.service"; import { CreateEmailUserDto } from "../DTO/create-email-user.dto"; import { UpdateEmailUserDto } from "../DTO/update-email-user.dto"; +import { UpdateUserProfileDto } from "../DTO/update-user-profile.dto"; import { UserListQueryDto } from "../DTO/user-list-query.dto"; import { Role } from "../entities/role.entity"; import { User } from "../entities/user.entity"; @@ -80,6 +81,16 @@ export class UsersService { }; } /*******************************/ + async updateProfile(userId: string, updateMeDto: UpdateUserProfileDto) { + const user = await this.userRepository.findOne({ id: userId, deletedAt: null }); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + + this.userRepository.assign(user, updateMeDto); + await this.em.flush(); + + return { message: UserMessage.USER_UPDATED_SUCCESSFULLY, user }; + } + /*******************************/ async findOneByIdWithEntityManager(id: string, em: EntityManager) { const user = await em.findOne(User, { id }, { populate: ["role"] }); diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index eded0f5..470b616 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -1,8 +1,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common"; -import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { CreateEmailUserDto } from "./DTO/create-email-user.dto"; import { UpdateEmailUserDto } from "./DTO/update-email-user.dto"; +import { UpdateUserProfileDto } from "./DTO/update-user-profile.dto"; import { UserListQueryDto } from "./DTO/user-list-query.dto"; import { UsersService } from "./services/users.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; @@ -12,7 +13,6 @@ import { ParamDto } from "../../common/DTO/param.dto"; import { BusinessInterceptor } from "../../core/interceptors/business.interceptor"; @Controller("users") -@ApiHeader({ name: "x-business-id", description: "Business ID" }) @AuthGuards() export class UsersController { constructor(private readonly usersService: UsersService) {} @@ -24,6 +24,21 @@ export class UsersController { return this.usersService.getMe(userId); } + @Patch(":id") + @UseInterceptors(BusinessInterceptor) + @ApiOperation({ summary: "Update email user" }) + @ApiResponse({ status: 200, description: "Email user updated successfully" }) + updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) { + return this.usersService.updateEmailUser(paramDto.id, businessId, body); + } + + @Patch("profile") + @ApiOperation({ summary: "Update current user" }) + @ApiResponse({ status: 200, description: "Current user updated successfully" }) + updateProfile(@UserDec("id") userId: string, @Body() updateMeDto: UpdateUserProfileDto) { + return this.usersService.updateProfile(userId, updateMeDto); + } + @Post() @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Create a new email user" }) @@ -48,14 +63,6 @@ export class UsersController { return this.usersService.deleteEmailUser(paramDto.id, businessId); } - @Patch(":id") - @UseInterceptors(BusinessInterceptor) - @ApiOperation({ summary: "Update email user" }) - @ApiResponse({ status: 200, description: "Email user updated successfully" }) - updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) { - return this.usersService.updateEmailUser(paramDto.id, businessId, body); - } - @Patch(":id/quota") @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Update email user quota" })