add auth and payment api
This commit is contained in:
13
.env
13
.env
@@ -6,3 +6,16 @@ DATABASE_PORT=5432
|
||||
DATABASE_USERNAME=malma
|
||||
DATABASE_PASSWORD=kucing
|
||||
DATABASE_NAME=goumrah
|
||||
|
||||
JWT_SECRET=goumrah
|
||||
JWT_ALGORITHM=HS256
|
||||
JWT_ISSUER=http://localhost:3000
|
||||
|
||||
MIDTRANS_BASE_URL=https://app.sandbox.midtrans.com/snap/v1
|
||||
MIDTRANS_MERCHANT_ID=G908685501
|
||||
MIDTRANS_SERVER_KEY=SB-Mid-server-caiT6pDw8na31mxUBRhvtVPk
|
||||
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_PORT=465
|
||||
MAIL_USERNAME=admin@goumrah.id
|
||||
MAIL_PASSWORD=oscm zldv npls wyzz
|
||||
|
||||
13
.env.example
13
.env.example
@@ -6,3 +6,16 @@ DATABASE_PORT=
|
||||
DATABASE_USER=
|
||||
DATABASE_PASSWORD=
|
||||
DATABASE_NAME=
|
||||
|
||||
JWT_SECRET=
|
||||
JWT_ALGORITHM=
|
||||
JWT_ISSUER=
|
||||
|
||||
MIDTRANS_BASE_URL=
|
||||
MIDTRANS_MERCHANT_ID=
|
||||
MIDTRANS_SERVER_KEY=
|
||||
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
|
||||
208
bun.lock
208
bun.lock
@@ -13,6 +13,8 @@
|
||||
"express": "5.1.0",
|
||||
"file-type": "21.0.0",
|
||||
"helmet": "8.1.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"nodemailer": "7.0.10",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"slugify": "1.6.6",
|
||||
"ulid": "3.0.1",
|
||||
@@ -23,12 +25,76 @@
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/nodemailer": "7.0.4",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.9.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@aws-crypto/sha256-browser": ["@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.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
|
||||
|
||||
"@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
|
||||
|
||||
"@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-sesv2": ["@aws-sdk/client-sesv2@3.934.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.934.0", "@aws-sdk/credential-provider-node": "3.934.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.934.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/signature-v4-multi-region": "3.934.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.934.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Sq2vikq+RDNgRkMN1tpeciNK59mY9FYDvYqrfNtA62urZ8zxD39pmrNK1gEIjWhYiVH2aEXLCxnx7njfwMinCg=="],
|
||||
|
||||
"@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.934.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.934.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.934.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.934.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-gsgJevqhY0j3x014ejhXtHLCA6o83FYm3rJoZG7tqoy3DnWerLv/FHaAnHI/+Q+csadqjoFkWGQTOedPoOunzA=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.934.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/xml-builder": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-b6k916ZxSrBwQPzeirncTIQXGnhps0HFOUakFt0ZEzjksePYUiEoU/SQ7VeY1j9JeAdJ24ejqddCiyLt99/3lg=="],
|
||||
|
||||
"@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-bnpIGYm7Jy46dxZa1cxMQ1sF0n2iBIT+TpOPHK51sz1N2dYOicUVWUHMDgU2xIFOVcKaqV+GV4VyicMmvDBcBQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/property-provider": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-WJcfFik7MPIgjE8lmuDcCqddHKRMpifzoBzTZWqUJJWYXIy0rDfNzt6pn3/TMLwVgnCGjnXlw6dChTxLzO60RQ=="],
|
||||
|
||||
"@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/credential-provider-env": "3.934.0", "@aws-sdk/credential-provider-http": "3.934.0", "@aws-sdk/credential-provider-process": "3.934.0", "@aws-sdk/credential-provider-sso": "3.934.0", "@aws-sdk/credential-provider-web-identity": "3.934.0", "@aws-sdk/nested-clients": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-3vVKGe1F2S09G9kC0ZcpWh09opyrGOgQETllqWbuxlTVd7zBgrZWloItLIvneSDP+dWvdLFUbkD7WDWNCeGiig=="],
|
||||
|
||||
"@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.934.0", "", { "dependencies": { "@aws-sdk/credential-provider-env": "3.934.0", "@aws-sdk/credential-provider-http": "3.934.0", "@aws-sdk/credential-provider-ini": "3.934.0", "@aws-sdk/credential-provider-process": "3.934.0", "@aws-sdk/credential-provider-sso": "3.934.0", "@aws-sdk/credential-provider-web-identity": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-nguy36xi8nbH346dJjCmwWtOgfS4VfL7yHP+EEGmma+yg+J7mxgs8kA1NGQdJ8B46GdjlJPpI1P9pm7Pmz7nOw=="],
|
||||
|
||||
"@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-PhvpAgoJ88IOuqlUws9nvHuPex2jK+WS+0s00BQcRTwqPP0jtLT7eql6UfCRduwv2sIy3m1wnWDUubvbpejp/Q=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.934.0", "", { "dependencies": { "@aws-sdk/client-sso": "3.934.0", "@aws-sdk/core": "3.934.0", "@aws-sdk/token-providers": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-7wO86w95V9MZSYo2dunBKruKHdAUmgg9ccOSJSYGnPip1PPBK/rgSgQ8mDlYtFAW3/82bdeM/668QcgLT4+ofA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/nested-clients": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-hb+lvFxiAPcAvUorB0hrUd1kDjDRXhZgCi5426I8KUpGzZ+ALh8/ep0KXAiYe2yg9ZkyMUbMaMvYYhMFcbXRFA=="],
|
||||
|
||||
"@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-x30jmm3TLu7b/b+67nMyoV0NlbnCVT5DI57yDrhXAPCtdgM1KtdLWt45UcHpKOm1JsaIkmYRh2WYu7Anx4MG0g=="],
|
||||
|
||||
"@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-vh4JBWzMCBW8wREvAwoSqB2geKsZwSHTa0nSt0OMOLp2PdTYIZDi0ZiVMmpfnjcx9XbS6aSluLv9sKx4RrG46A=="],
|
||||
|
||||
"@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.933.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws/lambda-invoke-store": "^0.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-qgrMlkVKzTCAdNw2A05DC2sPBo0KRQ7wk+lbYSRJnWVzcrceJhnmhoZVV5PFv7JtchK7sHVcfm9lcpiyd+XaCA=="],
|
||||
|
||||
"@aws-sdk/middleware-sdk-s3": ["@aws-sdk/middleware-sdk-s3@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/core": "^3.18.2", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-eU2R7pVOhCxnkDzq9mW+xh4WvCA3mdXVUHezIcJNFyKCKKv/c9I4WFcnMnUy+wnCWO2mzN/gwSgQxADkvxfLNQ=="],
|
||||
|
||||
"@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@smithy/core": "^3.18.2", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-68giGM2Zm9K6Qas14ws3Qo5wafpn0I8/L64fS9E6Rc6Tu0k+So73hupysw+9ZOzHwQS5FEBUqLOMtbUibAcjNA=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.934.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.934.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.934.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.934.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kRO61EMrDR4UuPlKAkziG6urcYXlhrFW/Ce5PjWFdjkm0ZOge75OFV1vhf/vE4Pmoop9jaAONX4E5BaIYrIQfg=="],
|
||||
|
||||
"@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/config-resolver": "^4.4.3", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-KL2JZqH6aYeQssu1g1KuWsReupdfOoxD6f1as2VC+rdwYFUu4LfzMsFfXnBvvQWWqQ7rZHWOw1T+o5gJmg7Dzw=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.934.0", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/signature-v4": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-cLphxVoHapSdouAdLSDEwR2Bktjg5dc11EpSpaLo8jcFpAXhFaDllKBfDfws0EqGY6N2CMqEjqPqxDFzmmQOQA=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.934.0", "", { "dependencies": { "@aws-sdk/core": "3.934.0", "@aws-sdk/nested-clients": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-M0WEmgXDdUxapSfjplqJoVCBMcn0vQ5Jou0X/XiQwyVDbfvIyNSHUHyMXEIBAew9kVx9sfMMEYz3LXewvQxdCA=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-we/vaAgwlEFW7IeftmCLlLMw+6hFs3DzZPJw7lVHbj/5HJ0bz9gndxEsS2lQoeJ1zhiiLqAqvXxmM43s0MBg0A=="],
|
||||
|
||||
"@aws-sdk/util-arn-parser": ["@aws-sdk/util-arn-parser@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA=="],
|
||||
|
||||
"@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-endpoints": "^3.2.5", "tslib": "^2.6.2" } }, "sha512-M2oEKBzzNAYr136RRc6uqw3aWlwCxqTP1Lawps9E1d2abRPvl1p1ztQmmXp1Ak4rv8eByIZ+yQyKQ3zPdRG5dw=="],
|
||||
|
||||
"@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.893.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.930.0", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/types": "^4.9.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-q6lCRm6UAe+e1LguM5E4EqM9brQlDem4XDcQ87NzEvlTW6GzmNCO0w1jS0XgCFXQHjDxjdlNFX+5sRbHijwklg=="],
|
||||
|
||||
"@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.934.0", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "3.934.0", "@aws-sdk/types": "3.930.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-vPRR4PaqNmuOQJSzq4EAVwFHUaSpPtgDgCEc7AYbArIy+59fckb6JNddlrjx4w4iWbqO0d+7OC5PtRcIk0AcZA=="],
|
||||
|
||||
"@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.930.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" } }, "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA=="],
|
||||
|
||||
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.0", "", {}, "sha512-D1jAmAZQYMoPiacfgNf7AWhg3DFN3Wq/vQv3WINt9znwjzHp2x+WzdJFxxj7xZL7V1U79As6G8f7PorMYWBKsQ=="],
|
||||
|
||||
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
|
||||
|
||||
"@jercle/yargonaut": ["@jercle/yargonaut@1.1.5", "", { "dependencies": { "chalk": "^4.1.2", "figlet": "^1.5.2", "parent-require": "^1.0.0" } }, "sha512-zBp2myVvBHp1UaJsNTyS6q4UDKT7eRiqTS4oNTS6VQMd6mpxYOdbeK4pY279cDCdakGy6hG0J3ejoXZVsPwHqw=="],
|
||||
@@ -55,6 +121,86 @@
|
||||
|
||||
"@rushstack/ts-command-line": ["@rushstack/ts-command-line@4.23.7", "", { "dependencies": { "@rushstack/terminal": "0.15.2", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-Gr9cB7DGe6uz5vq2wdr89WbVDKz0UeuFEn5H2CfWDe7JvjFFaiV15gi6mqDBTbHhHCWS7w8mF1h3BnIfUndqdA=="],
|
||||
|
||||
"@smithy/abort-controller": ["@smithy/abort-controller@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA=="],
|
||||
|
||||
"@smithy/config-resolver": ["@smithy/config-resolver@4.4.3", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.18.4", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.6", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-stream": "^4.5.6", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-o5tMqPZILBvvROfC8vC+dSVnWJl9a0u9ax1i1+Bq8515eYjUJqqk5XjjEsDLoeL5dSqGSh6WGdVx1eJ1E/Nwhw=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ=="],
|
||||
|
||||
"@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg=="],
|
||||
|
||||
"@smithy/hash-node": ["@smithy/hash-node@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA=="],
|
||||
|
||||
"@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A=="],
|
||||
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="],
|
||||
|
||||
"@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.5", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A=="],
|
||||
|
||||
"@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.3.11", "", { "dependencies": { "@smithy/core": "^3.18.4", "@smithy/middleware-serde": "^4.2.6", "@smithy/node-config-provider": "^4.3.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-middleware": "^4.2.5", "tslib": "^2.6.2" } }, "sha512-eJXq9VJzEer1W7EQh3HY2PDJdEcEUnv6sKuNt4eVjyeNWcQFS4KmnY+CKkYOIR6tSqarn6bjjCqg1UB+8UJiPQ=="],
|
||||
|
||||
"@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.11", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/service-error-classification": "^4.2.5", "@smithy/smithy-client": "^4.9.7", "@smithy/types": "^4.9.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-EL5OQHvFOKneJVRgzRW4lU7yidSwp/vRJOe542bHgExN3KNThr1rlg0iE4k4SnA+ohC+qlUxoK+smKeAYPzfAQ=="],
|
||||
|
||||
"@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.6", "", { "dependencies": { "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ=="],
|
||||
|
||||
"@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ=="],
|
||||
|
||||
"@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.5", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/shared-ini-file-loader": "^4.4.0", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg=="],
|
||||
|
||||
"@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.5", "", { "dependencies": { "@smithy/abort-controller": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/querystring-builder": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw=="],
|
||||
|
||||
"@smithy/property-provider": ["@smithy/property-provider@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg=="],
|
||||
|
||||
"@smithy/protocol-http": ["@smithy/protocol-http@5.3.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ=="],
|
||||
|
||||
"@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg=="],
|
||||
|
||||
"@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ=="],
|
||||
|
||||
"@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0" } }, "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ=="],
|
||||
|
||||
"@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.0", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA=="],
|
||||
|
||||
"@smithy/signature-v4": ["@smithy/signature-v4@5.3.5", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.5", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w=="],
|
||||
|
||||
"@smithy/smithy-client": ["@smithy/smithy-client@4.9.7", "", { "dependencies": { "@smithy/core": "^3.18.4", "@smithy/middleware-endpoint": "^4.3.11", "@smithy/middleware-stack": "^4.2.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-stream": "^4.5.6", "tslib": "^2.6.2" } }, "sha512-pskaE4kg0P9xNQWihfqlTMyxyFR3CH6Sr6keHYghgyqqDXzjl2QJg5lAzuVe/LzZiOzcbcVtxKYi1/fZPt/3DA=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.9.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA=="],
|
||||
|
||||
"@smithy/url-parser": ["@smithy/url-parser@4.2.5", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ=="],
|
||||
|
||||
"@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="],
|
||||
|
||||
"@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="],
|
||||
|
||||
"@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="],
|
||||
|
||||
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="],
|
||||
|
||||
"@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="],
|
||||
|
||||
"@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.10", "", { "dependencies": { "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.7", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-3iA3JVO1VLrP21FsZZpMCeF93aqP3uIOMvymAT3qHIJz2YlgDeRvNUspFwCNqd/j3qqILQJGtsVQnJZICh/9YA=="],
|
||||
|
||||
"@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.13", "", { "dependencies": { "@smithy/config-resolver": "^4.4.3", "@smithy/credential-provider-imds": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/property-provider": "^4.2.5", "@smithy/smithy-client": "^4.9.7", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-PTc6IpnpSGASuzZAgyUtaVfOFpU0jBD2mcGwrgDuHf7PlFgt5TIPxCYBDbFQs06jxgeV3kd/d/sok1pzV0nJRg=="],
|
||||
|
||||
"@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.5", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A=="],
|
||||
|
||||
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="],
|
||||
|
||||
"@smithy/util-middleware": ["@smithy/util-middleware@4.2.5", "", { "dependencies": { "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA=="],
|
||||
|
||||
"@smithy/util-retry": ["@smithy/util-retry@4.2.5", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg=="],
|
||||
|
||||
"@smithy/util-stream": ["@smithy/util-stream@4.5.6", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.6", "@smithy/node-http-handler": "^4.4.5", "@smithy/types": "^4.9.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ=="],
|
||||
|
||||
"@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="],
|
||||
|
||||
"@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.2.7", "", { "dependencies": { "debug": "^4.4.0", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg=="],
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
@@ -77,10 +223,16 @@
|
||||
|
||||
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
|
||||
|
||||
"@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
|
||||
|
||||
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
|
||||
|
||||
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="],
|
||||
|
||||
"@types/nodemailer": ["@types/nodemailer@7.0.4", "", { "dependencies": { "@aws-sdk/client-sesv2": "^3.839.0", "@types/node": "*" } }, "sha512-ee8fxWqOchH+Hv6MDDNNy028kwvVnLplrStm4Zf/3uHWw5zzo8FoYYeffpJtGs2wWysEumMH0ZIdMGMY1eMAow=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
|
||||
@@ -109,8 +261,12 @@
|
||||
|
||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
|
||||
"bowser": ["bowser@2.12.1", "", {}, "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
@@ -161,6 +317,8 @@
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="],
|
||||
@@ -191,6 +349,8 @@
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-xml-parser": ["fast-xml-parser@5.2.5", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
@@ -273,10 +433,30 @@
|
||||
|
||||
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
||||
|
||||
"jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="],
|
||||
|
||||
"jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="],
|
||||
|
||||
"jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="],
|
||||
|
||||
"knex": ["knex@3.1.0", "", { "dependencies": { "colorette": "2.0.19", "commander": "^10.0.0", "debug": "4.3.4", "escalade": "^3.1.1", "esm": "^3.2.25", "get-package-type": "^0.1.0", "getopts": "2.3.0", "interpret": "^2.2.0", "lodash": "^4.17.21", "pg-connection-string": "2.6.2", "rechoir": "^0.8.0", "resolve-from": "^5.0.0", "tarn": "^3.0.2", "tildify": "2.0.0" }, "bin": { "knex": "bin/cli.js" } }, "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
|
||||
|
||||
"lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
|
||||
|
||||
"lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
|
||||
|
||||
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
|
||||
|
||||
"lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
|
||||
|
||||
"lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
@@ -297,10 +477,12 @@
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
||||
|
||||
"nodemailer": ["nodemailer@7.0.10", "", {}, "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
@@ -421,6 +603,8 @@
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
||||
"strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
|
||||
|
||||
"strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
@@ -439,6 +623,8 @@
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
@@ -477,6 +663,10 @@
|
||||
|
||||
"zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
||||
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@rushstack/terminal/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
|
||||
|
||||
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
@@ -485,6 +675,8 @@
|
||||
|
||||
"body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"express/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"finalhandler/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
@@ -509,18 +701,14 @@
|
||||
|
||||
"send/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"express/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"finalhandler/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"knex/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
||||
|
||||
"router/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
"@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"dev": "bun run src/server.ts",
|
||||
"dev:watch": "bun run --watch src/server.ts"
|
||||
},
|
||||
@@ -19,6 +20,8 @@
|
||||
"express": "5.1.0",
|
||||
"file-type": "21.0.0",
|
||||
"helmet": "8.1.0",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"nodemailer": "7.0.10",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"slugify": "1.6.6",
|
||||
"ulid": "3.0.1",
|
||||
@@ -29,6 +32,8 @@
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/express": "5.0.5",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/nodemailer": "7.0.4",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.9.3"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { AbstractEmailService } from "@/common/services/email-service/abstract.email-service";
|
||||
import { LibraryEmailService } from "@/common/services/email-service/library.email-service";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import { LocalFileStorage } from "@/common/services/file-storage/local.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import { LibraryJwtService } from "@/common/services/jwt-service/library.jwt-service";
|
||||
import type { AbstractPaymentService } from "@/common/services/payment-service/abstract.payment-service";
|
||||
import { MidtransPaymentService } from "@/common/services/payment-service/midtrans.payment-service";
|
||||
import { serverConfig } from "@/configs/server.config";
|
||||
import { AdminController } from "@/modules/admin/admin.controller";
|
||||
import { AdminMapper } from "@/modules/admin/admin.mapper";
|
||||
import { AirlineController } from "@/modules/airline/airline.controller";
|
||||
import { AirlineMapper } from "@/modules/airline/airline.mapper";
|
||||
import { AirportController } from "@/modules/airport/airport.controller";
|
||||
@@ -15,8 +23,12 @@ import { HotelFacilityController } from "@/modules/hotel-facility/hotel-facility
|
||||
import { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
|
||||
import { HotelController } from "@/modules/hotel/hotel.controller";
|
||||
import { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||
import { OrderController } from "@/modules/order/order.controller";
|
||||
import { OrderMapper } from "@/modules/order/order.mapper";
|
||||
import { PackageController } from "@/modules/package/package.controller";
|
||||
import { PackageMapper } from "@/modules/package/package.mapper";
|
||||
import { PartnerController } from "@/modules/partner/partner.controller";
|
||||
import { PartnerMapper } from "@/modules/partner/partner.mapper";
|
||||
import { TransportationController } from "@/modules/transportation/transportation.controller";
|
||||
import { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||
import compression from "compression";
|
||||
@@ -28,14 +40,20 @@ export class Application {
|
||||
private readonly _app: express.Application;
|
||||
|
||||
// Services
|
||||
private _emailService!: AbstractEmailService;
|
||||
private _fileStorage!: AbstractFileStorage;
|
||||
private _jwtService!: AbstractJwtService;
|
||||
private _paymentService!: AbstractPaymentService;
|
||||
|
||||
public constructor() {
|
||||
this._app = express();
|
||||
}
|
||||
|
||||
public initializeServices() {
|
||||
this._emailService = new LibraryEmailService();
|
||||
this._fileStorage = new LocalFileStorage();
|
||||
this._jwtService = new LibraryJwtService();
|
||||
this._paymentService = new MidtransPaymentService();
|
||||
}
|
||||
|
||||
public initializeMiddlewares() {
|
||||
@@ -60,29 +78,66 @@ export class Application {
|
||||
hotelMapper,
|
||||
transportationMapper,
|
||||
);
|
||||
const adminMapper = new AdminMapper();
|
||||
const partnerMapper = new PartnerMapper();
|
||||
const orderMapper = new OrderMapper(packageMapper, partnerMapper);
|
||||
|
||||
const countryRouter = new CountryController(countryMapper).buildRouter();
|
||||
const cityRouter = new CityController(cityMapper).buildRouter();
|
||||
const countryRouter = new CountryController(
|
||||
countryMapper,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const cityRouter = new CityController(
|
||||
cityMapper,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const airlineRouter = new AirlineController(
|
||||
airlineMapper,
|
||||
this._fileStorage,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const airportRouter = new AirportController(
|
||||
airportMapper,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const flightRouter = new FlightController(
|
||||
flightMapper,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const airportRouter = new AirportController(airportMapper).buildRouter();
|
||||
const flightRouter = new FlightController(flightMapper).buildRouter();
|
||||
const hotelFacilityRouter = new HotelFacilityController(
|
||||
hotelFacilityMapper,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const hotelRouter = new HotelController(
|
||||
hotelMapper,
|
||||
this._fileStorage,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const transportationRouter = new TransportationController(
|
||||
transportationMapper,
|
||||
this._fileStorage,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const packageRouter = new PackageController(
|
||||
packageMapper,
|
||||
this._fileStorage,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const adminRouter = new AdminController(
|
||||
adminMapper,
|
||||
this._fileStorage,
|
||||
this._emailService,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const partnerRouter = new PartnerController(
|
||||
partnerMapper,
|
||||
this._fileStorage,
|
||||
this._emailService,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
const orderRouter = new OrderController(
|
||||
orderMapper,
|
||||
this._paymentService,
|
||||
this._jwtService,
|
||||
).buildRouter();
|
||||
|
||||
this._app.use("/countries", countryRouter);
|
||||
@@ -94,6 +149,9 @@ export class Application {
|
||||
this._app.use("/hotels", hotelRouter);
|
||||
this._app.use("/transportations", transportationRouter);
|
||||
this._app.use("/packages", packageRouter);
|
||||
this._app.use("/admins", adminRouter);
|
||||
this._app.use("/partners", partnerRouter);
|
||||
this._app.use("/orders", orderRouter);
|
||||
}
|
||||
|
||||
public initializeErrorHandlers() {}
|
||||
|
||||
5
src/common/errors/invalid-jwt.error.ts
Normal file
5
src/common/errors/invalid-jwt.error.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class InvalidJwtError extends Error {
|
||||
public constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
5
src/common/errors/payment.error.ts
Normal file
5
src/common/errors/payment.error.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export class PaymentError extends Error {
|
||||
public constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { orm } from "@/database/orm";
|
||||
import { RequestContext } from "@mikro-orm/core";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
export function ormMiddleware(
|
||||
export function createOrmContextMiddleware(
|
||||
_req: Request,
|
||||
_res: Response,
|
||||
next: NextFunction,
|
||||
51
src/common/middlewares/is-admin.middleware.ts
Normal file
51
src/common/middlewares/is-admin.middleware.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { type AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type { ErrorResponse } from "@/common/types";
|
||||
import type { Admin } from "@/database/entities/admin.entity";
|
||||
import type { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
export type AdminRequestPlugin = {
|
||||
admin: Admin;
|
||||
};
|
||||
|
||||
export function isAdminMiddleware(
|
||||
jwtService: AbstractJwtService,
|
||||
permissions: AdminPermission[] = [],
|
||||
) {
|
||||
return async (_req: Request, res: Response, next: NextFunction) => {
|
||||
const req = _req as Request & AdminRequestPlugin;
|
||||
|
||||
const authorization = req.headers["authorization"];
|
||||
if (!authorization || !authorization.startsWith("Bearer ")) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "Authorization",
|
||||
location: "header",
|
||||
message: "Invalid token.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const token = authorization.slice(7);
|
||||
|
||||
req.admin = await jwtService.verifyAdminToken(token);
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (!req.admin.permissions.includes(permission)) {
|
||||
return res.status(403).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: `You don't have '${permission}' permission.`,
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
34
src/common/middlewares/is-partner.middleware.ts
Normal file
34
src/common/middlewares/is-partner.middleware.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { type AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type { ErrorResponse } from "@/common/types";
|
||||
import type { Partner } from "@/database/entities/partner.entity";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
export type PartnerRequestPlugin = {
|
||||
partner: Partner;
|
||||
};
|
||||
|
||||
export function isPartnerMiddleware(jwtService: AbstractJwtService) {
|
||||
return async (_req: Request, res: Response, next: NextFunction) => {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const authorization = req.headers["authorization"];
|
||||
if (!authorization || !authorization.startsWith("Bearer ")) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "Authorization",
|
||||
location: "header",
|
||||
message: "Invalid token.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const token = authorization.slice(7);
|
||||
|
||||
req.partner = await jwtService.verifyPartnerToken(token);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -45,3 +45,19 @@ export const dateSchema = z
|
||||
}
|
||||
return parsedDate;
|
||||
});
|
||||
|
||||
export const emailSchema = z
|
||||
.email("Must be email string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(254, "Max 254 characters.");
|
||||
|
||||
export const phoneNumberSchema = z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.regex(/^\d+$/, "Must be numeric string.")
|
||||
.max(20, "Max 20 characters.");
|
||||
|
||||
export const passwordSchema = z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(72, "Max 72 characters.");
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export abstract class AbstractEmailService {
|
||||
public abstract sendVerificationEmail(
|
||||
to: string,
|
||||
code: string,
|
||||
): Promise<void>;
|
||||
}
|
||||
30
src/common/services/email-service/library.email-service.ts
Normal file
30
src/common/services/email-service/library.email-service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AbstractEmailService } from "@/common/services/email-service/abstract.email-service";
|
||||
import { mailConfig } from "@/configs/mail.config";
|
||||
import * as nodemailer from "nodemailer";
|
||||
|
||||
export class LibraryEmailService extends AbstractEmailService {
|
||||
private readonly _transporter: nodemailer.Transporter;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this._transporter = nodemailer.createTransport({
|
||||
host: mailConfig.host,
|
||||
port: mailConfig.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: mailConfig.username,
|
||||
pass: mailConfig.password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async sendVerificationEmail(to: string, code: string): Promise<void> {
|
||||
await this._transporter.sendMail({
|
||||
from: mailConfig.username,
|
||||
to,
|
||||
subject: "Email Verification",
|
||||
text: `Your verification code is: ${code}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
29
src/common/services/jwt-service/abstract.jwt-service.ts
Normal file
29
src/common/services/jwt-service/abstract.jwt-service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Admin } from "@/database/entities/admin.entity";
|
||||
import type { Partner } from "@/database/entities/partner.entity";
|
||||
|
||||
export enum JwtType {
|
||||
access = "access",
|
||||
refresh = "refresh",
|
||||
}
|
||||
|
||||
export abstract class AbstractJwtService {
|
||||
public abstract createAdminToken(
|
||||
admin: Admin,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
|
||||
public abstract verifyAdminToken(token: string): Promise<Admin>;
|
||||
|
||||
public abstract createPartnerToken(
|
||||
partner: Partner,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
|
||||
public abstract verifyPartnerToken(token: string): Promise<Partner>;
|
||||
}
|
||||
149
src/common/services/jwt-service/library.jwt-service.ts
Normal file
149
src/common/services/jwt-service/library.jwt-service.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { InvalidJwtError } from "@/common/errors/invalid-jwt.error";
|
||||
import {
|
||||
AbstractJwtService,
|
||||
JwtType,
|
||||
} from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import { jwtConfig } from "@/configs/jwt.config";
|
||||
import { Admin } from "@/database/entities/admin.entity";
|
||||
import { Partner } from "@/database/entities/partner.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import * as dateFns from "date-fns";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { ulid } from "ulid";
|
||||
import z from "zod";
|
||||
|
||||
const adminPayloadSchema = z.object({
|
||||
sub: z.ulid(),
|
||||
role: z.literal("admin"),
|
||||
});
|
||||
|
||||
const partnerPayloadSchema = z.object({
|
||||
sub: z.ulid(),
|
||||
role: z.literal("partner"),
|
||||
});
|
||||
|
||||
export class LibraryJwtService extends AbstractJwtService {
|
||||
public async createAdminToken(
|
||||
admin: Admin,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}> {
|
||||
const now = new Date();
|
||||
|
||||
let expiresAt: Date;
|
||||
switch (type) {
|
||||
case JwtType.access:
|
||||
expiresAt = dateFns.addDays(now, 1);
|
||||
break;
|
||||
case JwtType.refresh:
|
||||
expiresAt = dateFns.addDays(now, 30);
|
||||
break;
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
role: "admin",
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{
|
||||
algorithm: jwtConfig.algorithm,
|
||||
expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
|
||||
notBefore: "0 seconds",
|
||||
subject: admin.id,
|
||||
issuer: jwtConfig.issuer,
|
||||
jwtid: ulid(),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
public async verifyAdminToken(token: string): Promise<Admin> {
|
||||
const payload = jwt.verify(token, jwtConfig.secret, {
|
||||
algorithms: [jwtConfig.algorithm],
|
||||
issuer: jwtConfig.issuer,
|
||||
});
|
||||
|
||||
const parsePayloadResult = adminPayloadSchema.safeParse(payload);
|
||||
if (!parsePayloadResult.success) {
|
||||
throw new InvalidJwtError("Invalid payload.");
|
||||
}
|
||||
const adminPayload = parsePayloadResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(Admin, {
|
||||
id: adminPayload.sub,
|
||||
});
|
||||
if (!admin) {
|
||||
throw new InvalidJwtError("Admin not found.");
|
||||
}
|
||||
|
||||
return admin;
|
||||
}
|
||||
|
||||
public async createPartnerToken(
|
||||
partner: Partner,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}> {
|
||||
const now = new Date();
|
||||
|
||||
let expiresAt: Date;
|
||||
switch (type) {
|
||||
case JwtType.access:
|
||||
expiresAt = dateFns.addDays(now, 1);
|
||||
break;
|
||||
case JwtType.refresh:
|
||||
expiresAt = dateFns.addDays(now, 30);
|
||||
break;
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
role: "partner",
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{
|
||||
algorithm: jwtConfig.algorithm,
|
||||
expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
|
||||
notBefore: "0 seconds",
|
||||
subject: partner.id,
|
||||
issuer: jwtConfig.issuer,
|
||||
jwtid: ulid(),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
public async verifyPartnerToken(token: string): Promise<Partner> {
|
||||
const payload = jwt.verify(token, jwtConfig.secret, {
|
||||
algorithms: [jwtConfig.algorithm],
|
||||
issuer: jwtConfig.issuer,
|
||||
});
|
||||
|
||||
const parsePayloadResult = partnerPayloadSchema.safeParse(payload);
|
||||
if (!parsePayloadResult.success) {
|
||||
throw new InvalidJwtError("Invalid payload.");
|
||||
}
|
||||
const partnerPayload = parsePayloadResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(Partner, {
|
||||
id: partnerPayload.sub,
|
||||
});
|
||||
if (!partner) {
|
||||
throw new InvalidJwtError("Partner not found.");
|
||||
}
|
||||
|
||||
return partner;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Order } from "@/database/entities/order.entity";
|
||||
|
||||
export abstract class AbstractPaymentService {
|
||||
public abstract createPaymentUrl(order: Order): Promise<string>;
|
||||
}
|
||||
141
src/common/services/payment-service/midtrans.payment-service.ts
Normal file
141
src/common/services/payment-service/midtrans.payment-service.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { PaymentError } from "@/common/errors/payment.error";
|
||||
import { AbstractPaymentService } from "@/common/services/payment-service/abstract.payment-service";
|
||||
import { midtransConfig } from "@/configs/midtrans.config";
|
||||
import type { OrderDetail } from "@/database/entities/order-detail.entity";
|
||||
import type { Order } from "@/database/entities/order.entity";
|
||||
import { RoomType } from "@/database/enums/room-type.enum";
|
||||
|
||||
type CreateTransactionResponseSuccess = {
|
||||
token: string;
|
||||
redirect_url: string;
|
||||
};
|
||||
type CreateTransactionResponseFailed = {
|
||||
error_messages: string[];
|
||||
};
|
||||
|
||||
export class MidtransPaymentService extends AbstractPaymentService {
|
||||
private readonly _basicAuth: string;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this._basicAuth = `Basic ${Buffer.from(`${midtransConfig.serverKey}:`).toBase64()}`;
|
||||
}
|
||||
|
||||
private calculateOrderDetailsPrice(orderDetails: OrderDetail[]): number {
|
||||
let price = 0;
|
||||
for (const orderDetail of orderDetails) {
|
||||
switch (orderDetail.roomType) {
|
||||
case RoomType.double:
|
||||
price += orderDetail.order.package.doublePrice;
|
||||
break;
|
||||
case RoomType.triple:
|
||||
price += orderDetail.order.package.triplePrice;
|
||||
break;
|
||||
case RoomType.quad:
|
||||
price += orderDetail.order.package.quadPrice;
|
||||
break;
|
||||
case RoomType.infant:
|
||||
price += orderDetail.order.package.infantPrice ?? 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
public async createPaymentUrl(order: Order): Promise<string> {
|
||||
const doubleOrderDetails = order.details.filter(
|
||||
(orderDetail) => orderDetail.roomType === RoomType.double,
|
||||
);
|
||||
const tripleOrderDetails = order.details.filter(
|
||||
(orderDetail) => orderDetail.roomType === RoomType.double,
|
||||
);
|
||||
const quadOrderDetails = order.details.filter(
|
||||
(orderDetail) => orderDetail.roomType === RoomType.double,
|
||||
);
|
||||
const infantOrderDetails = order.details.filter(
|
||||
(orderDetail) => orderDetail.roomType === RoomType.double,
|
||||
);
|
||||
|
||||
const response = await fetch(`${midtransConfig.baseUrl}/transactions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: this._basicAuth,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transaction_details: {
|
||||
order_id: order.id,
|
||||
gross_amount: this.calculateOrderDetailsPrice(
|
||||
order.details.getItems(),
|
||||
),
|
||||
},
|
||||
item_details: [
|
||||
doubleOrderDetails.length > 0
|
||||
? {
|
||||
id: doubleOrderDetails[0].id,
|
||||
price: order.package.doublePrice,
|
||||
quantity: doubleOrderDetails.length,
|
||||
name: `${order.package.package.name} / Double`,
|
||||
brand: "GoUmrah",
|
||||
category: "Paket",
|
||||
merchant_name: "GoUmrah",
|
||||
}
|
||||
: undefined,
|
||||
tripleOrderDetails.length > 0
|
||||
? {
|
||||
id: tripleOrderDetails[0].id,
|
||||
price: order.package.triplePrice,
|
||||
quantity: tripleOrderDetails.length,
|
||||
name: `${order.package.package.name} / Triple`,
|
||||
brand: "GoUmrah",
|
||||
category: "Paket",
|
||||
merchant_name: "GoUmrah",
|
||||
}
|
||||
: undefined,
|
||||
quadOrderDetails.length > 0
|
||||
? {
|
||||
id: quadOrderDetails[0].id,
|
||||
price: order.package.quadPrice,
|
||||
quantity: quadOrderDetails.length,
|
||||
name: `${order.package.package.name} / Quad`,
|
||||
brand: "GoUmrah",
|
||||
category: "Paket",
|
||||
merchant_name: "GoUmrah",
|
||||
}
|
||||
: undefined,
|
||||
infantOrderDetails.length > 0
|
||||
? {
|
||||
id: infantOrderDetails[0].id,
|
||||
price: order.package.infantPrice,
|
||||
quantity: infantOrderDetails.length,
|
||||
name: `${order.package.package.name} / Infant`,
|
||||
brand: "GoUmrah",
|
||||
category: "Paket",
|
||||
merchant_name: "GoUmrah",
|
||||
}
|
||||
: undefined,
|
||||
],
|
||||
customer_details: {
|
||||
first_name: order.name,
|
||||
phone: order.whatsapp,
|
||||
},
|
||||
credit_card: {
|
||||
secure: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 201) {
|
||||
const responseBody =
|
||||
(await response.json()) as CreateTransactionResponseSuccess;
|
||||
return responseBody.redirect_url;
|
||||
} else {
|
||||
const responseBody =
|
||||
(await response.json()) as CreateTransactionResponseFailed;
|
||||
throw new PaymentError(responseBody.error_messages[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type z from "zod";
|
||||
|
||||
export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
|
||||
|
||||
export type SingleResponse<T> = {
|
||||
export type SingleResponse<T = unknown> = {
|
||||
data: T;
|
||||
errors: null;
|
||||
};
|
||||
|
||||
11
src/common/utils.ts
Normal file
11
src/common/utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function generateRandomCode(length: number): string {
|
||||
const numbers = "0123456789";
|
||||
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex = Math.floor(Math.random() * numbers.length);
|
||||
result += numbers[randomIndex];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -16,5 +16,41 @@ export const _env = z
|
||||
DATABASE_USERNAME: z.string("Must be string.").nonempty("Must not empty."),
|
||||
DATABASE_PASSWORD: z.string("Must be string.").nonempty("Must not empty."),
|
||||
DATABASE_NAME: z.string("Must be string.").nonempty("Must not empty."),
|
||||
|
||||
JWT_SECRET: z.string("Must be string.").nonempty("Must not empty."),
|
||||
JWT_ALGORITHM: z.enum(
|
||||
[
|
||||
"HS256",
|
||||
"HS384",
|
||||
"HS512",
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
],
|
||||
"Invalid value.",
|
||||
),
|
||||
JWT_ISSUER: z.string("Must be string.").nonempty("Must not empty."),
|
||||
|
||||
MIDTRANS_BASE_URL: z.url("Must be valid URL.").nonempty("Must not empty."),
|
||||
MIDTRANS_MERCHANT_ID: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty."),
|
||||
MIDTRANS_SERVER_KEY: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty."),
|
||||
|
||||
MAIL_HOST: z.string("Must be string.").nonempty("Must not empty."),
|
||||
MAIL_PORT: z.coerce
|
||||
.number("Must be number.")
|
||||
.int("Must be integer.")
|
||||
.min(0, "Min 0."),
|
||||
MAIL_USERNAME: z.string("Must be string.").nonempty("Must not empty."),
|
||||
MAIL_PASSWORD: z.string("Must be string.").nonempty("Must not empty."),
|
||||
})
|
||||
.parse(Bun.env);
|
||||
|
||||
7
src/configs/jwt.config.ts
Normal file
7
src/configs/jwt.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { _env } from "@/configs/_env";
|
||||
|
||||
export const jwtConfig = {
|
||||
secret: _env.JWT_SECRET,
|
||||
algorithm: _env.JWT_ALGORITHM,
|
||||
issuer: _env.JWT_ISSUER,
|
||||
} as const;
|
||||
8
src/configs/mail.config.ts
Normal file
8
src/configs/mail.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { _env } from "@/configs/_env";
|
||||
|
||||
export const mailConfig = {
|
||||
host: _env.MAIL_HOST,
|
||||
port: _env.MAIL_PORT,
|
||||
username: _env.MAIL_USERNAME,
|
||||
password: _env.MAIL_PASSWORD,
|
||||
} as const;
|
||||
7
src/configs/midtrans.config.ts
Normal file
7
src/configs/midtrans.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { _env } from "@/configs/_env";
|
||||
|
||||
export const midtransConfig = {
|
||||
baseUrl: _env.MIDTRANS_BASE_URL,
|
||||
merchantId: _env.MIDTRANS_MERCHANT_ID,
|
||||
serverKey: _env.MIDTRANS_SERVER_KEY,
|
||||
} as const;
|
||||
51
src/database/entities/admin.entity.ts
Normal file
51
src/database/entities/admin.entity.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Verification } from "@/database/entities/verification.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import {
|
||||
Entity,
|
||||
Enum,
|
||||
ManyToOne,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
type Rel,
|
||||
} from "@mikro-orm/core";
|
||||
|
||||
@Entity()
|
||||
export class Admin {
|
||||
@PrimaryKey({ type: "varchar", length: 30 })
|
||||
id!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100 })
|
||||
name!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 254, unique: true })
|
||||
email!: string;
|
||||
|
||||
@Property({ type: "text" })
|
||||
password!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100, nullable: true })
|
||||
avatar!: string | null;
|
||||
|
||||
@Enum({
|
||||
items: () => AdminPermission,
|
||||
array: true,
|
||||
nativeEnumName: "admin_permission",
|
||||
})
|
||||
permissions!: AdminPermission[];
|
||||
|
||||
@ManyToOne(() => Verification, { nullable: true })
|
||||
verification!: Rel<Verification> | null;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
})
|
||||
createdAt!: Date;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
})
|
||||
updatedAt!: Date;
|
||||
}
|
||||
@@ -14,13 +14,12 @@ export class Airline {
|
||||
code!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100 })
|
||||
@Unique()
|
||||
logo!: string;
|
||||
|
||||
@Property({ type: "int", unsigned: true })
|
||||
skytraxRating!: number;
|
||||
|
||||
@Enum(() => SkytraxType)
|
||||
@Enum({ items: () => SkytraxType, nativeEnumName: "skytrax_type" })
|
||||
skytraxType!: SkytraxType;
|
||||
|
||||
@Property({
|
||||
|
||||
35
src/database/entities/order-detail.entity.ts
Normal file
35
src/database/entities/order-detail.entity.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Order } from "@/database/entities/order.entity";
|
||||
import { RoomType } from "@/database/enums/room-type.enum";
|
||||
import {
|
||||
Entity,
|
||||
Enum,
|
||||
ManyToOne,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
type Rel,
|
||||
} from "@mikro-orm/core";
|
||||
|
||||
@Entity()
|
||||
export class OrderDetail {
|
||||
@PrimaryKey({ type: "varchar", length: 30 })
|
||||
id!: string;
|
||||
|
||||
@ManyToOne(() => Order)
|
||||
order!: Rel<Order>;
|
||||
|
||||
@Enum({ items: () => RoomType, nativeEnumName: "room_type" })
|
||||
roomType!: RoomType;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
})
|
||||
createdAt!: Date;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
})
|
||||
updatedAt!: Date;
|
||||
}
|
||||
61
src/database/entities/order.entity.ts
Normal file
61
src/database/entities/order.entity.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { OrderDetail } from "@/database/entities/order-detail.entity";
|
||||
import { PackageDetail } from "@/database/entities/package-detail.entity";
|
||||
import { Partner } from "@/database/entities/partner.entity";
|
||||
import { Verification } from "@/database/entities/verification.entity";
|
||||
import {
|
||||
Collection,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
type Rel,
|
||||
} from "@mikro-orm/core";
|
||||
|
||||
@Entity()
|
||||
export class Order {
|
||||
@PrimaryKey({ type: "varchar", length: 30 })
|
||||
id!: string;
|
||||
|
||||
@ManyToOne(() => PackageDetail)
|
||||
package!: Rel<PackageDetail>;
|
||||
|
||||
@Property({ type: "varchar", length: 100 })
|
||||
name!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 20 })
|
||||
whatsapp!: string;
|
||||
|
||||
@ManyToOne(() => Verification, { nullable: true })
|
||||
verification!: Rel<Verification> | null;
|
||||
|
||||
@ManyToOne(() => Partner, { nullable: true })
|
||||
partner!: Rel<Partner | null>;
|
||||
|
||||
@Property({ type: "timestamp", nullable: true })
|
||||
expiredAt!: Date | null;
|
||||
|
||||
@Property({ type: "timestamp", nullable: true })
|
||||
purchasedAt!: Date | null;
|
||||
|
||||
@Property({ type: "timestamp", nullable: true })
|
||||
finishedAt!: Date | null;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
})
|
||||
createdAt!: Date;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
})
|
||||
updatedAt!: Date;
|
||||
|
||||
// Collections
|
||||
|
||||
@OneToMany(() => OrderDetail, (orderDetail) => orderDetail.order)
|
||||
details = new Collection<OrderDetail>(this);
|
||||
}
|
||||
@@ -21,7 +21,10 @@ export abstract class PackageItineraryWidget {
|
||||
@ManyToOne(() => PackageItineraryDay)
|
||||
packageItineraryDay!: Rel<PackageItineraryDay>;
|
||||
|
||||
@Enum(() => PackageItineraryWidgetType)
|
||||
@Enum({
|
||||
type: () => PackageItineraryWidgetType,
|
||||
nativeEnumName: "package_itinerary_widget_type",
|
||||
})
|
||||
type!: PackageItineraryWidgetType;
|
||||
|
||||
@Property({
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
Unique,
|
||||
} from "@mikro-orm/core";
|
||||
|
||||
@Entity()
|
||||
@@ -20,14 +19,13 @@ export class Package {
|
||||
@Property({ type: "varchar", length: 100 })
|
||||
name!: string;
|
||||
|
||||
@Enum(() => PackageType)
|
||||
@Enum({ items: () => PackageType, nativeEnumName: "package_type" })
|
||||
type!: PackageType;
|
||||
|
||||
@Enum(() => PackageClass)
|
||||
@Enum({ items: () => PackageClass, nativeEnumName: "package_class" })
|
||||
class!: PackageClass;
|
||||
|
||||
@Property({ type: "varchar", length: 100 })
|
||||
@Unique()
|
||||
thumbnail!: string;
|
||||
|
||||
@Property({ type: "boolean" })
|
||||
|
||||
53
src/database/entities/partner.entity.ts
Normal file
53
src/database/entities/partner.entity.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Order } from "@/database/entities/order.entity";
|
||||
import { Verification } from "@/database/entities/verification.entity";
|
||||
import {
|
||||
Collection,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryKey,
|
||||
Property,
|
||||
type Rel,
|
||||
} from "@mikro-orm/core";
|
||||
|
||||
@Entity()
|
||||
export class Partner {
|
||||
@PrimaryKey({ type: "varchar", length: 30 })
|
||||
id!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100 })
|
||||
name!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 254, unique: true })
|
||||
email!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 20, unique: true })
|
||||
whatsapp!: string;
|
||||
|
||||
@Property({ type: "text" })
|
||||
password!: string;
|
||||
|
||||
@Property({ type: "varchar", length: 100, nullable: true })
|
||||
avatar!: string | null;
|
||||
|
||||
@ManyToOne(() => Verification, { nullable: true })
|
||||
verification!: Rel<Verification> | null;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
})
|
||||
createdAt!: Date;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
})
|
||||
updatedAt!: Date;
|
||||
|
||||
// Collections
|
||||
|
||||
@OneToMany(() => Order, (order) => order.partner)
|
||||
orders = new Collection<Order>(this);
|
||||
}
|
||||
35
src/database/entities/verification.entity.ts
Normal file
35
src/database/entities/verification.entity.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { VerificationType } from "@/database/enums/verification-type.enum";
|
||||
import { Entity, Enum, PrimaryKey, Property } from "@mikro-orm/core";
|
||||
|
||||
@Entity()
|
||||
export class Verification {
|
||||
@PrimaryKey({ type: "varchar", length: 30 })
|
||||
id!: string;
|
||||
|
||||
@Property({ type: "char", length: 6 })
|
||||
code!: string;
|
||||
|
||||
@Enum({
|
||||
items: () => VerificationType,
|
||||
nativeEnumName: "verification_type",
|
||||
})
|
||||
type!: VerificationType;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
})
|
||||
expiredAt!: Date;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
})
|
||||
createdAt!: Date;
|
||||
|
||||
@Property({
|
||||
type: "timestamp",
|
||||
onCreate: () => new Date(),
|
||||
onUpdate: () => new Date(),
|
||||
})
|
||||
updatedAt!: Date;
|
||||
}
|
||||
58
src/database/enums/admin-permission.enum.ts
Normal file
58
src/database/enums/admin-permission.enum.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
export enum AdminPermission {
|
||||
// Country permissions
|
||||
createCountry = "country:create",
|
||||
updateCountry = "country:update",
|
||||
deleteCountry = "country:delete",
|
||||
// City permissions
|
||||
createCity = "city:create",
|
||||
updateCity = "city:update",
|
||||
deleteCity = "city:delete",
|
||||
// Airline permissions
|
||||
createAirline = "airline:create",
|
||||
updateAirline = "airline:update",
|
||||
deleteAirline = "airline:delete",
|
||||
// Airport permissions
|
||||
createAirport = "airport:create",
|
||||
updateAirport = "airport:update",
|
||||
deleteAirport = "airport:delete",
|
||||
// Flight permissions
|
||||
createFlight = "flight:create",
|
||||
updateFlight = "flight:update",
|
||||
deleteFlight = "flight:delete",
|
||||
// Flight class permissions
|
||||
createFlightClass = "flight-class:create",
|
||||
updateFlightClass = "flight-class:update",
|
||||
deleteFlightClass = "flight-class:delete",
|
||||
// Hotel facility permissions
|
||||
createHotelFacility = "hotel-facility:create",
|
||||
updateHotelFacility = "hotel-facility:update",
|
||||
deleteHotelFacility = "hotel-facility:delete",
|
||||
// Hotel permissions
|
||||
createHotel = "hotel:create",
|
||||
updateHotel = "hotel:update",
|
||||
deleteHotel = "hotel:delete",
|
||||
// Transportation permissions
|
||||
createTransportation = "transportation:create",
|
||||
updateTransportation = "transportation:update",
|
||||
deleteTransportation = "transportation:delete",
|
||||
// Transportation class permissions
|
||||
createTransportationClass = "transportation-class:create",
|
||||
updateTransportationClass = "transportation-class:update",
|
||||
deleteTransportationClass = "transportation-class:delete",
|
||||
// Package permissions
|
||||
createPackage = "package:create",
|
||||
updatePackage = "package:update",
|
||||
deletePackage = "package:delete",
|
||||
// Package detail permissions
|
||||
createPackageDetail = "package-detail:create",
|
||||
updatePackageDetail = "package-detail:update",
|
||||
deletePackageDetail = "package-detail:delete",
|
||||
// Admin permissions
|
||||
createAdmin = "admin:create",
|
||||
updateAdmin = "admin:update",
|
||||
deleteAdmin = "admin:delete",
|
||||
// Partner permissions
|
||||
createPartner = "partner:create",
|
||||
updatePartner = "partner:update",
|
||||
deletePartner = "partner:delete",
|
||||
}
|
||||
6
src/database/enums/room-type.enum.ts
Normal file
6
src/database/enums/room-type.enum.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export enum RoomType {
|
||||
double = "double",
|
||||
triple = "triple",
|
||||
quad = "quad",
|
||||
infant = "infant",
|
||||
}
|
||||
14
src/database/enums/verification-type.enum.ts
Normal file
14
src/database/enums/verification-type.enum.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export enum VerificationType {
|
||||
// Admin verifications
|
||||
createAdmin = "admin:create",
|
||||
changeEmailAdmin = "admin:changeEmail",
|
||||
changePasswordAdmin = "admin:changePassword",
|
||||
updateAdmin = "admin:update",
|
||||
// Partner verifications
|
||||
createPartner = "partner:create",
|
||||
changeEmailPartner = "partner:changeEmail",
|
||||
changePasswordPartner = "partner:changePassword",
|
||||
updatePartner = "partner:update",
|
||||
// Order verifications
|
||||
createOrder = "order:create",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,35 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
import { Migration } from "@mikro-orm/migrations";
|
||||
|
||||
export class Migration20251112105413 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`,
|
||||
);
|
||||
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`);
|
||||
this.addSql(
|
||||
`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "transportation_image" alter column "transportation_id" drop not null;`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on delete cascade;`,
|
||||
);
|
||||
}
|
||||
|
||||
this.addSql(`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`);
|
||||
this.addSql(`alter table "transportation_image" alter column "transportation_id" drop not null;`);
|
||||
this.addSql(`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on delete cascade;`);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`);
|
||||
|
||||
this.addSql(`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`);
|
||||
this.addSql(`alter table "transportation_image" alter column "transportation_id" set not null;`);
|
||||
this.addSql(`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on update cascade;`);
|
||||
}
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "transportation_image" alter column "transportation_id" type varchar(30) using ("transportation_id"::varchar(30));`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "transportation_image" alter column "transportation_id" set not null;`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on update cascade;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
184
src/database/migrations/Migration20251119103455.ts
Normal file
184
src/database/migrations/Migration20251119103455.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { Migration } from "@mikro-orm/migrations";
|
||||
|
||||
export class Migration20251119103455 extends Migration {
|
||||
override async up(): Promise<void> {
|
||||
this.addSql(
|
||||
`create type "skytrax_type" as enum ('full_service', 'low_cost');`,
|
||||
);
|
||||
this.addSql(`create type "package_type" as enum ('reguler', 'plus');`);
|
||||
this.addSql(
|
||||
`create type "package_class" as enum ('silver', 'gold', 'platinum');`,
|
||||
);
|
||||
this.addSql(
|
||||
`create type "package_itinerary_widget_type" as enum ('hotel', 'information', 'transport');`,
|
||||
);
|
||||
this.addSql(
|
||||
`create type "verification_type" as enum ('admin:create', 'admin:changeEmail', 'admin:changePassword', 'admin:update', 'partner:create', 'partner:changeEmail', 'partner:changePassword', 'partner:update', 'order:create');`,
|
||||
);
|
||||
this.addSql(
|
||||
`create type "room_type" as enum ('double', 'triple', 'quad', 'infant');`,
|
||||
);
|
||||
this.addSql(
|
||||
`create type "admin_permission" as enum ('country:create', 'country:update', 'country:delete', 'city:create', 'city:update', 'city:delete', 'airline:create', 'airline:update', 'airline:delete', 'airport:create', 'airport:update', 'airport:delete', 'flight:create', 'flight:update', 'flight:delete', 'flight-class:create', 'flight-class:update', 'flight-class:delete', 'hotel-facility:create', 'hotel-facility:update', 'hotel-facility:delete', 'hotel:create', 'hotel:update', 'hotel:delete', 'transportation:create', 'transportation:update', 'transportation:delete', 'transportation-class:create', 'transportation-class:update', 'transportation-class:delete', 'package:create', 'package:update', 'package:delete', 'package-detail:create', 'package-detail:update', 'package-detail:delete', 'admin:create', 'admin:update', 'admin:delete', 'partner:create', 'partner:update', 'partner:delete');`,
|
||||
);
|
||||
this.addSql(
|
||||
`create table "verification" ("id" varchar(30) not null, "code" char(6) not null, "type" "verification_type" not null, "expired_at" timestamptz not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "verification_pkey" primary key ("id"));`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`create table "partner" ("id" varchar(30) not null, "name" varchar(100) not null, "email" varchar(254) not null, "whatsapp" varchar(20) not null, "password" text not null, "avatar" varchar(100) null, "verification_id" varchar(30) null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "partner_pkey" primary key ("id"));`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "partner" add constraint "partner_email_unique" unique ("email");`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "partner" add constraint "partner_whatsapp_unique" unique ("whatsapp");`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`create table "order" ("id" varchar(30) not null, "package_id" varchar(30) not null, "name" varchar(100) not null, "whatsapp" varchar(20) not null, "verification_id" varchar(30) null, "partner_id" varchar(30) null, "expired_at" timestamptz null, "purchased_at" timestamptz null, "finished_at" timestamptz null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "order_pkey" primary key ("id"));`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`create table "order_detail" ("id" varchar(30) not null, "order_id" varchar(30) not null, "room_type" "room_type" not null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "order_detail_pkey" primary key ("id"));`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`create table "admin" ("id" varchar(30) not null, "name" varchar(100) not null, "email" varchar(254) not null, "password" text not null, "avatar" varchar(100) null, "permissions" "admin_permission"[] not null, "verification_id" varchar(30) null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "admin_pkey" primary key ("id"));`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "admin" add constraint "admin_email_unique" unique ("email");`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "partner" add constraint "partner_verification_id_foreign" foreign key ("verification_id") references "verification" ("id") on update cascade on delete set null;`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "order" add constraint "order_package_id_foreign" foreign key ("package_id") references "package_detail" ("id") on update cascade;`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "order" add constraint "order_verification_id_foreign" foreign key ("verification_id") references "verification" ("id") on update cascade on delete set null;`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "order" add constraint "order_partner_id_foreign" foreign key ("partner_id") references "partner" ("id") on update cascade on delete set null;`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "order_detail" add constraint "order_detail_order_id_foreign" foreign key ("order_id") references "order" ("id") on update cascade;`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "admin" add constraint "admin_verification_id_foreign" foreign key ("verification_id") references "verification" ("id") on update cascade on delete set null;`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "airline" drop constraint if exists "airline_skytrax_type_check";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "package" drop constraint if exists "package_type_check";`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "package" drop constraint if exists "package_class_check";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "package_itinerary_widget" drop constraint if exists "package_itinerary_widget_type_check";`,
|
||||
);
|
||||
|
||||
this.addSql(`alter table "airline" drop constraint "airline_logo_unique";`);
|
||||
|
||||
this.addSql(
|
||||
`alter table "airline" alter column "skytrax_type" type "skytrax_type" using ("skytrax_type"::"skytrax_type");`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "package" drop constraint "package_thumbnail_unique";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "package" alter column "type" type "package_type" using ("type"::"package_type");`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "package" alter column "class" type "package_class" using ("class"::"package_class");`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "package_itinerary_widget" alter column "type" type "package_itinerary_widget_type" using ("type"::"package_itinerary_widget_type");`,
|
||||
);
|
||||
}
|
||||
|
||||
override async down(): Promise<void> {
|
||||
this.addSql(
|
||||
`alter table "partner" drop constraint "partner_verification_id_foreign";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "order" drop constraint "order_verification_id_foreign";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "admin" drop constraint "admin_verification_id_foreign";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "order" drop constraint "order_partner_id_foreign";`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "order_detail" drop constraint "order_detail_order_id_foreign";`,
|
||||
);
|
||||
|
||||
this.addSql(`drop table if exists "verification" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "partner" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "order" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "order_detail" cascade;`);
|
||||
|
||||
this.addSql(`drop table if exists "admin" cascade;`);
|
||||
|
||||
this.addSql(
|
||||
`alter table "airline" alter column "skytrax_type" type text using ("skytrax_type"::text);`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "airline" add constraint "airline_skytrax_type_check" check("skytrax_type" in ('full_service', 'low_cost'));`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "airline" add constraint "airline_logo_unique" unique ("logo");`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "package" alter column "type" type text using ("type"::text);`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "package" alter column "class" type text using ("class"::text);`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "package" add constraint "package_type_check" check("type" in ('reguler', 'plus'));`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "package" add constraint "package_class_check" check("class" in ('silver', 'gold', 'platinum'));`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "package" add constraint "package_thumbnail_unique" unique ("thumbnail");`,
|
||||
);
|
||||
|
||||
this.addSql(
|
||||
`alter table "package_itinerary_widget" alter column "type" type text using ("type"::text);`,
|
||||
);
|
||||
this.addSql(
|
||||
`alter table "package_itinerary_widget" add constraint "package_itinerary_widget_type_check" check("type" in ('transport', 'hotel', 'information'));`,
|
||||
);
|
||||
|
||||
this.addSql(`drop type "skytrax_type";`);
|
||||
this.addSql(`drop type "package_type";`);
|
||||
this.addSql(`drop type "package_class";`);
|
||||
this.addSql(`drop type "package_itinerary_widget_type";`);
|
||||
this.addSql(`drop type "verification_type";`);
|
||||
this.addSql(`drop type "room_type";`);
|
||||
this.addSql(`drop type "admin_permission";`);
|
||||
}
|
||||
}
|
||||
627
src/modules/admin/admin.controller.ts
Normal file
627
src/modules/admin/admin.controller.ts
Normal file
@@ -0,0 +1,627 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import {
|
||||
isAdminMiddleware,
|
||||
type AdminRequestPlugin,
|
||||
} from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractEmailService } from "@/common/services/email-service/abstract.email-service";
|
||||
import type {
|
||||
AbstractFileStorage,
|
||||
FileResult,
|
||||
} from "@/common/services/file-storage/abstract.file-storage";
|
||||
import {
|
||||
JwtType,
|
||||
type AbstractJwtService,
|
||||
} from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { generateRandomCode } from "@/common/utils";
|
||||
import { Admin } from "@/database/entities/admin.entity";
|
||||
import { Verification } from "@/database/entities/verification.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { VerificationType } from "@/database/enums/verification-type.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { AdminMapper } from "@/modules/admin/admin.mapper";
|
||||
import {
|
||||
adminChangeEmailRequestSchema,
|
||||
adminChangePasswordRequestSchema,
|
||||
adminParamsSchema,
|
||||
adminRequestSchema,
|
||||
adminUpdateRequestSchema,
|
||||
adminVerifyRequestSchema,
|
||||
} from "@/modules/admin/admin.schemas";
|
||||
import type { AdminResponse } from "@/modules/admin/admin.types";
|
||||
import * as dateFns from "date-fns";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class AdminController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: AdminMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly emailService: AbstractEmailService,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = adminRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
let avatarFile: null | FileResult = null;
|
||||
if (body.avatar !== null) {
|
||||
avatarFile = await this.fileStorage.storeFile(
|
||||
Buffer.from(body.avatar, "base64"),
|
||||
);
|
||||
}
|
||||
|
||||
const verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.createAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const admin = orm.em.create(Admin, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
password: await Bun.password.hash(body.password),
|
||||
avatar: avatarFile?.name,
|
||||
permissions: body.permissions,
|
||||
verification,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
verification.code,
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin created successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async login(req: Request, res: Response) {
|
||||
const parseBodyResult = adminRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(Admin, { email: body.email });
|
||||
if (!admin) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
location: "body",
|
||||
message: "Incorrect email or password.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
if (!(await Bun.password.verify(body.password, admin.password))) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
location: "body",
|
||||
message: "Incorrect email or password.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
if (admin.verification !== null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Admin is not verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const access = await this.jwtService.createAdminToken(
|
||||
admin,
|
||||
JwtType.access,
|
||||
);
|
||||
const refresh = await this.jwtService.createAdminToken(
|
||||
admin,
|
||||
JwtType.refresh,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
access_token: access.token,
|
||||
access_token_expires_at: access.expiresAt,
|
||||
refresh_token: refresh.token,
|
||||
refresh_token_expires_at: refresh.expiresAt,
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async list(req: Request, res: Response) {
|
||||
const parseQueryResult = paginationQuerySchema.safeParse(req.query);
|
||||
if (!parseQueryResult.success) {
|
||||
return this.handleZodError(parseQueryResult.error, res, "query");
|
||||
}
|
||||
const query = parseQueryResult.data;
|
||||
|
||||
const count = await orm.em.count(Admin);
|
||||
|
||||
const admins = await orm.em.find(
|
||||
Admin,
|
||||
{
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: admins.map(this.mapper.mapEntityToResponse.bind(this.mapper)),
|
||||
errors: null,
|
||||
meta: {
|
||||
page: query.page,
|
||||
per_page: query.per_page,
|
||||
total_pages: Math.ceil(count / query.per_page),
|
||||
total_items: count,
|
||||
},
|
||||
} satisfies ListResponse<AdminResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(admin),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AdminResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminUpdateRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (body.avatar !== null) {
|
||||
await this.fileStorage.storeFile(
|
||||
Buffer.from(body.avatar, "base64"),
|
||||
admin.avatar ?? undefined,
|
||||
);
|
||||
} else if (admin.avatar !== null) {
|
||||
await this.fileStorage.removeFile(admin.avatar);
|
||||
}
|
||||
|
||||
admin.name = body.name;
|
||||
admin.permissions = body.permissions;
|
||||
admin.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.updateAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
admin.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin updated successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async changeEmail(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminChangeEmailRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
admin.email = body.new_email;
|
||||
admin.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.changeEmailAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
admin.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin's email changed successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async changePassword(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminChangePasswordRequestSchema.safeParse(
|
||||
req.body,
|
||||
);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (!(await Bun.password.verify(body.old_password, admin.password))) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "old_password",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
admin.password = await Bun.password.hash(body.new_password);
|
||||
admin.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.changePasswordAdmin,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
admin.email,
|
||||
admin.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Admin's password changed successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async verify(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = adminVerifyRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (admin.verification === null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Admin is already verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (admin.verification.code !== body.code) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "code",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
orm.em.remove(admin.verification);
|
||||
|
||||
admin.verification = null;
|
||||
admin.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(admin),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<AdminResponse>);
|
||||
}
|
||||
|
||||
async refresh(_req: Request, res: Response) {
|
||||
const req = _req as Request & AdminRequestPlugin;
|
||||
|
||||
const access = await this.jwtService.createAdminToken(
|
||||
req.admin,
|
||||
JwtType.access,
|
||||
);
|
||||
const refresh = await this.jwtService.createAdminToken(
|
||||
req.admin,
|
||||
JwtType.refresh,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
access_token: access.token,
|
||||
access_token_expires_at: access.expiresAt,
|
||||
refresh_token: refresh.token,
|
||||
refresh_token_expires_at: refresh.expiresAt,
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = adminParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(
|
||||
Admin,
|
||||
{ id: params.id, verification: null },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!admin) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Admin not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (admin.avatar !== null) {
|
||||
await this.fileStorage.removeFile(admin.avatar);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(admin);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createAdmin]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.post("/login", createOrmContextMiddleware, this.login.bind(this));
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAdmin]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/email",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAdmin]),
|
||||
this.changeEmail.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/password",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAdmin]),
|
||||
this.changePassword.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/verify",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAdmin]),
|
||||
this.verify.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/refresh",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService),
|
||||
this.refresh.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteAdmin]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
18
src/modules/admin/admin.mapper.ts
Normal file
18
src/modules/admin/admin.mapper.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Admin } from "@/database/entities/admin.entity";
|
||||
import type { AdminResponse } from "@/modules/admin/admin.types";
|
||||
|
||||
export class AdminMapper {
|
||||
public constructor() {}
|
||||
|
||||
public mapEntityToResponse(admin: Admin): AdminResponse {
|
||||
return {
|
||||
id: admin.id,
|
||||
name: admin.name,
|
||||
email: admin.email,
|
||||
avatar: admin.avatar,
|
||||
permissions: admin.permissions,
|
||||
created_at: admin.createdAt,
|
||||
updated_at: admin.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
56
src/modules/admin/admin.schemas.ts
Normal file
56
src/modules/admin/admin.schemas.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { emailSchema, passwordSchema } from "@/common/schemas";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const adminRequestSchema = z.object({
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
avatar: z
|
||||
.base64("Must be base64 string.")
|
||||
.nonempty("Must not empty.")
|
||||
.nullable(),
|
||||
permissions: z
|
||||
.array(
|
||||
z.enum(AdminPermission, "Must be valid permission."),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty."),
|
||||
});
|
||||
|
||||
export const adminLoginRequestSchema = adminRequestSchema.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
export const adminUpdateRequestSchema = adminRequestSchema.pick({
|
||||
name: true,
|
||||
avatar: true,
|
||||
permissions: true,
|
||||
});
|
||||
|
||||
export const adminChangeEmailRequestSchema = z.object({
|
||||
new_email: emailSchema,
|
||||
});
|
||||
|
||||
export const adminChangePasswordRequestSchema = z.object({
|
||||
old_password: passwordSchema,
|
||||
new_password: passwordSchema,
|
||||
});
|
||||
|
||||
export const adminVerifyRequestSchema = z.object({
|
||||
code: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.length(6, "Must be 6 characters."),
|
||||
});
|
||||
|
||||
export const adminParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
});
|
||||
33
src/modules/admin/admin.types.ts
Normal file
33
src/modules/admin/admin.types.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import type {
|
||||
adminChangeEmailRequestSchema,
|
||||
adminChangePasswordRequestSchema,
|
||||
adminParamsSchema,
|
||||
adminRequestSchema,
|
||||
adminUpdateRequestSchema,
|
||||
} from "@/modules/admin/admin.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type AdminRequest = z.infer<typeof adminRequestSchema>;
|
||||
|
||||
export type AdminUpdateRequest = z.infer<typeof adminUpdateRequestSchema>;
|
||||
|
||||
export type AdminChangeEmailRequest = z.infer<
|
||||
typeof adminChangeEmailRequestSchema
|
||||
>;
|
||||
|
||||
export type AdminChangePasswordRequest = z.infer<
|
||||
typeof adminChangePasswordRequestSchema
|
||||
>;
|
||||
|
||||
export type AdminParams = z.infer<typeof adminParamsSchema>;
|
||||
|
||||
export type AdminResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
permissions: AdminPermission[];
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Airline } from "@/database/entities/airline.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { AirlineMapper } from "@/modules/airline/airline.mapper";
|
||||
import {
|
||||
@@ -22,6 +25,7 @@ export class AirlineController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: AirlineMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -43,7 +47,7 @@ export class AirlineController extends Controller {
|
||||
code: body.code,
|
||||
logo: logoFile.name,
|
||||
skytraxRating: body.skytrax_rating,
|
||||
skytraxType: this.mapper.mapSkytraxType(body.skytrax_type),
|
||||
skytraxType: body.skytrax_type,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
@@ -149,7 +153,7 @@ export class AirlineController extends Controller {
|
||||
airline.name = body.name;
|
||||
airline.code = body.code;
|
||||
airline.skytraxRating = body.skytrax_rating;
|
||||
airline.skytraxType = this.mapper.mapSkytraxType(body.skytrax_type);
|
||||
airline.skytraxType = body.skytrax_type;
|
||||
airline.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
@@ -196,11 +200,26 @@ export class AirlineController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createAirline]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAirline]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteAirline]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import type { Airline } from "@/database/entities/airline.entity";
|
||||
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||
import type {
|
||||
AirlineRequest,
|
||||
AirlineResponse,
|
||||
} from "@/modules/airline/airline.types";
|
||||
import type { AirlineResponse } from "@/modules/airline/airline.types";
|
||||
|
||||
export class AirlineMapper {
|
||||
public constructor() {}
|
||||
|
||||
public mapSkytraxType(
|
||||
skytraxType: AirlineRequest["skytrax_type"],
|
||||
): SkytraxType {
|
||||
switch (skytraxType) {
|
||||
case "full_service":
|
||||
return SkytraxType.fullService;
|
||||
case "low_cost":
|
||||
return SkytraxType.lowCost;
|
||||
}
|
||||
}
|
||||
|
||||
public mapEntityToResponse(airline: Airline): AirlineResponse {
|
||||
return {
|
||||
id: airline.id,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const airlineRequestSchema = z.object({
|
||||
@@ -16,7 +17,7 @@ export const airlineRequestSchema = z.object({
|
||||
.min(1, "Minimum 1.")
|
||||
.max(5, "Maximum 5."),
|
||||
skytrax_type: z.enum(
|
||||
["full_service", "low_cost"],
|
||||
SkytraxType,
|
||||
"Must be either 'full_service' or 'low_cost'.",
|
||||
),
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { SkytraxType } from "@/database/enums/skytrax-type.enum";
|
||||
import type {
|
||||
airlineParamsSchema,
|
||||
airlineRequestSchema,
|
||||
@@ -14,7 +15,7 @@ export type AirlineResponse = {
|
||||
code: string;
|
||||
logo: string;
|
||||
skytrax_rating: number;
|
||||
skytrax_type: "full_service" | "low_cost";
|
||||
skytrax_type: SkytraxType;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
} from "@/common/types";
|
||||
import { Airport } from "@/database/entities/airport.entity";
|
||||
import { City } from "@/database/entities/city.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { AirportMapper } from "@/modules/airport/airport.mapper";
|
||||
import {
|
||||
@@ -19,7 +22,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class AirportController extends Controller {
|
||||
public constructor(private readonly mapper: AirportMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: AirportMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -224,11 +230,26 @@ export class AirportController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createAirport]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateAirport]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteAirport]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
} from "@/common/types";
|
||||
import { City } from "@/database/entities/city.entity";
|
||||
import { Country } from "@/database/entities/country.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { CityMapper } from "@/modules/city/city.mapper";
|
||||
import {
|
||||
@@ -19,7 +22,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class CityController extends Controller {
|
||||
public constructor(private readonly mapper: CityMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: CityMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -210,11 +216,26 @@ export class CityController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createCity]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateCity]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteCity]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { Country } from "@/database/entities/country.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { CountryMapper } from "@/modules/country/country.mapper";
|
||||
import {
|
||||
@@ -18,7 +21,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class CountryController extends Controller {
|
||||
public constructor(private readonly mapper: CountryMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: CountryMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -174,11 +180,26 @@ export class CountryController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createCountry]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateCountry]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteCountry]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -10,6 +12,7 @@ import { Airline } from "@/database/entities/airline.entity";
|
||||
import { Airport } from "@/database/entities/airport.entity";
|
||||
import { FlightClass } from "@/database/entities/flight-class.entity";
|
||||
import { Flight } from "@/database/entities/flight.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { FlightMapper } from "@/modules/flight/flight.mapper";
|
||||
import {
|
||||
@@ -26,7 +29,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class FlightController extends Controller {
|
||||
public constructor(private readonly mapper: FlightMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: FlightMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -622,26 +628,52 @@ export class FlightController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this));
|
||||
router.get("/:id/classes", ormMiddleware, this.listClasses.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createFlight]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateFlight]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteFlight]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
router.post(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createFlightClass]),
|
||||
this.createClass.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
this.listClasses.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
this.viewClass.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateFlightClass]),
|
||||
this.updateClass.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:flight_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteFlightClass]),
|
||||
this.deleteClass.bind(this),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
|
||||
import {
|
||||
@@ -18,7 +21,10 @@ import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class HotelFacilityController extends Controller {
|
||||
public constructor(private readonly mapper: HotelFacilityMapper) {
|
||||
public constructor(
|
||||
private readonly mapper: HotelFacilityMapper,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -184,11 +190,26 @@ export class HotelFacilityController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createHotelFacility]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateHotelFacility]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteHotelFacility]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -11,6 +13,7 @@ import { City } from "@/database/entities/city.entity";
|
||||
import { HotelFacility } from "@/database/entities/hotel-facility.entity";
|
||||
import { HotelImage } from "@/database/entities/hotel-image.entity";
|
||||
import { Hotel } from "@/database/entities/hotel.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||
import {
|
||||
@@ -25,6 +28,7 @@ export class HotelController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: HotelMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -332,11 +336,26 @@ export class HotelController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createHotel]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updateHotel]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deleteHotel]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
389
src/modules/order/order.controller.ts
Normal file
389
src/modules/order/order.controller.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import {
|
||||
isPartnerMiddleware,
|
||||
type PartnerRequestPlugin,
|
||||
} from "@/common/middlewares/is-partner.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type { AbstractPaymentService } from "@/common/services/payment-service/abstract.payment-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { generateRandomCode } from "@/common/utils";
|
||||
import { OrderDetail } from "@/database/entities/order-detail.entity";
|
||||
import { Order } from "@/database/entities/order.entity";
|
||||
import { PackageDetail } from "@/database/entities/package-detail.entity";
|
||||
import { Partner } from "@/database/entities/partner.entity";
|
||||
import { Verification } from "@/database/entities/verification.entity";
|
||||
import { VerificationType } from "@/database/enums/verification-type.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { OrderMapper } from "@/modules/order/order.mapper";
|
||||
import {
|
||||
orderParamsSchema,
|
||||
orderRequestSchema,
|
||||
orderVerifyRequestSchema,
|
||||
} from "@/modules/order/order.schemas";
|
||||
import type { OrderResponse } from "@/modules/order/order.types";
|
||||
import * as dateFns from "date-fns";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class OrderController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: OrderMapper,
|
||||
private readonly paymentService: AbstractPaymentService,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = orderRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const packageDetail = await orm.em.findOne(PackageDetail, {
|
||||
id: body.package_id,
|
||||
});
|
||||
if (!packageDetail) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "package_id",
|
||||
location: "body",
|
||||
message: "Package detail not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.createOrder,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const order = orm.em.create(Order, {
|
||||
id: ulid(),
|
||||
package: packageDetail,
|
||||
name: body.name,
|
||||
whatsapp: body.whatsapp,
|
||||
verification,
|
||||
partner: null,
|
||||
expiredAt: null,
|
||||
purchasedAt: null,
|
||||
finishedAt: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
for (const roomType of body.room_types) {
|
||||
order.details.add(
|
||||
orm.em.create(OrderDetail, {
|
||||
id: ulid(),
|
||||
order,
|
||||
roomType,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(201).json({
|
||||
data: {
|
||||
message:
|
||||
"Order created successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async list(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const parseQueryResult = paginationQuerySchema.safeParse(req.query);
|
||||
if (!parseQueryResult.success) {
|
||||
return this.handleZodError(parseQueryResult.error, res, "query");
|
||||
}
|
||||
const query = parseQueryResult.data;
|
||||
|
||||
const count = await orm.em.count(Order);
|
||||
|
||||
const orders = await orm.em.find(
|
||||
Order,
|
||||
{
|
||||
verification: null,
|
||||
partner: req.partner,
|
||||
},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: orders.map(this.mapper.mapEntityToResponse.bind(this.mapper)),
|
||||
errors: null,
|
||||
meta: {
|
||||
page: query.page,
|
||||
per_page: query.per_page,
|
||||
total_pages: Math.ceil(count / query.per_page),
|
||||
total_items: count,
|
||||
},
|
||||
} satisfies ListResponse<OrderResponse>);
|
||||
}
|
||||
|
||||
async view(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
partner: req.partner,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(order),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<OrderResponse>);
|
||||
}
|
||||
|
||||
async finish(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
partner: req.partner,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
order.finishedAt = new Date();
|
||||
order.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(order),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<OrderResponse>);
|
||||
}
|
||||
|
||||
async verify(req: Request, res: Response) {
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = orderVerifyRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (order.verification === null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Order is already verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (order.verification.code !== body.code) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "code",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
orm.em.remove(order.verification);
|
||||
|
||||
const partners = await orm.em.findAll(Partner, { populate: ["*"] });
|
||||
const partner = partners.toSorted(
|
||||
(a, b) =>
|
||||
a.orders.filter((order) => order.finishedAt === null).length -
|
||||
b.orders.filter((order) => order.finishedAt === null).length,
|
||||
)[0];
|
||||
|
||||
order.verification = null;
|
||||
order.partner = partner;
|
||||
order.expiredAt = dateFns.addHours(new Date(), 24);
|
||||
order.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
const paymentUrl = await this.paymentService.createPaymentUrl(order);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
...this.mapper.mapEntityToResponse(order),
|
||||
payment_url: paymentUrl,
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse<
|
||||
OrderResponse & {
|
||||
payment_url: string;
|
||||
}
|
||||
>);
|
||||
}
|
||||
|
||||
async delete(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const parseParamsResult = orderParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const order = await orm.em.findOne(
|
||||
Order,
|
||||
{ id: params.id, verification: null, partner: req.partner },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!order) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Order not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(order);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", createOrmContextMiddleware, this.create.bind(this));
|
||||
router.get(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.list.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.view.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/finish",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.finish.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/verify",
|
||||
createOrmContextMiddleware,
|
||||
this.verify.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
59
src/modules/order/order.mapper.ts
Normal file
59
src/modules/order/order.mapper.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Order } from "@/database/entities/order.entity";
|
||||
import { RoomType } from "@/database/enums/room-type.enum";
|
||||
import type { OrderResponse } from "@/modules/order/order.types";
|
||||
import type { PackageMapper } from "@/modules/package/package.mapper";
|
||||
import type { PartnerMapper } from "@/modules/partner/partner.mapper";
|
||||
|
||||
export class OrderMapper {
|
||||
public constructor(
|
||||
private readonly packageMapper: PackageMapper,
|
||||
private readonly partnerMapper: PartnerMapper,
|
||||
) {}
|
||||
|
||||
public mapEntityToResponse(order: Order): OrderResponse {
|
||||
const details: OrderResponse["details"] = [];
|
||||
let totalPrice = 0;
|
||||
for (const detail of order.details) {
|
||||
let price = 0;
|
||||
switch (detail.roomType) {
|
||||
case RoomType.double:
|
||||
price = order.package.doublePrice;
|
||||
break;
|
||||
case RoomType.triple:
|
||||
price = order.package.triplePrice;
|
||||
break;
|
||||
case RoomType.quad:
|
||||
price = order.package.quadPrice;
|
||||
break;
|
||||
case RoomType.infant:
|
||||
price = order.package.infantPrice ?? 0;
|
||||
break;
|
||||
}
|
||||
|
||||
details.push({
|
||||
price,
|
||||
room_type: detail.roomType,
|
||||
});
|
||||
|
||||
totalPrice += price;
|
||||
}
|
||||
|
||||
return {
|
||||
id: order.id,
|
||||
package: this.packageMapper.mapDetailEntityToResponse(order.package),
|
||||
name: order.name,
|
||||
whatsapp: order.whatsapp,
|
||||
details,
|
||||
total_price: totalPrice,
|
||||
is_verified: order.verification === null,
|
||||
partner: order.partner
|
||||
? this.partnerMapper.mapEntityToResponse(order.partner)
|
||||
: null,
|
||||
expired_at: order.expiredAt,
|
||||
purchased_at: order.purchasedAt,
|
||||
finished_at: order.finishedAt,
|
||||
created_at: order.createdAt,
|
||||
updated_at: order.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
38
src/modules/order/order.schemas.ts
Normal file
38
src/modules/order/order.schemas.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { phoneNumberSchema } from "@/common/schemas";
|
||||
import { RoomType } from "@/database/enums/room-type.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const orderRequestSchema = z.object({
|
||||
package_id: z
|
||||
.ulid("Must be ulid string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(30, "Max 30 characters."),
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
whatsapp: phoneNumberSchema,
|
||||
room_types: z
|
||||
.array(
|
||||
z.enum(
|
||||
RoomType,
|
||||
"Must be either 'double', 'triple', 'quad', or 'infant'.",
|
||||
),
|
||||
"Must be array.",
|
||||
)
|
||||
.nonempty("Must not empty."),
|
||||
});
|
||||
|
||||
export const orderVerifyRequestSchema = z.object({
|
||||
code: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.length(6, "Must be 6 characters."),
|
||||
});
|
||||
|
||||
export const orderParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
});
|
||||
31
src/modules/order/order.types.ts
Normal file
31
src/modules/order/order.types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { RoomType } from "@/database/enums/room-type.enum";
|
||||
import type {
|
||||
orderParamsSchema,
|
||||
orderRequestSchema,
|
||||
} from "@/modules/order/order.schemas";
|
||||
import type { PackageDetailResponse } from "@/modules/package/package.types";
|
||||
import type { PartnerResponse } from "@/modules/partner/partner.types";
|
||||
import z from "zod";
|
||||
|
||||
export type OrderRequest = z.infer<typeof orderRequestSchema>;
|
||||
|
||||
export type OrderParams = z.infer<typeof orderParamsSchema>;
|
||||
|
||||
export type OrderResponse = {
|
||||
id: string;
|
||||
package: PackageDetailResponse;
|
||||
name: string;
|
||||
whatsapp: string;
|
||||
details: {
|
||||
room_type: RoomType;
|
||||
price: number;
|
||||
}[];
|
||||
total_price: number;
|
||||
is_verified: boolean;
|
||||
partner: PartnerResponse | null;
|
||||
expired_at: Date | null;
|
||||
purchased_at: Date | null;
|
||||
finished_at: Date | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
import { PackageItinerary } from "@/database/entities/package-itinerary.entity";
|
||||
import { Package } from "@/database/entities/package.entity";
|
||||
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { PackageMapper } from "@/modules/package/package.mapper";
|
||||
@@ -43,6 +46,7 @@ export class PackageController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: PackageMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -61,8 +65,8 @@ export class PackageController extends Controller {
|
||||
const package_ = orm.em.create(Package, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
type: this.mapper.mapPackageType(body.type),
|
||||
class: this.mapper.mapPackageClass(body.class),
|
||||
type: body.type,
|
||||
class: body.class,
|
||||
thumbnail: thumbnailFile.name,
|
||||
useFastTrain: body.use_fast_train,
|
||||
createdAt: new Date(),
|
||||
@@ -177,8 +181,8 @@ export class PackageController extends Controller {
|
||||
);
|
||||
|
||||
package_.name = body.name;
|
||||
package_.type = this.mapper.mapPackageType(body.type);
|
||||
package_.class = this.mapper.mapPackageClass(body.class);
|
||||
package_.type = body.type;
|
||||
package_.class = body.class;
|
||||
package_.useFastTrain = body.use_fast_train;
|
||||
package_.updatedAt = new Date();
|
||||
|
||||
@@ -1330,26 +1334,52 @@ export class PackageController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/details", ormMiddleware, this.createDetail.bind(this));
|
||||
router.get("/:id/details", ormMiddleware, this.listDetails.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createPackage]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updatePackage]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deletePackage]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
router.post(
|
||||
"/:id/details",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createPackageDetail]),
|
||||
this.createDetail.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id/details",
|
||||
createOrmContextMiddleware,
|
||||
this.listDetails.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:package_id/details/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
this.viewDetail.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:package_id/details/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updatePackageDetail]),
|
||||
this.updateDetail.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:package_id/details/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deletePackageDetail]),
|
||||
this.deleteDetail.bind(this),
|
||||
);
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@ import type {
|
||||
} from "@/database/entities/package-itinerary-widget.entity";
|
||||
import type { PackageItinerary } from "@/database/entities/package-itinerary.entity";
|
||||
import type { Package } from "@/database/entities/package.entity";
|
||||
import { PackageClass } from "@/database/enums/package-class.enum";
|
||||
import { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
|
||||
import { PackageType } from "@/database/enums/package-type.enum";
|
||||
import type { FlightMapper } from "@/modules/flight/flight.mapper";
|
||||
import type { FlightClassResponse } from "@/modules/flight/flight.types";
|
||||
import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
|
||||
@@ -22,7 +20,6 @@ import type {
|
||||
PackageItineraryDayResponse,
|
||||
PackageItineraryResponse,
|
||||
PackageItineraryWidgetResponse,
|
||||
PackageRequest,
|
||||
PackageResponse,
|
||||
} from "@/modules/package/package.types";
|
||||
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||
@@ -35,26 +32,6 @@ export class PackageMapper {
|
||||
private readonly transportationMapper: TransportationMapper,
|
||||
) {}
|
||||
|
||||
public mapPackageType(packageType: PackageRequest["type"]): PackageType {
|
||||
switch (packageType) {
|
||||
case "reguler":
|
||||
return PackageType.reguler;
|
||||
case "plus":
|
||||
return PackageType.plus;
|
||||
}
|
||||
}
|
||||
|
||||
public mapPackageClass(packageClass: PackageRequest["class"]): PackageClass {
|
||||
switch (packageClass) {
|
||||
case "silver":
|
||||
return PackageClass.silver;
|
||||
case "gold":
|
||||
return PackageClass.gold;
|
||||
case "platinum":
|
||||
return PackageClass.platinum;
|
||||
}
|
||||
}
|
||||
|
||||
public mapEntityToResponse(package_: Package): PackageResponse {
|
||||
return {
|
||||
id: package_.id,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { dateSchema, timeSchema } from "@/common/schemas";
|
||||
import { PackageClass } from "@/database/enums/package-class.enum";
|
||||
import { PackageType } from "@/database/enums/package-type.enum";
|
||||
import z from "zod";
|
||||
|
||||
export const packageRequestSchema = z.object({
|
||||
@@ -6,9 +8,9 @@ export const packageRequestSchema = z.object({
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
type: z.enum(["reguler", "plus"], "Must be either 'reguler' or 'plus'."),
|
||||
type: z.enum(PackageType, "Must be either 'reguler' or 'plus'."),
|
||||
class: z.enum(
|
||||
["silver", "gold", "platinum"],
|
||||
PackageClass,
|
||||
"Must be either 'silver', 'gold', or 'platinum'.",
|
||||
),
|
||||
thumbnail: z.base64("Must be base64 string.").nonempty("Must not empty."),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { PackageClass } from "@/database/enums/package-class.enum";
|
||||
import type { PackageType } from "@/database/enums/package-type.enum";
|
||||
import type { FlightClassResponse } from "@/modules/flight/flight.types";
|
||||
import type { HotelResponse } from "@/modules/hotel/hotel.types";
|
||||
import type {
|
||||
@@ -20,8 +22,8 @@ export type PackageDetailParams = z.infer<typeof packageDetailParamsSchema>;
|
||||
export type PackageResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "reguler" | "plus";
|
||||
class: "silver" | "gold" | "platinum";
|
||||
type: PackageType;
|
||||
class: PackageClass;
|
||||
thumbnail: string;
|
||||
use_fast_train: boolean;
|
||||
created_at: Date;
|
||||
|
||||
627
src/modules/partner/partner.controller.ts
Normal file
627
src/modules/partner/partner.controller.ts
Normal file
@@ -0,0 +1,627 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import {
|
||||
isPartnerMiddleware,
|
||||
type PartnerRequestPlugin,
|
||||
} from "@/common/middlewares/is-partner.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractEmailService } from "@/common/services/email-service/abstract.email-service";
|
||||
import type {
|
||||
AbstractFileStorage,
|
||||
FileResult,
|
||||
} from "@/common/services/file-storage/abstract.file-storage";
|
||||
import {
|
||||
JwtType,
|
||||
type AbstractJwtService,
|
||||
} from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
SingleResponse,
|
||||
} from "@/common/types";
|
||||
import { generateRandomCode } from "@/common/utils";
|
||||
import { Partner } from "@/database/entities/partner.entity";
|
||||
import { Verification } from "@/database/entities/verification.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { VerificationType } from "@/database/enums/verification-type.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { PartnerMapper } from "@/modules/partner/partner.mapper";
|
||||
import {
|
||||
partnerChangeEmailRequestSchema,
|
||||
partnerChangePasswordRequestSchema,
|
||||
partnerParamsSchema,
|
||||
partnerRequestSchema,
|
||||
partnerUpdateRequestSchema,
|
||||
partnerVerifyRequestSchema,
|
||||
} from "@/modules/partner/partner.schemas";
|
||||
import type { PartnerResponse } from "@/modules/partner/partner.types";
|
||||
import * as dateFns from "date-fns";
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
export class PartnerController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: PartnerMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly emailService: AbstractEmailService,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async create(req: Request, res: Response) {
|
||||
const parseBodyResult = partnerRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
let avatarFile: null | FileResult = null;
|
||||
if (body.avatar !== null) {
|
||||
avatarFile = await this.fileStorage.storeFile(
|
||||
Buffer.from(body.avatar, "base64"),
|
||||
);
|
||||
}
|
||||
|
||||
const verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.createPartner,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const partner = orm.em.create(Partner, {
|
||||
id: ulid(),
|
||||
name: body.name,
|
||||
email: body.email,
|
||||
whatsapp: body.whatsapp,
|
||||
password: await Bun.password.hash(body.password),
|
||||
avatar: avatarFile?.name,
|
||||
verification,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
partner.email,
|
||||
verification.code,
|
||||
);
|
||||
|
||||
return res.status(201).json({
|
||||
data: {
|
||||
message:
|
||||
"Partner created successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async login(req: Request, res: Response) {
|
||||
const parseBodyResult = partnerRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(Partner, { email: body.email });
|
||||
if (!partner) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
location: "body",
|
||||
message: "Incorrect email or password.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
if (!(await Bun.password.verify(body.password, partner.password))) {
|
||||
return res.status(401).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
location: "body",
|
||||
message: "Incorrect email or password.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
if (partner.verification !== null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Partner is not verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
const access = await this.jwtService.createPartnerToken(
|
||||
partner,
|
||||
JwtType.access,
|
||||
);
|
||||
const refresh = await this.jwtService.createPartnerToken(
|
||||
partner,
|
||||
JwtType.refresh,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
access_token: access.token,
|
||||
access_token_expires_at: access.expiresAt,
|
||||
refresh_token: refresh.token,
|
||||
refresh_token_expires_at: refresh.expiresAt,
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async list(req: Request, res: Response) {
|
||||
const parseQueryResult = paginationQuerySchema.safeParse(req.query);
|
||||
if (!parseQueryResult.success) {
|
||||
return this.handleZodError(parseQueryResult.error, res, "query");
|
||||
}
|
||||
const query = parseQueryResult.data;
|
||||
|
||||
const count = await orm.em.count(Partner);
|
||||
|
||||
const partners = await orm.em.find(
|
||||
Partner,
|
||||
{
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
limit: query.per_page,
|
||||
offset: (query.page - 1) * query.per_page,
|
||||
orderBy: { createdAt: "DESC" },
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: partners.map(this.mapper.mapEntityToResponse.bind(this.mapper)),
|
||||
errors: null,
|
||||
meta: {
|
||||
page: query.page,
|
||||
per_page: query.per_page,
|
||||
total_pages: Math.ceil(count / query.per_page),
|
||||
total_items: count,
|
||||
},
|
||||
} satisfies ListResponse<PartnerResponse>);
|
||||
}
|
||||
|
||||
async view(req: Request, res: Response) {
|
||||
const parseParamsResult = partnerParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(
|
||||
Partner,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!partner) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Partner not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(partner),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<PartnerResponse>);
|
||||
}
|
||||
|
||||
async update(req: Request, res: Response) {
|
||||
const parseParamsResult = partnerParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = partnerUpdateRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(
|
||||
Partner,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!partner) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Partner not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (body.avatar !== null) {
|
||||
await this.fileStorage.storeFile(
|
||||
Buffer.from(body.avatar, "base64"),
|
||||
partner.avatar ?? undefined,
|
||||
);
|
||||
} else if (partner.avatar !== null) {
|
||||
await this.fileStorage.removeFile(partner.avatar);
|
||||
}
|
||||
|
||||
partner.name = body.name;
|
||||
partner.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.updatePartner,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
partner.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
partner.email,
|
||||
partner.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Partner updated successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async changeEmail(req: Request, res: Response) {
|
||||
const parseParamsResult = partnerParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = partnerChangeEmailRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(
|
||||
Partner,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!partner) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Partner not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
partner.email = body.new_email;
|
||||
partner.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.changeEmailPartner,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
partner.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
partner.email,
|
||||
partner.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Partner's email changed successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async changePassword(req: Request, res: Response) {
|
||||
const parseParamsResult = partnerParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = partnerChangePasswordRequestSchema.safeParse(
|
||||
req.body,
|
||||
);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(
|
||||
Partner,
|
||||
{
|
||||
id: params.id,
|
||||
verification: null,
|
||||
},
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!partner) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Partner not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (!(await Bun.password.verify(body.old_password, partner.password))) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "old_password",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
partner.password = await Bun.password.hash(body.new_password);
|
||||
partner.verification = orm.em.create(Verification, {
|
||||
id: ulid(),
|
||||
code: generateRandomCode(6),
|
||||
type: VerificationType.changePasswordPartner,
|
||||
expiredAt: dateFns.addHours(new Date(), 1),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
partner.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
await this.emailService.sendVerificationEmail(
|
||||
partner.email,
|
||||
partner.verification.code,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
message:
|
||||
"Partner's password changed successfully. Please check your email for verification.",
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async verify(req: Request, res: Response) {
|
||||
const parseParamsResult = partnerParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const parseBodyResult = partnerVerifyRequestSchema.safeParse(req.body);
|
||||
if (!parseBodyResult.success) {
|
||||
return this.handleZodError(parseBodyResult.error, res, "body");
|
||||
}
|
||||
const body = parseBodyResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(
|
||||
Partner,
|
||||
{ id: params.id },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!partner) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Partner not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (partner.verification === null) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
message: "Partner is already verified.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (partner.verification.code !== body.code) {
|
||||
return res.status(400).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "code",
|
||||
location: "body",
|
||||
message: "Incorrect.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
orm.em.remove(partner.verification);
|
||||
|
||||
partner.verification = null;
|
||||
partner.updatedAt = new Date();
|
||||
|
||||
await orm.em.flush();
|
||||
|
||||
return res.status(200).json({
|
||||
data: this.mapper.mapEntityToResponse(partner),
|
||||
errors: null,
|
||||
} satisfies SingleResponse<PartnerResponse>);
|
||||
}
|
||||
|
||||
async refresh(_req: Request, res: Response) {
|
||||
const req = _req as Request & PartnerRequestPlugin;
|
||||
|
||||
const access = await this.jwtService.createPartnerToken(
|
||||
req.partner,
|
||||
JwtType.access,
|
||||
);
|
||||
const refresh = await this.jwtService.createPartnerToken(
|
||||
req.partner,
|
||||
JwtType.refresh,
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
data: {
|
||||
access_token: access.token,
|
||||
access_token_expires_at: access.expiresAt,
|
||||
refresh_token: refresh.token,
|
||||
refresh_token_expires_at: refresh.expiresAt,
|
||||
},
|
||||
errors: null,
|
||||
} satisfies SingleResponse);
|
||||
}
|
||||
|
||||
async delete(req: Request, res: Response) {
|
||||
const parseParamsResult = partnerParamsSchema.safeParse(req.params);
|
||||
if (!parseParamsResult.success) {
|
||||
return this.handleZodError(parseParamsResult.error, res, "params");
|
||||
}
|
||||
const params = parseParamsResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(
|
||||
Partner,
|
||||
{ id: params.id, verification: null },
|
||||
{
|
||||
populate: ["*"],
|
||||
},
|
||||
);
|
||||
if (!partner) {
|
||||
return res.status(404).json({
|
||||
data: null,
|
||||
errors: [
|
||||
{
|
||||
path: "id",
|
||||
location: "params",
|
||||
message: "Partner not found.",
|
||||
},
|
||||
],
|
||||
} satisfies ErrorResponse);
|
||||
}
|
||||
|
||||
if (partner.avatar !== null) {
|
||||
await this.fileStorage.removeFile(partner.avatar);
|
||||
}
|
||||
|
||||
await orm.em.removeAndFlush(partner);
|
||||
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.createPartner]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.post("/login", createOrmContextMiddleware, this.login.bind(this));
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/email",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]),
|
||||
this.changeEmail.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/password",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]),
|
||||
this.changePassword.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:id/verify",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.updatePartner]),
|
||||
this.verify.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/refresh",
|
||||
createOrmContextMiddleware,
|
||||
isPartnerMiddleware(this.jwtService),
|
||||
this.refresh.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [AdminPermission.deletePartner]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
18
src/modules/partner/partner.mapper.ts
Normal file
18
src/modules/partner/partner.mapper.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Partner } from "@/database/entities/partner.entity";
|
||||
import type { PartnerResponse } from "@/modules/partner/partner.types";
|
||||
|
||||
export class PartnerMapper {
|
||||
public constructor() {}
|
||||
|
||||
public mapEntityToResponse(partner: Partner): PartnerResponse {
|
||||
return {
|
||||
id: partner.id,
|
||||
name: partner.name,
|
||||
email: partner.email,
|
||||
whatsapp: partner.whatsapp,
|
||||
avatar: partner.avatar,
|
||||
created_at: partner.createdAt,
|
||||
updated_at: partner.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
54
src/modules/partner/partner.schemas.ts
Normal file
54
src/modules/partner/partner.schemas.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
emailSchema,
|
||||
passwordSchema,
|
||||
phoneNumberSchema,
|
||||
} from "@/common/schemas";
|
||||
import z from "zod";
|
||||
|
||||
export const partnerRequestSchema = z.object({
|
||||
name: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(100, "Max 100 characters."),
|
||||
email: emailSchema,
|
||||
whatsapp: phoneNumberSchema,
|
||||
password: passwordSchema,
|
||||
avatar: z
|
||||
.base64("Must be base64 string.")
|
||||
.nonempty("Must not empty.")
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export const partnerLoginRequestSchema = partnerRequestSchema.pick({
|
||||
email: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
export const partnerUpdateRequestSchema = partnerRequestSchema.pick({
|
||||
name: true,
|
||||
avatar: true,
|
||||
permissions: true,
|
||||
});
|
||||
|
||||
export const partnerChangeEmailRequestSchema = z.object({
|
||||
new_email: emailSchema,
|
||||
});
|
||||
|
||||
export const partnerChangePasswordRequestSchema = z.object({
|
||||
old_password: passwordSchema,
|
||||
new_password: passwordSchema,
|
||||
});
|
||||
|
||||
export const partnerVerifyRequestSchema = z.object({
|
||||
code: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.length(6, "Must be 6 characters."),
|
||||
});
|
||||
|
||||
export const partnerParamsSchema = z.object({
|
||||
id: z
|
||||
.string("Must be string.")
|
||||
.nonempty("Must not empty.")
|
||||
.max(200, "Max 200 characters."),
|
||||
});
|
||||
32
src/modules/partner/partner.types.ts
Normal file
32
src/modules/partner/partner.types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type {
|
||||
partnerChangeEmailRequestSchema,
|
||||
partnerChangePasswordRequestSchema,
|
||||
partnerParamsSchema,
|
||||
partnerRequestSchema,
|
||||
partnerUpdateRequestSchema,
|
||||
} from "@/modules/partner/partner.schemas";
|
||||
import z from "zod";
|
||||
|
||||
export type PartnerRequest = z.infer<typeof partnerRequestSchema>;
|
||||
|
||||
export type PartnerUpdateRequest = z.infer<typeof partnerUpdateRequestSchema>;
|
||||
|
||||
export type PartnerChangeEmailRequest = z.infer<
|
||||
typeof partnerChangeEmailRequestSchema
|
||||
>;
|
||||
|
||||
export type PartnerChangePasswordRequest = z.infer<
|
||||
typeof partnerChangePasswordRequestSchema
|
||||
>;
|
||||
|
||||
export type PartnerParams = z.infer<typeof partnerParamsSchema>;
|
||||
|
||||
export type PartnerResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
whatsapp: string;
|
||||
avatar: string | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Controller } from "@/common/controller";
|
||||
import { ormMiddleware } from "@/common/middlewares/orm.middleware";
|
||||
import { createOrmContextMiddleware } from "@/common/middlewares/create-orm-context.middleware";
|
||||
import { isAdminMiddleware } from "@/common/middlewares/is-admin.middleware";
|
||||
import { paginationQuerySchema } from "@/common/schemas";
|
||||
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
|
||||
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import type {
|
||||
ErrorResponse,
|
||||
ListResponse,
|
||||
@@ -10,6 +12,7 @@ import type {
|
||||
import { TransportationClass } from "@/database/entities/transportation-class.entity";
|
||||
import { TransportationImage } from "@/database/entities/transportation-image.entity";
|
||||
import { Transportation } from "@/database/entities/transportation.entity";
|
||||
import { AdminPermission } from "@/database/enums/admin-permission.enum";
|
||||
import { orm } from "@/database/orm";
|
||||
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
|
||||
import {
|
||||
@@ -29,6 +32,7 @@ export class TransportationController extends Controller {
|
||||
public constructor(
|
||||
private readonly mapper: TransportationMapper,
|
||||
private readonly fileStorage: AbstractFileStorage,
|
||||
private readonly jwtService: AbstractJwtService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -526,26 +530,64 @@ export class TransportationController extends Controller {
|
||||
|
||||
public buildRouter(): Router {
|
||||
const router = Router();
|
||||
router.post("/", ormMiddleware, this.create.bind(this));
|
||||
router.get("/", ormMiddleware, this.list.bind(this));
|
||||
router.get("/:id", ormMiddleware, this.view.bind(this));
|
||||
router.put("/:id", ormMiddleware, this.update.bind(this));
|
||||
router.delete("/:id", ormMiddleware, this.delete.bind(this));
|
||||
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this));
|
||||
router.get("/:id/classes", ormMiddleware, this.listClasses.bind(this));
|
||||
router.post(
|
||||
"/",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.createTransportation,
|
||||
]),
|
||||
this.create.bind(this),
|
||||
);
|
||||
router.get("/", createOrmContextMiddleware, this.list.bind(this));
|
||||
router.get("/:id", createOrmContextMiddleware, this.view.bind(this));
|
||||
router.put(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.updateTransportation,
|
||||
]),
|
||||
this.update.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:id",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.deleteTransportation,
|
||||
]),
|
||||
this.delete.bind(this),
|
||||
);
|
||||
router.post(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.createTransportationClass,
|
||||
]),
|
||||
this.createClass.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:id/classes",
|
||||
createOrmContextMiddleware,
|
||||
this.listClasses.bind(this),
|
||||
);
|
||||
router.get(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
this.viewClass.bind(this),
|
||||
);
|
||||
router.put(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.updateTransportationClass,
|
||||
]),
|
||||
this.updateClass.bind(this),
|
||||
);
|
||||
router.delete(
|
||||
"/:transportation_id/classes/:id",
|
||||
ormMiddleware,
|
||||
createOrmContextMiddleware,
|
||||
isAdminMiddleware(this.jwtService, [
|
||||
AdminPermission.deleteTransportationClass,
|
||||
]),
|
||||
this.deleteClass.bind(this),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user