add auth and payment api

This commit is contained in:
ItsMalma
2025-11-19 23:53:56 +07:00
parent 8f91994f29
commit 7e7a865368
64 changed files with 9067 additions and 2662 deletions

13
.env
View File

@@ -6,3 +6,16 @@ DATABASE_PORT=5432
DATABASE_USERNAME=malma DATABASE_USERNAME=malma
DATABASE_PASSWORD=kucing DATABASE_PASSWORD=kucing
DATABASE_NAME=goumrah 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

View File

@@ -6,3 +6,16 @@ DATABASE_PORT=
DATABASE_USER= DATABASE_USER=
DATABASE_PASSWORD= DATABASE_PASSWORD=
DATABASE_NAME= 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
View File

@@ -13,6 +13,8 @@
"express": "5.1.0", "express": "5.1.0",
"file-type": "21.0.0", "file-type": "21.0.0",
"helmet": "8.1.0", "helmet": "8.1.0",
"jsonwebtoken": "9.0.2",
"nodemailer": "7.0.10",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"slugify": "1.6.6", "slugify": "1.6.6",
"ulid": "3.0.1", "ulid": "3.0.1",
@@ -23,12 +25,76 @@
"@types/compression": "1.8.1", "@types/compression": "1.8.1",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
"@types/express": "5.0.5", "@types/express": "5.0.5",
"@types/jsonwebtoken": "9.0.10",
"@types/nodemailer": "7.0.4",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "5.9.3", "typescript": "5.9.3",
}, },
}, },
}, },
"packages": { "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=="], "@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=="], "@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=="], "@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/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=="], "@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/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/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/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/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], "@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=="], "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=="], "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=="], "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=="], "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=="], "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=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], "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-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=="], "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=="], "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=="], "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=="], "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": ["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=="], "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=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
@@ -297,10 +477,12 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "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=="], "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-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "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=="], "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=="], "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=="], "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=="], "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-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=="], "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=="], "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=="], "@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=="], "@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=="], "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=="], "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=="], "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/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=="], "@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=="],
"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=="],
"knex/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], "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=="],
} }
} }

View File

@@ -1,5 +1,6 @@
{ {
"scripts": { "scripts": {
"format": "prettier --write .",
"dev": "bun run src/server.ts", "dev": "bun run src/server.ts",
"dev:watch": "bun run --watch src/server.ts" "dev:watch": "bun run --watch src/server.ts"
}, },
@@ -19,6 +20,8 @@
"express": "5.1.0", "express": "5.1.0",
"file-type": "21.0.0", "file-type": "21.0.0",
"helmet": "8.1.0", "helmet": "8.1.0",
"jsonwebtoken": "9.0.2",
"nodemailer": "7.0.10",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"slugify": "1.6.6", "slugify": "1.6.6",
"ulid": "3.0.1", "ulid": "3.0.1",
@@ -29,6 +32,8 @@
"@types/compression": "1.8.1", "@types/compression": "1.8.1",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
"@types/express": "5.0.5", "@types/express": "5.0.5",
"@types/jsonwebtoken": "9.0.10",
"@types/nodemailer": "7.0.4",
"prettier": "3.6.2", "prettier": "3.6.2",
"typescript": "5.9.3" "typescript": "5.9.3"
} }

View File

