This commit is contained in:
2026-07-06 14:17:20 +03:30
parent bfc077df1c
commit 7f9358e637
9 changed files with 756 additions and 0 deletions
+8
View File
@@ -38,3 +38,11 @@ ZARINPAL_MERCHANT_ID=8bf4bd49-df53-44b3-a58d-b068d4c1ec98
ZARINPAL_SANDBOX=true ZARINPAL_SANDBOX=true
ZARINPAL_CALLBACK_URL=http://localhost:4000/api/payments/callback ZARINPAL_CALLBACK_URL=http://localhost:4000/api/payments/callback
ZARINPAL_RETURN_URL=http://localhost:3000/payment/result ZARINPAL_RETURN_URL=http://localhost:3000/payment/result
# Object Storage (S3-compatible / Parspack)
S3_ENDPOINT=https://c274938.parspack.net
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET=c274938
S3_REGION=us-east-1
# S3_PUBLIC_BASE_URL=https://c274938.parspack.net
+470
View File
@@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1079.0",
"@nestjs/axios": "^4.0.1", "@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.4", "@nestjs/config": "^4.0.4",
@@ -22,6 +23,7 @@
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"multer": "^2.2.0",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pg": "^8.22.0", "pg": "^8.22.0",
@@ -38,6 +40,7 @@
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/multer": "^2.2.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/supertest": "^7.0.0", "@types/supertest": "^7.0.0",
@@ -201,6 +204,314 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/@aws-sdk/checksums": {
"version": "3.1000.12",
"resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.12.tgz",
"integrity": "sha512-RgNDWfhNRIlNEzePIRrYTNi/6q+wwRMMapojn8YVzw4ZcJRa/gxVMtUbeZARR1gmopuv6oIhMbY7J66qIQ0ynw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/client-s3": {
"version": "3.1079.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1079.0.tgz",
"integrity": "sha512-di9U/7Po7qlVYb2dq58ULsbBAE1pBIk53rux+50LQCvH1X+/l1Ys+BIk/QLBtdaK1nADk0xRNEBbA1QWVnMccw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/checksums": "^3.1000.12",
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/credential-provider-node": "^3.972.62",
"@aws-sdk/middleware-sdk-s3": "^3.972.58",
"@aws-sdk/signature-v4-multi-region": "^3.996.38",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/fetch-http-handler": "^5.6.2",
"@smithy/node-http-handler": "^4.9.2",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/core": {
"version": "3.974.27",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.27.tgz",
"integrity": "sha512-WRWEgIq6vx+NU6ot3VrRu4Jovj9MIObitSi6of/GV5THDDPccBhivCRNkWJutMM+m3GvdeI3l/UbGNcoOobxOA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.15",
"@aws-sdk/xml-builder": "^3.972.33",
"@aws/lambda-invoke-store": "^0.3.0",
"@smithy/core": "^3.29.0",
"@smithy/signature-v4": "^5.6.1",
"@smithy/types": "^4.15.1",
"bowser": "^2.11.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-env": {
"version": "3.972.53",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.53.tgz",
"integrity": "sha512-+KDA3uc/HZ1vIneGu5QMQb0gAXDYrm2vOE60+BJ7lS0YinMQ5i2oV4PR1A16XkF6K1IbSwjEHd1hQIIgMsK48w==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-http": {
"version": "3.972.55",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.55.tgz",
"integrity": "sha512-1gBfkWY3RWeBlCoB9lIJjXMx45/54wxcgfzv6BY9otTmMrZPcNPi1v+MwZxxaCUg441NV3jsr1efnFNCXiW70g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/fetch-http-handler": "^5.6.2",
"@smithy/node-http-handler": "^4.9.2",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.972.60",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.60.tgz",
"integrity": "sha512-CV2md+PXvABwRjApWGhQ0wACy9WSFIhnUGrovLcjnjBCd/46TbuivLADtkF8IWNjtCQmQ+2IagSaxqBYqXBNAQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/credential-provider-env": "^3.972.53",
"@aws-sdk/credential-provider-http": "^3.972.55",
"@aws-sdk/credential-provider-login": "^3.972.59",
"@aws-sdk/credential-provider-process": "^3.972.53",
"@aws-sdk/credential-provider-sso": "^3.972.59",
"@aws-sdk/credential-provider-web-identity": "^3.972.59",
"@aws-sdk/nested-clients": "^3.997.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/credential-provider-imds": "^4.4.5",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-login": {
"version": "3.972.59",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.59.tgz",
"integrity": "sha512-JG4S9yyA1GFzJdJXqLKrUzZbyK+VDp2QIsJD7YOicJHAhqymfHpDJIok2dLnhOdVB0I37RjdC53uOwCMVS00gw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/nested-clients": "^3.997.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-node": {
"version": "3.972.62",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.62.tgz",
"integrity": "sha512-S6Slq3Tx7bvFk5yc34XNADyZYTX2HUXvaFAnowGRQnhjBO8J/mP62Fn7lxvJwjaDyYm/7gh9h6HEHaltRyMFXw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/credential-provider-env": "^3.972.53",
"@aws-sdk/credential-provider-http": "^3.972.55",
"@aws-sdk/credential-provider-ini": "^3.972.60",
"@aws-sdk/credential-provider-process": "^3.972.53",
"@aws-sdk/credential-provider-sso": "^3.972.59",
"@aws-sdk/credential-provider-web-identity": "^3.972.59",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/credential-provider-imds": "^4.4.5",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-process": {
"version": "3.972.53",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.53.tgz",
"integrity": "sha512-EhfH+MQlqOMCkXIVa8MMObPzAQqwTTtxA7KhEJiyPeuNVA8PLOOUpgK7nBrgaDaGiIDLN/9LpGdaHuDjomeRTw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.972.59",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.59.tgz",
"integrity": "sha512-h8793pOjcImx0SB+VcLONcaQQ57VAvKVuqyewQMRKqqH+CSXsG2dwOeLMUJPMxLdNvL7dXOM0ueTukyNUnu5mA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/nested-clients": "^3.997.27",
"@aws-sdk/token-providers": "3.1079.0",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.972.59",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.59.tgz",
"integrity": "sha512-VoyO9+vl3XVmpZwn4obskrWIkrA/Jf3lSe1E3ZERlaN9u0D4YZ6+HywC3+L98QOXqZesEfedk67gRER8tK8+8w==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/nested-clients": "^3.997.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/middleware-sdk-s3": {
"version": "3.972.58",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.58.tgz",
"integrity": "sha512-6uaWRRYJGhOqc9EoTSbLDf9nI/doSAb5vAwGshs5/Hlv5Ce25b246lBkbRd/77fLAi+uMI1a70mJzVyLyCEufQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/signature-v4-multi-region": "^3.996.38",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/nested-clients": {
"version": "3.997.27",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.27.tgz",
"integrity": "sha512-A8PIePF9NIIOJ/4Lg1rl9xm/+QaKkHGetq+Z9wb5B+3Da31YYXRo8n7IDMh5C+HQI5eyEmjrwkGWVdYtnLtbXQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/signature-v4-multi-region": "^3.996.38",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/fetch-http-handler": "^5.6.2",
"@smithy/node-http-handler": "^4.9.2",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/signature-v4-multi-region": {
"version": "3.996.38",
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.38.tgz",
"integrity": "sha512-C379Sk+MiFZCfWZphKlMyLHKxV22OjoGM5KJjj5IJNJcOCWL4IGIpnEGzv1FQiRwhYXfq55SJMfxlqPE08JJ9g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.15",
"@smithy/signature-v4": "^5.6.1",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/token-providers": {
"version": "3.1079.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1079.0.tgz",
"integrity": "sha512-cbietrLlHPhhmbnMPTuDS4Zj/KNGhY+3vVhn6dwjO6Dqzrwothzg2srtcY34T9mlICsTXn34avDoWLHSntP54A==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "^3.974.27",
"@aws-sdk/nested-clients": "^3.997.27",
"@aws-sdk/types": "^3.973.15",
"@smithy/core": "^3.29.0",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/types": {
"version": "3.973.15",
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.15.tgz",
"integrity": "sha512-IULn8uBV/SMtmOIANsm4WHXIOtVPBWfOWs3WGL0j/sI+KhaYehvOw0ET+9urnn8MBpiijuU/0JOpuwKOE451PQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/xml-builder": {
"version": "3.972.33",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.33.tgz",
"integrity": "sha512-ezbwz9WpuLctm6o7P2t2naDhVVPI5jFGrVefVybhcKGjU57VIyT46pQVO0RI2RYkUdhdj2Z9uSIlAzGZE9NW9A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws/lambda-invoke-store": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.7", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -2770,6 +3081,87 @@
"@sinonjs/commons": "^3.0.1" "@sinonjs/commons": "^3.0.1"
} }
}, },
"node_modules/@smithy/core": {
"version": "3.29.1",
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.1.tgz",
"integrity": "sha512-qoiY4nrk5OCu1+eIR1VB8l5DmON/oKiqrd5zZFAhXJXjJlLWQusKEW/SkBDAtGDcPaz86m9kfcE1lngU0GlM6A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/credential-provider-imds": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.6.tgz",
"integrity": "sha512-B2WQ/PV/H6Jeg3lrIq6bKUfa6Hy01mtK7CGs6lhjzHA6k4aagldH6T6eEjnzKl4HI0cJnAsxfJ19pgb5PV+CVQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.1",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/fetch-http-handler": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.3.tgz",
"integrity": "sha512-CwCc/7SMTj45y97MUnDTbTaxvtAsiNNRm81z3abROIuMbMsC2Iy5EKfkkVdsKrz8WExQAAMx1EJapq+9j4fFTQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.1",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/node-http-handler": {
"version": "4.9.3",
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.3.tgz",
"integrity": "sha512-qZTa4gQFUo8RM02rk6q5UVTDLNrQ1oS20LsepBzqq1QBVc/EHJ03OOUADcqMZiXHArW+Y7+OGY0BpdTwZRq/Yg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.1",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/signature-v4": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.2.tgz",
"integrity": "sha512-QgHflghMoPxCJ9axiCVh8KZfbC9fuP6vkXXyK//E3cq7nLaSSyyLj0GAoqVWezYeDQmXIZhmlRvLE16jsqDK6g==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/core": "^3.29.1",
"@smithy/types": "^4.15.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/types": {
"version": "4.15.1",
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.1.tgz",
"integrity": "sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@sqltools/formatter": { "node_modules/@sqltools/formatter": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
@@ -3050,6 +3442,16 @@
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/multer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz",
"integrity": "sha512-3U1troeqGV8Ntp7Q3klwf4zr23VEoqYVocYXaswm9+8z3O9UHDYAqLxjJ/h550iRADTjKdOdhhasXw6gD6kYtg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.13.2", "version": "24.13.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
@@ -4472,6 +4874,12 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/bowser": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.15", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
@@ -8026,6 +8434,68 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/multer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/multer/node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/multer/node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mute-stream": { "node_modules/mute-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+3
View File
@@ -24,6 +24,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1079.0",
"@nestjs/axios": "^4.0.1", "@nestjs/axios": "^4.0.1",
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.4", "@nestjs/config": "^4.0.4",
@@ -37,6 +38,7 @@
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.15.1", "class-validator": "^0.15.1",
"multer": "^2.2.0",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"pg": "^8.22.0", "pg": "^8.22.0",
@@ -53,6 +55,7 @@
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^30.0.0", "@types/jest": "^30.0.0",
"@types/multer": "^2.2.0",
"@types/node": "^24.0.0", "@types/node": "^24.0.0",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/supertest": "^7.0.0", "@types/supertest": "^7.0.0",
+2
View File
@@ -16,6 +16,7 @@ import { StylistsModule } from './stylists/stylists.module';
import { SubscriptionsModule } from './subscriptions/subscriptions.module'; import { SubscriptionsModule } from './subscriptions/subscriptions.module';
import { UsersModule } from './users/users.module'; import { UsersModule } from './users/users.module';
import { PaymentsModule } from './payments/payments.module'; import { PaymentsModule } from './payments/payments.module';
import { UploaderModule } from './uploader/uploader.module';
@Module({ @Module({
imports: [ imports: [
@@ -37,6 +38,7 @@ import { PaymentsModule } from './payments/payments.module';
SentMessagesModule, SentMessagesModule,
SubscriptionsModule, SubscriptionsModule,
PaymentsModule, PaymentsModule,
UploaderModule,
], ],
controllers: [], controllers: [],
providers: [], providers: [],
+7
View File
@@ -31,5 +31,12 @@ export default () => {
'http://localhost:4000/api/payments/callback', 'http://localhost:4000/api/payments/callback',
ZARINPAL_RETURN_URL: ZARINPAL_RETURN_URL:
process.env.ZARINPAL_RETURN_URL ?? 'http://localhost:3000/payment/result', process.env.ZARINPAL_RETURN_URL ?? 'http://localhost:3000/payment/result',
S3_ENDPOINT: process.env.S3_ENDPOINT ?? 'https://c274938.parspack.net',
S3_ACCESS_KEY: process.env.S3_ACCESS_KEY ?? '',
S3_SECRET_KEY: process.env.S3_SECRET_KEY ?? '',
S3_BUCKET: process.env.S3_BUCKET ?? 'c274938',
S3_REGION: process.env.S3_REGION ?? 'us-east-1',
S3_PUBLIC_BASE_URL:
process.env.S3_PUBLIC_BASE_URL ?? process.env.S3_ENDPOINT ?? 'https://c274938.parspack.net',
}; };
}; };
+23
View File
@@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
export class UploadFileResponseDto {
@ApiProperty({ example: 'uploads/2026/07/uuid-photo.jpg' })
key: string;
@ApiProperty({ example: 'https://c274938.parspack.net/uploads/2026/07/uuid-photo.jpg' })
url: string;
@ApiProperty({ example: 'photo.jpg' })
originalName: string;
@ApiProperty({ example: 'image/jpeg' })
mimeType: string;
@ApiProperty({ example: 102400 })
size: number;
}
export class MultiUploadResponseDto {
@ApiProperty({ type: [UploadFileResponseDto] })
files: UploadFileResponseDto[];
}
+100
View File
@@ -0,0 +1,100 @@
import {
Controller,
Post,
UploadedFile,
UploadedFiles,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import {
ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiTags,
} from '@nestjs/swagger';
import { memoryStorage } from 'multer';
import { AllowWithoutSubscription } from '../auth/decorators/allow-without-subscription.decorator';
import { Roles } from '../auth/decorators/roles.decorator';
import { RolesGuard } from '../auth/guards/roles.guard';
import { PermissionsGuard } from '../auth/guards/permissions.guard';
import { UserRole } from '../users/enums/user-role.enum';
import { SWAGGER_JWT_AUTH } from '../swagger/swagger.setup';
import {
ApiStandardController,
ApiStandardOkResponse,
} from '../swagger/decorators/swagger-response.decorator';
import {
MultiUploadResponseDto,
UploadFileResponseDto,
} from './dto/upload-response.dto';
import { UploaderService } from './uploader.service';
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
@ApiTags('Uploader')
@ApiBearerAuth(SWAGGER_JWT_AUTH)
@ApiStandardController()
@Controller('uploader')
@UseGuards(RolesGuard, PermissionsGuard)
export class UploaderController {
constructor(private readonly uploaderService: UploaderService) {}
@Roles(UserRole.ADMIN, UserRole.SALON)
@AllowWithoutSubscription()
@Post('single-upload')
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
required: ['file'],
properties: {
file: {
type: 'string',
format: 'binary',
},
},
},
})
@ApiStandardOkResponse(UploadFileResponseDto)
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(),
limits: { fileSize: MAX_FILE_SIZE_BYTES },
}),
)
singleUpload(@UploadedFile() file: Express.Multer.File) {
return this.uploaderService.uploadSingle(file);
}
@Roles(UserRole.ADMIN, UserRole.SALON)
@AllowWithoutSubscription()
@Post('multi-upload')
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
required: ['files'],
properties: {
files: {
type: 'array',
items: {
type: 'string',
format: 'binary',
},
},
},
},
})
@ApiStandardOkResponse(MultiUploadResponseDto)
@UseInterceptors(
FilesInterceptor('files', 10, {
storage: memoryStorage(),
limits: { fileSize: MAX_FILE_SIZE_BYTES },
}),
)
async multiUpload(@UploadedFiles() files: Express.Multer.File[]) {
const uploadedFiles = await this.uploaderService.uploadMultiple(files);
return { files: uploadedFiles };
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthGuardsModule } from '../auth/auth-guards.module';
import { UploaderController } from './uploader.controller';
import { UploaderService } from './uploader.service';
@Module({
imports: [AuthGuardsModule],
controllers: [UploaderController],
providers: [UploaderService],
exports: [UploaderService],
})
export class UploaderModule {}
+131
View File
@@ -0,0 +1,131 @@
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import {
BadRequestException,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { randomUUID } from 'crypto';
import { extname } from 'path';
import { UploadFileResponseDto } from './dto/upload-response.dto';
const ALLOWED_MIME_TYPES = new Set([
'image/jpeg',
'image/png',
'image/webp',
'image/gif',
'image/svg+xml',
'application/pdf',
]);
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
@Injectable()
export class UploaderService {
private readonly s3Client: S3Client;
private readonly bucket: string;
private readonly publicBaseUrl: string;
constructor(private readonly configService: ConfigService) {
const endpoint = this.configService.getOrThrow<string>('S3_ENDPOINT');
const accessKeyId = this.configService.getOrThrow<string>('S3_ACCESS_KEY');
const secretAccessKey =
this.configService.getOrThrow<string>('S3_SECRET_KEY');
this.bucket = this.configService.getOrThrow<string>('S3_BUCKET');
this.publicBaseUrl = (
this.configService.get<string>('S3_PUBLIC_BASE_URL') ?? endpoint
).replace(/\/$/, '');
this.s3Client = new S3Client({
endpoint,
region: this.configService.get<string>('S3_REGION') ?? 'us-east-1',
credentials: { accessKeyId, secretAccessKey },
forcePathStyle: true,
});
}
async uploadSingle(file: Express.Multer.File): Promise<UploadFileResponseDto> {
this.validateFile(file);
return this.uploadFile(file);
}
async uploadMultiple(
files: Express.Multer.File[],
): Promise<UploadFileResponseDto[]> {
if (!files?.length) {
throw new BadRequestException('At least one file is required');
}
for (const file of files) {
this.validateFile(file);
}
return Promise.all(files.map((file) => this.uploadFile(file)));
}
private validateFile(file: Express.Multer.File): void {
if (!file) {
throw new BadRequestException('File is required');
}
if (file.size > MAX_FILE_SIZE_BYTES) {
throw new BadRequestException(
`File size must not exceed ${MAX_FILE_SIZE_BYTES / (1024 * 1024)}MB`,
);
}
if (!ALLOWED_MIME_TYPES.has(file.mimetype)) {
throw new BadRequestException(
`Unsupported file type: ${file.mimetype}. Allowed: ${[...ALLOWED_MIME_TYPES].join(', ')}`,
);
}
}
private async uploadFile(
file: Express.Multer.File,
): Promise<UploadFileResponseDto> {
const key = this.buildObjectKey(file.originalname);
try {
await this.s3Client.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
ACL: 'public-read',
}),
);
} catch (error) {
throw new InternalServerErrorException(
`Failed to upload file: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
return {
key,
url: this.buildPublicUrl(key),
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size,
};
}
private buildPublicUrl(key: string): string {
return `${this.publicBaseUrl}/${this.bucket}/${key}`;
}
private buildObjectKey(originalName: string): string {
const now = new Date();
const year = now.getUTCFullYear();
const month = String(now.getUTCMonth() + 1).padStart(2, '0');
const extension = extname(originalName).toLowerCase();
const safeName = originalName
.replace(extname(originalName), '')
.replace(/[^a-zA-Z0-9._-]/g, '-')
.slice(0, 80);
return `uploads/${year}/${month}/${randomUUID()}-${safeName || 'file'}${extension}`;
}
}