@@ -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 type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
import { LocalFileStorage } from "@/common/services/file-storage/local.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 { 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 { AirlineController } from "@/modules/airline/airline.controller";
import { AirlineMapper } from "@/modules/airline/airline.mapper"; import { AirlineMapper } from "@/modules/airline/airline.mapper";
import { AirportController } from "@/modules/airport/airport.controller"; 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 { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
import { HotelController } from "@/modules/hotel/hotel.controller"; import { HotelController } from "@/modules/hotel/hotel.controller";
import { HotelMapper } from "@/modules/hotel/hotel.mapper"; 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 { PackageController } from "@/modules/package/package.controller";
import { PackageMapper } from "@/modules/package/package.mapper"; 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 { TransportationController } from "@/modules/transportation/transportation.controller";
import { TransportationMapper } from "@/modules/transportation/transportation.mapper"; import { TransportationMapper } from "@/modules/transportation/transportation.mapper";
import compression from "compression"; import compression from "compression";
@@ -28,14 +40,20 @@ export class Application {
private readonly _app: express.Application; private readonly _app: express.Application;
// Services // Services
private _emailService!: AbstractEmailService;
private _fileStorage!: AbstractFileStorage; private _fileStorage!: AbstractFileStorage;
private _jwtService!: AbstractJwtService;
private _paymentService!: AbstractPaymentService;
public constructor() { public constructor() {
this._app = express(); this._app = express();
} }
public initializeServices() { public initializeServices() {
this._emailService = new LibraryEmailService();
this._fileStorage = new LocalFileStorage(); this._fileStorage = new LocalFileStorage();
this._jwtService = new LibraryJwtService();
this._paymentService = new MidtransPaymentService();
} }
public initializeMiddlewares() { public initializeMiddlewares() {
@@ -60,29 +78,66 @@ export class Application {
hotelMapper, hotelMapper,
transportationMapper, transportationMapper,
); );
const adminMapper = new AdminMapper();
const partnerMapper = new PartnerMapper();
const orderMapper = new OrderMapper(packageMapper, partnerMapper);
const countryRouter = new CountryController(countryMapper).buildRouter(); const countryRouter = new CountryController(
const cityRouter = new CityController(cityMapper).buildRouter(); countryMapper,
this._jwtService,
).buildRouter();
const cityRouter = new CityController(
cityMapper,
this._jwtService,
).buildRouter();
const airlineRouter = new AirlineController( const airlineRouter = new AirlineController(
airlineMapper, airlineMapper,
this._fileStorage, this._fileStorage,
this._jwtService,
).buildRouter();
const airportRouter = new AirportController(
airportMapper,
this._jwtService,
).buildRouter();
const flightRouter = new FlightController(
flightMapper,
this._jwtService,
).buildRouter(); ).buildRouter();
const airportRouter = new AirportController(airportMapper).buildRouter();
const flightRouter = new FlightController(flightMapper).buildRouter();
const hotelFacilityRouter = new HotelFacilityController( const hotelFacilityRouter = new HotelFacilityController(
hotelFacilityMapper, hotelFacilityMapper,
this._jwtService,
).buildRouter(); ).buildRouter();
const hotelRouter = new HotelController( const hotelRouter = new HotelController(
hotelMapper, hotelMapper,
this._fileStorage, this._fileStorage,
this._jwtService,
).buildRouter(); ).buildRouter();
const transportationRouter = new TransportationController( const transportationRouter = new TransportationController(
transportationMapper, transportationMapper,
this._fileStorage, this._fileStorage,
this._jwtService,
).buildRouter(); ).buildRouter();
const packageRouter = new PackageController( const packageRouter = new PackageController(
packageMapper, packageMapper,
this._fileStorage, 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(); ).buildRouter();
this._app.use("/countries", countryRouter); this._app.use("/countries", countryRouter);
@@ -94,6 +149,9 @@ export class Application {
this._app.use("/hotels", hotelRouter); this._app.use("/hotels", hotelRouter);
this._app.use("/transportations", transportationRouter); this._app.use("/transportations", transportationRouter);
this._app.use("/packages", packageRouter); this._app.use("/packages", packageRouter);
this._app.use("/admins", adminRouter);
this._app.use("/partners", partnerRouter);
this._app.use("/orders", orderRouter);
} }
public initializeErrorHandlers() {} public initializeErrorHandlers() {}

View File

@@ -0,0 +1,5 @@
export class InvalidJwtError extends Error {
public constructor(message: string) {
super(message);
}
}

View File

@@ -0,0 +1,5 @@
export class PaymentError extends Error {
public constructor(message: string) {
super(message);
}
}

View File

@@ -2,7 +2,7 @@ import { orm } from "@/database/orm";
import { RequestContext } from "@mikro-orm/core"; import { RequestContext } from "@mikro-orm/core";
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from "express";
export function ormMiddleware( export function createOrmContextMiddleware(
_req: Request, _req: Request,
_res: Response, _res: Response,
next: NextFunction, next: NextFunction,

View 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();
};
}

View 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();
};
}

View File

@@ -45,3 +45,19 @@ export const dateSchema = z
} }
return parsedDate; 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.");

View File

@@ -0,0 +1,6 @@
export abstract class AbstractEmailService {
public abstract sendVerificationEmail(
to: string,
code: string,
): Promise<void>;
}

View 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}`,
});
}
}

View 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>;
}

View 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;
}
}

View File

@@ -0,0 +1,5 @@
import type { Order } from "@/database/entities/order.entity";
export abstract class AbstractPaymentService {
public abstract createPaymentUrl(order: Order): Promise<string>;
}

View 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]);
}
}
}

View File

@@ -3,7 +3,7 @@ import type z from "zod";
export type PaginationQuery = z.infer<typeof paginationQuerySchema>; export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
export type SingleResponse<T> = { export type SingleResponse<T = unknown> = {
data: T; data: T;
errors: null; errors: null;
}; };

11
src/common/utils.ts Normal file
View 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;
}

View File

@@ -16,5 +16,41 @@ export const _env = z
DATABASE_USERNAME: z.string("Must be string.").nonempty("Must not empty."), DATABASE_USERNAME: z.string("Must be string.").nonempty("Must not empty."),
DATABASE_PASSWORD: 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."), 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); .parse(Bun.env);

View 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;

View 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;

View 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;

View 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;
}

View File

@@ -14,13 +14,12 @@ export class Airline {
code!: string; code!: string;
@Property({ type: "varchar", length: 100 }) @Property({ type: "varchar", length: 100 })
@Unique()
logo!: string; logo!: string;
@Property({ type: "int", unsigned: true }) @Property({ type: "int", unsigned: true })
skytraxRating!: number; skytraxRating!: number;
@Enum(() => SkytraxType) @Enum({ items: () => SkytraxType, nativeEnumName: "skytrax_type" })
skytraxType!: SkytraxType; skytraxType!: SkytraxType;
@Property({ @Property({

View 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;
}

View 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);
}

View File

@@ -21,7 +21,10 @@ export abstract class PackageItineraryWidget {
@ManyToOne(() => PackageItineraryDay) @ManyToOne(() => PackageItineraryDay)
packageItineraryDay!: Rel<PackageItineraryDay>; packageItineraryDay!: Rel<PackageItineraryDay>;
@Enum(() => PackageItineraryWidgetType) @Enum({
type: () => PackageItineraryWidgetType,
nativeEnumName: "package_itinerary_widget_type",
})
type!: PackageItineraryWidgetType; type!: PackageItineraryWidgetType;
@Property({ @Property({

View File

@@ -9,7 +9,6 @@ import {
OneToMany, OneToMany,
PrimaryKey, PrimaryKey,
Property, Property,
Unique,
} from "@mikro-orm/core"; } from "@mikro-orm/core";
@Entity() @Entity()
@@ -20,14 +19,13 @@ export class Package {
@Property({ type: "varchar", length: 100 }) @Property({ type: "varchar", length: 100 })
name!: string; name!: string;
@Enum(() => PackageType) @Enum({ items: () => PackageType, nativeEnumName: "package_type" })
type!: PackageType; type!: PackageType;
@Enum(() => PackageClass) @Enum({ items: () => PackageClass, nativeEnumName: "package_class" })
class!: PackageClass; class!: PackageClass;
@Property({ type: "varchar", length: 100 }) @Property({ type: "varchar", length: 100 })
@Unique()
thumbnail!: string; thumbnail!: string;
@Property({ type: "boolean" }) @Property({ type: "boolean" })

View 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);
}

View 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;
}

View 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",
}

View File

@@ -0,0 +1,6 @@
export enum RoomType {
double = "double",
triple = "triple",
quad = "quad",
infant = "infant",
}

View 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

View File

@@ -1,21 +1,35 @@
import { Migration } from '@mikro-orm/migrations'; import { Migration } from "@mikro-orm/migrations";
export class Migration20251112105413 extends Migration { 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(
this.addSql(`alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`); `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));`); override async down(): Promise<void> {
this.addSql(`alter table "transportation_image" alter column "transportation_id" drop not null;`); this.addSql(
this.addSql(`alter table "transportation_image" add constraint "transportation_image_transportation_id_foreign" foreign key ("transportation_id") references "transportation_class" ("id") on delete cascade;`); `alter table "transportation_image" drop constraint "transportation_image_transportation_id_foreign";`,
} );
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;`);
}
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;`,
);
}
} }

View 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";`);
}
}

View 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;
}
}

View 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,
};
}
}

View 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."),
});

View 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;
};

View File

@@ -1,13 +1,16 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage"; import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
SingleResponse, SingleResponse,
} from "@/common/types"; } from "@/common/types";
import { Airline } from "@/database/entities/airline.entity"; import { Airline } from "@/database/entities/airline.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { AirlineMapper } from "@/modules/airline/airline.mapper"; import type { AirlineMapper } from "@/modules/airline/airline.mapper";
import { import {
@@ -22,6 +25,7 @@ export class AirlineController extends Controller {
public constructor( public constructor(
private readonly mapper: AirlineMapper, private readonly mapper: AirlineMapper,
private readonly fileStorage: AbstractFileStorage, private readonly fileStorage: AbstractFileStorage,
private readonly jwtService: AbstractJwtService,
) { ) {
super(); super();
} }
@@ -43,7 +47,7 @@ export class AirlineController extends Controller {
code: body.code, code: body.code,
logo: logoFile.name, logo: logoFile.name,
skytraxRating: body.skytrax_rating, skytraxRating: body.skytrax_rating,
skytraxType: this.mapper.mapSkytraxType(body.skytrax_type), skytraxType: body.skytrax_type,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}); });
@@ -149,7 +153,7 @@ export class AirlineController extends Controller {
airline.name = body.name; airline.name = body.name;
airline.code = body.code; airline.code = body.code;
airline.skytraxRating = body.skytrax_rating; airline.skytraxRating = body.skytrax_rating;
airline.skytraxType = this.mapper.mapSkytraxType(body.skytrax_type); airline.skytraxType = body.skytrax_type;
airline.updatedAt = new Date(); airline.updatedAt = new Date();
await orm.em.flush(); await orm.em.flush();
@@ -196,11 +200,26 @@ export class AirlineController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createAirline]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); 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; return router;
} }

View File

@@ -1,24 +1,9 @@
import type { Airline } from "@/database/entities/airline.entity"; import type { Airline } from "@/database/entities/airline.entity";
import { SkytraxType } from "@/database/enums/skytrax-type.enum"; import type { AirlineResponse } from "@/modules/airline/airline.types";
import type {
AirlineRequest,
AirlineResponse,
} from "@/modules/airline/airline.types";
export class AirlineMapper { export class AirlineMapper {
public constructor() {} 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 { public mapEntityToResponse(airline: Airline): AirlineResponse {
return { return {
id: airline.id, id: airline.id,

View File

@@ -1,3 +1,4 @@
import { SkytraxType } from "@/database/enums/skytrax-type.enum";
import z from "zod"; import z from "zod";
export const airlineRequestSchema = z.object({ export const airlineRequestSchema = z.object({
@@ -16,7 +17,7 @@ export const airlineRequestSchema = z.object({
.min(1, "Minimum 1.") .min(1, "Minimum 1.")
.max(5, "Maximum 5."), .max(5, "Maximum 5."),
skytrax_type: z.enum( skytrax_type: z.enum(
["full_service", "low_cost"], SkytraxType,
"Must be either 'full_service' or 'low_cost'.", "Must be either 'full_service' or 'low_cost'.",
), ),
}); });

View File

@@ -1,3 +1,4 @@
import type { SkytraxType } from "@/database/enums/skytrax-type.enum";
import type { import type {
airlineParamsSchema, airlineParamsSchema,
airlineRequestSchema, airlineRequestSchema,
@@ -14,7 +15,7 @@ export type AirlineResponse = {
code: string; code: string;
logo: string; logo: string;
skytrax_rating: number; skytrax_rating: number;
skytrax_type: "full_service" | "low_cost"; skytrax_type: SkytraxType;
created_at: Date; created_at: Date;
updated_at: Date; updated_at: Date;
}; };

View File

@@ -1,6 +1,8 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
@@ -8,6 +10,7 @@ import type {
} from "@/common/types"; } from "@/common/types";
import { Airport } from "@/database/entities/airport.entity"; import { Airport } from "@/database/entities/airport.entity";
import { City } from "@/database/entities/city.entity"; import { City } from "@/database/entities/city.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { AirportMapper } from "@/modules/airport/airport.mapper"; import type { AirportMapper } from "@/modules/airport/airport.mapper";
import { import {
@@ -19,7 +22,10 @@ import { Router, type Request, type Response } from "express";
import { ulid } from "ulid"; import { ulid } from "ulid";
export class AirportController extends Controller { export class AirportController extends Controller {
public constructor(private readonly mapper: AirportMapper) { public constructor(
private readonly mapper: AirportMapper,
private readonly jwtService: AbstractJwtService,
) {
super(); super();
} }
@@ -224,11 +230,26 @@ export class AirportController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createAirport]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); 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; return router;
} }

View File

@@ -1,6 +1,8 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
@@ -8,6 +10,7 @@ import type {
} from "@/common/types"; } from "@/common/types";
import { City } from "@/database/entities/city.entity"; import { City } from "@/database/entities/city.entity";
import { Country } from "@/database/entities/country.entity"; import { Country } from "@/database/entities/country.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { CityMapper } from "@/modules/city/city.mapper"; import type { CityMapper } from "@/modules/city/city.mapper";
import { import {
@@ -19,7 +22,10 @@ import { Router, type Request, type Response } from "express";
import { ulid } from "ulid"; import { ulid } from "ulid";
export class CityController extends Controller { export class CityController extends Controller {
public constructor(private readonly mapper: CityMapper) { public constructor(
private readonly mapper: CityMapper,
private readonly jwtService: AbstractJwtService,
) {
super(); super();
} }
@@ -210,11 +216,26 @@ export class CityController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createCity]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); 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; return router;
} }

View File

@@ -1,12 +1,15 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
SingleResponse, SingleResponse,
} from "@/common/types"; } from "@/common/types";
import { Country } from "@/database/entities/country.entity"; import { Country } from "@/database/entities/country.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { CountryMapper } from "@/modules/country/country.mapper"; import type { CountryMapper } from "@/modules/country/country.mapper";
import { import {
@@ -18,7 +21,10 @@ import { Router, type Request, type Response } from "express";
import { ulid } from "ulid"; import { ulid } from "ulid";
export class CountryController extends Controller { export class CountryController extends Controller {
public constructor(private readonly mapper: CountryMapper) { public constructor(
private readonly mapper: CountryMapper,
private readonly jwtService: AbstractJwtService,
) {
super(); super();
} }
@@ -174,11 +180,26 @@ export class CountryController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createCountry]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); 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; return router;
} }

View File

@@ -1,6 +1,8 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
@@ -10,6 +12,7 @@ import { Airline } from "@/database/entities/airline.entity";
import { Airport } from "@/database/entities/airport.entity"; import { Airport } from "@/database/entities/airport.entity";
import { FlightClass } from "@/database/entities/flight-class.entity"; import { FlightClass } from "@/database/entities/flight-class.entity";
import { Flight } from "@/database/entities/flight.entity"; import { Flight } from "@/database/entities/flight.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { FlightMapper } from "@/modules/flight/flight.mapper"; import type { FlightMapper } from "@/modules/flight/flight.mapper";
import { import {
@@ -26,7 +29,10 @@ import { Router, type Request, type Response } from "express";
import { ulid } from "ulid"; import { ulid } from "ulid";
export class FlightController extends Controller { export class FlightController extends Controller {
public constructor(private readonly mapper: FlightMapper) { public constructor(
private readonly mapper: FlightMapper,
private readonly jwtService: AbstractJwtService,
) {
super(); super();
} }
@@ -622,26 +628,52 @@ export class FlightController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createFlight]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); this.create.bind(this),
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this)); );
router.get("/:id/classes", ormMiddleware, this.listClasses.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( router.get(
"/:flight_id/classes/:id", "/:flight_id/classes/:id",
ormMiddleware, createOrmContextMiddleware,
this.viewClass.bind(this), this.viewClass.bind(this),
); );
router.put( router.put(
"/:flight_id/classes/:id", "/:flight_id/classes/:id",
ormMiddleware, createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [AdminPermission.updateFlightClass]),
this.updateClass.bind(this), this.updateClass.bind(this),
); );
router.delete( router.delete(
"/:flight_id/classes/:id", "/:flight_id/classes/:id",
ormMiddleware, createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [AdminPermission.deleteFlightClass]),
this.deleteClass.bind(this), this.deleteClass.bind(this),
); );

View File

@@ -1,12 +1,15 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
SingleResponse, SingleResponse,
} from "@/common/types"; } from "@/common/types";
import { HotelFacility } from "@/database/entities/hotel-facility.entity"; import { HotelFacility } from "@/database/entities/hotel-facility.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper"; import type { HotelFacilityMapper } from "@/modules/hotel-facility/hotel-facility.mapper";
import { import {
@@ -18,7 +21,10 @@ import { Router, type Request, type Response } from "express";
import { ulid } from "ulid"; import { ulid } from "ulid";
export class HotelFacilityController extends Controller { export class HotelFacilityController extends Controller {
public constructor(private readonly mapper: HotelFacilityMapper) { public constructor(
private readonly mapper: HotelFacilityMapper,
private readonly jwtService: AbstractJwtService,
) {
super(); super();
} }
@@ -184,11 +190,26 @@ export class HotelFacilityController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createHotelFacility]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); 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; return router;
} }

View File

@@ -1,7 +1,9 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage"; import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
@@ -11,6 +13,7 @@ import { City } from "@/database/entities/city.entity";
import { HotelFacility } from "@/database/entities/hotel-facility.entity"; import { HotelFacility } from "@/database/entities/hotel-facility.entity";
import { HotelImage } from "@/database/entities/hotel-image.entity"; import { HotelImage } from "@/database/entities/hotel-image.entity";
import { Hotel } from "@/database/entities/hotel.entity"; import { Hotel } from "@/database/entities/hotel.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { HotelMapper } from "@/modules/hotel/hotel.mapper"; import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
import { import {
@@ -25,6 +28,7 @@ export class HotelController extends Controller {
public constructor( public constructor(
private readonly mapper: HotelMapper, private readonly mapper: HotelMapper,
private readonly fileStorage: AbstractFileStorage, private readonly fileStorage: AbstractFileStorage,
private readonly jwtService: AbstractJwtService,
) { ) {
super(); super();
} }
@@ -332,11 +336,26 @@ export class HotelController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createHotel]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); 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; return router;
} }

View 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;
}
}

View 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,
};
}
}

View 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."),
});

View 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;
};

View File

@@ -1,7 +1,9 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage"; import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
@@ -22,6 +24,7 @@ import {
import { PackageItinerary } from "@/database/entities/package-itinerary.entity"; import { PackageItinerary } from "@/database/entities/package-itinerary.entity";
import { Package } from "@/database/entities/package.entity"; import { Package } from "@/database/entities/package.entity";
import { TransportationClass } from "@/database/entities/transportation-class.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 { PackageItineraryWidgetType } from "@/database/enums/package-itinerary-widget-type.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { PackageMapper } from "@/modules/package/package.mapper"; import type { PackageMapper } from "@/modules/package/package.mapper";
@@ -43,6 +46,7 @@ export class PackageController extends Controller {
public constructor( public constructor(
private readonly mapper: PackageMapper, private readonly mapper: PackageMapper,
private readonly fileStorage: AbstractFileStorage, private readonly fileStorage: AbstractFileStorage,
private readonly jwtService: AbstractJwtService,
) { ) {
super(); super();
} }
@@ -61,8 +65,8 @@ export class PackageController extends Controller {
const package_ = orm.em.create(Package, { const package_ = orm.em.create(Package, {
id: ulid(), id: ulid(),
name: body.name, name: body.name,
type: this.mapper.mapPackageType(body.type), type: body.type,
class: this.mapper.mapPackageClass(body.class), class: body.class,
thumbnail: thumbnailFile.name, thumbnail: thumbnailFile.name,
useFastTrain: body.use_fast_train, useFastTrain: body.use_fast_train,
createdAt: new Date(), createdAt: new Date(),
@@ -177,8 +181,8 @@ export class PackageController extends Controller {
); );
package_.name = body.name; package_.name = body.name;
package_.type = this.mapper.mapPackageType(body.type); package_.type = body.type;
package_.class = this.mapper.mapPackageClass(body.class); package_.class = body.class;
package_.useFastTrain = body.use_fast_train; package_.useFastTrain = body.use_fast_train;
package_.updatedAt = new Date(); package_.updatedAt = new Date();
@@ -1330,26 +1334,52 @@ export class PackageController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [AdminPermission.createPackage]),
router.delete("/:id", ormMiddleware, this.delete.bind(this)); this.create.bind(this),
router.post("/:id/details", ormMiddleware, this.createDetail.bind(this)); );
router.get("/:id/details", ormMiddleware, this.listDetails.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( router.get(
"/:package_id/details/:id", "/:package_id/details/:id",
ormMiddleware, createOrmContextMiddleware,
this.viewDetail.bind(this), this.viewDetail.bind(this),
); );
router.put( router.put(
"/:package_id/details/:id", "/:package_id/details/:id",
ormMiddleware, createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [AdminPermission.updatePackageDetail]),
this.updateDetail.bind(this), this.updateDetail.bind(this),
); );
router.delete( router.delete(
"/:package_id/details/:id", "/:package_id/details/:id",
ormMiddleware, createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [AdminPermission.deletePackageDetail]),
this.deleteDetail.bind(this), this.deleteDetail.bind(this),
); );

View File

@@ -10,9 +10,7 @@ import type {
} from "@/database/entities/package-itinerary-widget.entity"; } from "@/database/entities/package-itinerary-widget.entity";
import type { PackageItinerary } from "@/database/entities/package-itinerary.entity"; import type { PackageItinerary } from "@/database/entities/package-itinerary.entity";
import type { Package } from "@/database/entities/package.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 { 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 { FlightMapper } from "@/modules/flight/flight.mapper";
import type { FlightClassResponse } from "@/modules/flight/flight.types"; import type { FlightClassResponse } from "@/modules/flight/flight.types";
import type { HotelMapper } from "@/modules/hotel/hotel.mapper"; import type { HotelMapper } from "@/modules/hotel/hotel.mapper";
@@ -22,7 +20,6 @@ import type {
PackageItineraryDayResponse, PackageItineraryDayResponse,
PackageItineraryResponse, PackageItineraryResponse,
PackageItineraryWidgetResponse, PackageItineraryWidgetResponse,
PackageRequest,
PackageResponse, PackageResponse,
} from "@/modules/package/package.types"; } from "@/modules/package/package.types";
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper"; import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
@@ -35,26 +32,6 @@ export class PackageMapper {
private readonly transportationMapper: TransportationMapper, 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 { public mapEntityToResponse(package_: Package): PackageResponse {
return { return {
id: package_.id, id: package_.id,

View File

@@ -1,4 +1,6 @@
import { dateSchema, timeSchema } from "@/common/schemas"; 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"; import z from "zod";
export const packageRequestSchema = z.object({ export const packageRequestSchema = z.object({
@@ -6,9 +8,9 @@ export const packageRequestSchema = z.object({
.string("Must be string.") .string("Must be string.")
.nonempty("Must not empty.") .nonempty("Must not empty.")
.max(100, "Max 100 characters."), .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( class: z.enum(
["silver", "gold", "platinum"], PackageClass,
"Must be either 'silver', 'gold', or 'platinum'.", "Must be either 'silver', 'gold', or 'platinum'.",
), ),
thumbnail: z.base64("Must be base64 string.").nonempty("Must not empty."), thumbnail: z.base64("Must be base64 string.").nonempty("Must not empty."),

View File

@@ -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 { FlightClassResponse } from "@/modules/flight/flight.types";
import type { HotelResponse } from "@/modules/hotel/hotel.types"; import type { HotelResponse } from "@/modules/hotel/hotel.types";
import type { import type {
@@ -20,8 +22,8 @@ export type PackageDetailParams = z.infer<typeof packageDetailParamsSchema>;
export type PackageResponse = { export type PackageResponse = {
id: string; id: string;
name: string; name: string;
type: "reguler" | "plus"; type: PackageType;
class: "silver" | "gold" | "platinum"; class: PackageClass;
thumbnail: string; thumbnail: string;
use_fast_train: boolean; use_fast_train: boolean;
created_at: Date; created_at: Date;

View 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;
}
}

View 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,
};
}
}

View 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."),
});

View 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;
};

View File

@@ -1,7 +1,9 @@
import { Controller } from "@/common/controller"; 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 { paginationQuerySchema } from "@/common/schemas";
import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage"; import type { AbstractFileStorage } from "@/common/services/file-storage/abstract.file-storage";
import type { AbstractJwtService } from "@/common/services/jwt-service/abstract.jwt-service";
import type { import type {
ErrorResponse, ErrorResponse,
ListResponse, ListResponse,
@@ -10,6 +12,7 @@ import type {
import { TransportationClass } from "@/database/entities/transportation-class.entity"; import { TransportationClass } from "@/database/entities/transportation-class.entity";
import { TransportationImage } from "@/database/entities/transportation-image.entity"; import { TransportationImage } from "@/database/entities/transportation-image.entity";
import { Transportation } from "@/database/entities/transportation.entity"; import { Transportation } from "@/database/entities/transportation.entity";
import { AdminPermission } from "@/database/enums/admin-permission.enum";
import { orm } from "@/database/orm"; import { orm } from "@/database/orm";
import type { TransportationMapper } from "@/modules/transportation/transportation.mapper"; import type { TransportationMapper } from "@/modules/transportation/transportation.mapper";
import { import {
@@ -29,6 +32,7 @@ export class TransportationController extends Controller {
public constructor( public constructor(
private readonly mapper: TransportationMapper, private readonly mapper: TransportationMapper,
private readonly fileStorage: AbstractFileStorage, private readonly fileStorage: AbstractFileStorage,
private readonly jwtService: AbstractJwtService,
) { ) {
super(); super();
} }
@@ -526,26 +530,64 @@ export class TransportationController extends Controller {
public buildRouter(): Router { public buildRouter(): Router {
const router = Router(); const router = Router();
router.post("/", ormMiddleware, this.create.bind(this)); router.post(
router.get("/", ormMiddleware, this.list.bind(this)); "/",
router.get("/:id", ormMiddleware, this.view.bind(this)); createOrmContextMiddleware,
router.put("/:id", ormMiddleware, this.update.bind(this)); isAdminMiddleware(this.jwtService, [
router.delete("/:id", ormMiddleware, this.delete.bind(this)); AdminPermission.createTransportation,
router.post("/:id/classes", ormMiddleware, this.createClass.bind(this)); ]),
router.get("/:id/classes", ormMiddleware, this.listClasses.bind(this)); 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( router.get(
"/:transportation_id/classes/:id", "/:transportation_id/classes/:id",
ormMiddleware, createOrmContextMiddleware,
this.viewClass.bind(this), this.viewClass.bind(this),
); );
router.put( router.put(
"/:transportation_id/classes/:id", "/:transportation_id/classes/:id",
ormMiddleware, createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [
AdminPermission.updateTransportationClass,
]),
this.updateClass.bind(this), this.updateClass.bind(this),
); );
router.delete( router.delete(
"/:transportation_id/classes/:id", "/:transportation_id/classes/:id",
ormMiddleware, createOrmContextMiddleware,
isAdminMiddleware(this.jwtService, [
AdminPermission.deleteTransportationClass,
]),
this.deleteClass.bind(this), this.deleteClass.bind(this),
); );