diff --git a/.jest/setEnvVars.ts b/.jest/setEnvVars.ts index 48a2337f..7505d6a8 100644 --- a/.jest/setEnvVars.ts +++ b/.jest/setEnvVars.ts @@ -1 +1 @@ -process.env.KAFKAJS_LOG_LEVEL='ERROR' \ No newline at end of file +process.env.KAFKAJS_LOG_LEVEL = "ERROR"; diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 0e4e8a74..00000000 --- a/.prettierrc.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - trailingComma: "es5", - tabWidth: 4, - printWidth: 120, - semi: true, - singleQuote: false, - useTabs: false, - bracketSpacing: true, - arrowParens: "avoid", - endOfLine: "auto", -}; diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..79794de7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ + { + "trailingComma": "es5", + "tabWidth": 2, + "printWidth": 120, + "semi": true, + "singleQuote": false, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} diff --git a/package-lock.json b/package-lock.json index 0f78ad45..eccea426 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,15 +98,6 @@ "typescript": "^4.8.3" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -235,12 +226,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.24.2", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -248,30 +239,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", - "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", - "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.4", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.4", - "@babel/parser": "^7.24.4", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -296,12 +287,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", - "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", "dev": true, "dependencies": { - "@babel/types": "^7.24.0", + "@babel/types": "^7.25.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" @@ -311,14 +302,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.23.5", - "@babel/helper-validator-option": "^7.23.5", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -350,63 +341,29 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.15", - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", - "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", "dev": true, "dependencies": { - "@babel/types": "^7.24.0" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-module-imports": "^7.22.15", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.20" + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" }, "engines": { "node": ">=6.9.0" @@ -416,86 +373,74 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", - "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", "dev": true, "dependencies": { - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.1", - "@babel/types": "^7.24.0" + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -576,10 +521,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", - "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -623,6 +571,36 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", @@ -648,12 +626,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", - "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -734,6 +712,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", @@ -750,12 +743,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", - "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.24.0" + "@babel/helper-plugin-utils": "^7.24.7" }, "engines": { "node": ">=6.9.0" @@ -765,33 +758,30 @@ } }, "node_modules/@babel/template": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.24.0", - "@babel/types": "^7.24.0" + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", - "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.1", - "@babel/generator": "^7.24.1", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.1", - "@babel/types": "^7.24.0", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -809,13 +799,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -887,9 +877,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -950,11 +940,11 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.6.tgz", - "integrity": "sha512-xP58G7wDQ4TCmN/cMUHh00DS7SRDv/+lC+xFLrTkMIN8h55X5NhZMLYbvy7dSELP15qlI6hPhNCRWVMtZMwqLA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.1.tgz", + "integrity": "sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==", "dependencies": { - "@grpc/proto-loader": "^0.7.10", + "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" }, "engines": { @@ -962,13 +952,13 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.12", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.12.tgz", - "integrity": "sha512-DCVwMxqYzpUCiDMl7hQ384FqP4T3DbNpXU8pt681l3UWCip1WUiD5JrkImUwCB9a7f2cq4CUTmi5r/xIMRPY1Q==", + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", - "protobufjs": "^7.2.4", + "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { @@ -979,9 +969,9 @@ } }, "node_modules/@grpc/proto-loader/node_modules/protobufjs": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", - "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -1005,6 +995,7 @@ "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", @@ -1054,6 +1045,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@isaacs/cliui": { @@ -1574,9 +1566,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { @@ -1626,9 +1618,9 @@ } }, "node_modules/@nestjs/axios": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", - "integrity": "sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.3.tgz", + "integrity": "sha512-h6TCn3yJwD6OKqqqfmtRS5Zo4E46Ip2n+gK1sqwzNBC+qxQ9xpCu+ODVRFur6V3alHSCSBxb3nNtt73VEdluyA==", "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "axios": "^1.3.1", @@ -2305,9 +2297,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" @@ -2391,9 +2383,9 @@ "dev": true }, "node_modules/@types/eslint": { - "version": "8.56.8", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.8.tgz", - "integrity": "sha512-LdDdQVDzDXf3ijhhMnE27C5vc0QEknD8GiMR/Hi+fVbdZNfAfCy2j69m0LjUd2MAy0+kIgnOtd5ndTmDk/VWCA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-gi6WQJ7cHRgZxtkQEoyHMppPjq9Kxo5Tjn2prSKDSmZrCz8TZ3jSRCeTJm+WoM+oB0WG37bRqLzaaU3q7JypGg==", "dev": true, "dependencies": { "@types/estree": "*", @@ -2429,9 +2421,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", - "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", "dev": true, "dependencies": { "@types/node": "*", @@ -2515,9 +2507,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", - "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "node_modules/@types/long": { @@ -2543,9 +2535,9 @@ "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, "node_modules/@types/nodemailer": { - "version": "6.4.14", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", - "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "version": "6.4.15", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", + "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -2617,9 +2609,9 @@ } }, "node_modules/@types/qs": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", - "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", "dev": true }, "node_modules/@types/range-parser": { @@ -2662,14 +2654,15 @@ "dev": true }, "node_modules/@types/superagent": { - "version": "8.1.6", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.6.tgz", - "integrity": "sha512-yzBOv+6meEHSzV2NThYYOA6RtqvPr3Hbob9ZLp3i07SH27CrYVfm8CrF7ydTmidtelsFiKx2I4gZAiAOamGgvQ==", + "version": "8.1.8", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.8.tgz", + "integrity": "sha512-nTqHJ2OTa7PFEpLahzSEEeFeqbMpmcN7OeayiOc7v+xk+/vyTKljRe+o4MPqSnPeRCMvtxuLG+5QqluUVQJOnA==", "dev": true, "dependencies": { "@types/cookiejar": "^2.1.5", "@types/methods": "^1.1.4", - "@types/node": "*" + "@types/node": "*", + "form-data": "^4.0.0" } }, "node_modules/@types/supertest": { @@ -2688,14 +2681,14 @@ "dev": true }, "node_modules/@types/validator": { - "version": "13.11.9", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", - "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==" }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -2711,9 +2704,9 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -3105,9 +3098,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "devOptional": true, "bin": { "acorn": "bin/acorn" @@ -3116,10 +3109,10 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-assertions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", - "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -3135,10 +3128,13 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", "devOptional": true, + "dependencies": { + "acorn": "^8.11.0" + }, "engines": { "node": ">=0.4.0" } @@ -3187,15 +3183,15 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -3315,6 +3311,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -3365,9 +3362,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -3464,23 +3461,26 @@ } }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", "dev": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -3590,21 +3590,21 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "dev": true, "funding": [ { @@ -3621,10 +3621,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -3758,9 +3758,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001608", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001608.tgz", - "integrity": "sha512-cjUJTQkk9fQlJR2s4HMuPMvTiRggl0rAVMtthQuyOlDWuqHXqN8azLq+pi8B2TjwKJ32diHjUqRIKeFX4z1FoA==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true, "funding": [ { @@ -3851,9 +3851,9 @@ } }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "engines": { "node": ">=6.0" @@ -3875,9 +3875,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, "node_modules/class-transformer": { @@ -4341,14 +4341,14 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -4565,10 +4565,25 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { - "version": "1.4.733", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz", - "integrity": "sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ==", + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", "dev": true }, "node_modules/emittery": { @@ -4605,9 +4620,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", - "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -4646,9 +4661,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", - "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "dev": true }, "node_modules/es6-promisify": { @@ -4844,9 +4859,9 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -5078,6 +5093,12 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -5132,10 +5153,31 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -5217,9 +5259,9 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -5373,9 +5415,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", - "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", "dev": true }, "node_modules/fs.realpath": { @@ -5383,6 +5425,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5395,6 +5451,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -5470,6 +5527,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5559,9 +5617,9 @@ } }, "node_modules/google-protobuf": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", - "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.4.tgz", + "integrity": "sha512-MnG7N936zcKTco4Jd2PX2U96Kf9PxygAPKBug+74LHzmHXmceN16MmRcdgZv+DGef/S9YvQAfRsNCn4cjf9yyQ==" }, "node_modules/gopd": { "version": "1.0.1", @@ -5742,9 +5800,9 @@ ] }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -5767,9 +5825,9 @@ } }, "node_modules/import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "dependencies": { "pkg-dir": "^4.2.0", @@ -5798,6 +5856,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5875,12 +5934,15 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5995,9 +6057,9 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", - "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "dependencies": { "@babel/core": "^7.23.9", @@ -6084,15 +6146,12 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -6100,6 +6159,46 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -6854,9 +6953,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.10.60", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.60.tgz", - "integrity": "sha512-Ctgq2lXUpEJo5j1762NOzl2xo7z7pqmVWYai0p07LvAkQ32tbPv3wb+tcUeHEiXhKU5buM4H9MXsXo6OlM6C2g==" + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.7.tgz", + "integrity": "sha512-x2xON4/Qg2bRIS11KIN9yCNYUjhtiEjNyptjX0mX+pyKHecxuJVLIpfX1lq9ZD6CrC/rB+y4GBi18c6CEcUR+A==" }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -6980,9 +7079,9 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } @@ -7107,12 +7206,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -7131,9 +7230,9 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", "engines": { "node": ">= 0.6" } @@ -7149,6 +7248,14 @@ "node": ">= 0.6" } }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -7162,6 +7269,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7437,15 +7545,15 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "dev": true }, "node_modules/nodemailer": { - "version": "6.9.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", - "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", + "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", "engines": { "node": ">=6.0.0" } @@ -7489,6 +7597,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -7514,9 +7623,12 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7564,17 +7676,17 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -7666,6 +7778,11 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7826,27 +7943,24 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/path-to-regexp": { "version": "3.2.0", @@ -7882,9 +7996,9 @@ } }, "node_modules/pg": { - "version": "8.11.5", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz", - "integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", "dependencies": { "pg-connection-string": "^2.6.4", "pg-pool": "^3.6.2", @@ -7971,9 +8085,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -8319,9 +8433,9 @@ } }, "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/readable-stream": { @@ -8468,14 +8582,15 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dependencies": { "glob": "^7.1.3" }, @@ -8537,9 +8652,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" }, "node_modules/schema-utils": { "version": "3.3.0", @@ -8560,12 +8675,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -8573,17 +8685,6 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -8954,6 +9055,7 @@ "version": "8.1.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", "integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==", + "deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net", "dev": true, "dependencies": { "component-emitter": "^1.3.0", @@ -9084,9 +9186,9 @@ } }, "node_modules/terser": { - "version": "5.30.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.30.3.tgz", - "integrity": "sha512-STdUgOUx8rLbMGO9IOwHLpCqolkDITFFQSMYYwKE1N2lY6MVSaeoi10z/EhWxRc6ybqoVmKSkhKYH/XUpl7vSA==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -9311,12 +9413,13 @@ } }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", + "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", "dev": true, "dependencies": { "bs-logger": "0.x", + "ejs": "^3.1.10", "fast-json-stable-stringify": "2.x", "jest-util": "^29.0.0", "json5": "^2.2.3", @@ -9329,10 +9432,11 @@ "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -9342,6 +9446,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -9617,19 +9724,31 @@ } }, "node_modules/typeorm/node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, "engines": { "node": ">=16 || 14 >=14.17" }, @@ -9638,9 +9757,9 @@ } }, "node_modules/typeorm/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "engines": { "node": ">=16 || 14 >=14.17" } @@ -9718,9 +9837,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -9737,8 +9856,8 @@ } ], "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -9783,9 +9902,9 @@ "devOptional": true }, "node_modules/v8-to-istanbul": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", @@ -9797,9 +9916,9 @@ } }, "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", "engines": { "node": ">= 0.10" } @@ -9827,9 +9946,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", @@ -9854,9 +9973,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.91.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.91.0.tgz", - "integrity": "sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -9865,10 +9984,10 @@ "@webassemblyjs/wasm-edit": "^1.12.1", "@webassemblyjs/wasm-parser": "^1.12.1", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.9.0", + "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.16.0", + "enhanced-resolve": "^5.17.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -10033,6 +10152,15 @@ "node": ">=8.12.0" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10085,9 +10213,9 @@ } }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { "node": ">=8.3.0" }, diff --git a/src/auth/api-key.strategy.ts b/src/auth/api-key.strategy.ts index a7593ffa..4f53dc2a 100644 --- a/src/auth/api-key.strategy.ts +++ b/src/auth/api-key.strategy.ts @@ -10,47 +10,36 @@ import { ApiKeyHeader, ApiKeyStrategyName, HeaderApiVerifiedCallback } from "./c const passReqToCallback = false; @Injectable() -export class ApiKeyStrategy extends PassportStrategy( - HeaderAPIKeyStrategy, - ApiKeyStrategyName -) { - constructor( - private authService: AuthService, - private permissionService: PermissionService - ) { - super( - { - header: ApiKeyHeader, - prefix: "", - }, - passReqToCallback - ); - } +export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, ApiKeyStrategyName) { + constructor(private authService: AuthService, private permissionService: PermissionService) { + super( + { + header: ApiKeyHeader, + prefix: "", + }, + passReqToCallback + ); + } - async validate( - apiKey: string, - _done: HeaderApiVerifiedCallback - ): Promise { - const apiKeyDb = await this.authService.validateApiKey(apiKey); - if (!apiKeyDb) { - throw new UnauthorizedException(ErrorCodes.ApiKeyAuthFailed); - } + async validate(apiKey: string, _done: HeaderApiVerifiedCallback): Promise { + const apiKeyDb = await this.authService.validateApiKey(apiKey); + if (!apiKeyDb) { + throw new UnauthorizedException(ErrorCodes.ApiKeyAuthFailed); + } - // Get the permissions and the UserID from the API Key instead of the user - const permissions = await this.permissionService.findPermissionGroupedByLevelForApiKey( - apiKeyDb.id - ); + // Get the permissions and the UserID from the API Key instead of the user + const permissions = await this.permissionService.findPermissionGroupedByLevelForApiKey(apiKeyDb.id); - // const permissions = dbApiKey.permissions as Permission[]; - const userId = apiKeyDb.systemUser.id; + // const permissions = dbApiKey.permissions as Permission[]; + const userId = apiKeyDb.systemUser.id; - // Set the permissions and the userId on the returned user - const user: AuthenticatedUser = { - userId, - username: apiKeyDb.systemUser.name, - permissions, - }; + // Set the permissions and the userId on the returned user + const user: AuthenticatedUser = { + userId, + username: apiKeyDb.systemUser.name, + permissions, + }; - return user; - } + return user; + } } diff --git a/src/auth/constants.ts b/src/auth/constants.ts index cf503f0f..7e34fc2c 100644 --- a/src/auth/constants.ts +++ b/src/auth/constants.ts @@ -1,9 +1,9 @@ import { AuthenticatedUser } from "@dto/internal/authenticated-user"; export type HeaderApiVerifiedCallback = ( - err: Error | null, - user?: AuthenticatedUser, - info?: Record + err: Error | null, + user?: AuthenticatedUser, + info?: Record ) => void; export const ApiKeyStrategyName = "api-key"; diff --git a/src/auth/custom-exception-filter.ts b/src/auth/custom-exception-filter.ts index 05e3e13b..8232c015 100644 --- a/src/auth/custom-exception-filter.ts +++ b/src/auth/custom-exception-filter.ts @@ -4,21 +4,21 @@ import { RedirectingException } from "./redirecting-exception"; @Catch(HttpException) export class CustomExceptionFilter implements ExceptionFilter { - catch(exception: HttpException, host: ArgumentsHost): void { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const request = ctx.getRequest(); - const status = exception.getStatus(); + catch(exception: HttpException, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus(); - if (status == 302) { - const asRedirectingException = exception as RedirectingException; - return response.redirect(asRedirectingException.url); - } - - response.status(status).json({ - statusCode: status, - timestamp: new Date().toISOString(), - path: request.url, - }); + if (status == 302) { + const asRedirectingException = exception as RedirectingException; + return response.redirect(asRedirectingException.url); } + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + }); + } } diff --git a/src/auth/handle-redirect-url-parameter.middleware.ts b/src/auth/handle-redirect-url-parameter.middleware.ts index 8592ac34..b7452af9 100644 --- a/src/auth/handle-redirect-url-parameter.middleware.ts +++ b/src/auth/handle-redirect-url-parameter.middleware.ts @@ -3,18 +3,18 @@ import { Request, Response } from "express"; @Injectable() export class HandleRedirectUrlParameterMiddleware implements NestMiddleware { - private readonly logger = new Logger(HandleRedirectUrlParameterMiddleware.name); + private readonly logger = new Logger(HandleRedirectUrlParameterMiddleware.name); - // eslint-disable-next-line @typescript-eslint/ban-types - use(req: Request, res: Response, next: Function): void { - const redirectParam = req.query["redirect"]; - if (redirectParam) { - this.logger.debug(`Has 'redirect' param: ${redirectParam}`); - res.cookie("redirect", redirectParam, { - expires: new Date(Date.now() + 900000), - }); - } - - next(); + // eslint-disable-next-line @typescript-eslint/ban-types + use(req: Request, res: Response, next: Function): void { + const redirectParam = req.query["redirect"]; + if (redirectParam) { + this.logger.debug(`Has 'redirect' param: ${redirectParam}`); + res.cookie("redirect", redirectParam, { + expires: new Date(Date.now() + 900000), + }); } + + next(); + } } diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index 83a0cc8b..6e89a16d 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -10,51 +10,46 @@ import { JwtStrategyName } from "./constants"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, JwtStrategyName) { - constructor( - private permissionService: PermissionService, - private userService: UserService, - private configService: ConfigService - ) { - super({ - // Configure the strategy to look for the JWT token in the Authorization header - jwtFromRequest: ExtractJwt.fromExtractors([ - ExtractJwt.fromAuthHeaderAsBearerToken(), - ExtractJwt.fromUrlQueryParameter("secret_token"), - ]), - ignoreExpiration: false, - secretOrKey: configService.get("jwt.secret"), - }); - } - private readonly logger = new Logger(JwtStrategy.name); - - private readonly NAME_ID_FORMAT = - "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"; + constructor( + private permissionService: PermissionService, + private userService: UserService, + private configService: ConfigService + ) { + super({ + // Configure the strategy to look for the JWT token in the Authorization header + jwtFromRequest: ExtractJwt.fromExtractors([ + ExtractJwt.fromAuthHeaderAsBearerToken(), + ExtractJwt.fromUrlQueryParameter("secret_token"), + ]), + ignoreExpiration: false, + secretOrKey: configService.get("jwt.secret"), + }); + } + private readonly logger = new Logger(JwtStrategy.name); - async validate(payload: JwtPayloadDto): Promise { - // Does the user still exist? - const exists = await this.userService.findOne(payload.sub); - if (!exists) { - this.logger.warn( - `Authorization for user with id: ${payload.sub} failed, since they no longer exists` - ); - throw new UnauthorizedException(); - } + private readonly NAME_ID_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"; - const result: AuthenticatedUser = { - userId: payload.sub, - username: payload.username, - }; + async validate(payload: JwtPayloadDto): Promise { + // Does the user still exist? + const exists = await this.userService.findOne(payload.sub); + if (!exists) { + this.logger.warn(`Authorization for user with id: ${payload.sub} failed, since they no longer exists`); + throw new UnauthorizedException(); + } - if (exists.nameId) { - // Add SAML stuff - result.nameID = exists.nameId; - result.nameIDFormat = this.NAME_ID_FORMAT; - } - // This data is already validated - result.permissions = await this.permissionService.findPermissionGroupedByLevelForUser( - payload.sub - ); + const result: AuthenticatedUser = { + userId: payload.sub, + username: payload.username, + }; - return result; + if (exists.nameId) { + // Add SAML stuff + result.nameID = exists.nameId; + result.nameIDFormat = this.NAME_ID_FORMAT; } + // This data is already validated + result.permissions = await this.permissionService.findPermissionGroupedByLevelForUser(payload.sub); + + return result; + } } diff --git a/src/auth/kombit-auth.guard.ts b/src/auth/kombit-auth.guard.ts index b9caa68d..943f7ec0 100644 --- a/src/auth/kombit-auth.guard.ts +++ b/src/auth/kombit-auth.guard.ts @@ -1,47 +1,32 @@ import { ErrorCodes } from "@enum/error-codes.enum"; -import { - ExecutionContext, - Injectable, - Logger, - UnauthorizedException, -} from "@nestjs/common"; +import { ExecutionContext, Injectable, Logger, UnauthorizedException } from "@nestjs/common"; import { AuthGuard } from "@nestjs/passport"; import { Request as expressRequest, Response } from "express"; import { RedirectingException } from "./redirecting-exception"; @Injectable() export class KombitAuthGuard extends AuthGuard("kombit") { - constructor() { - super(); - } + constructor() { + super(); + } - private readonly logger = new Logger(KombitAuthGuard.name); + private readonly logger = new Logger(KombitAuthGuard.name); - handleRequest( - err: any, - user: any, - info: any, - context: ExecutionContext, - status: any - ) { - if (err || !user) { - const req: expressRequest = context.switchToHttp().getRequest(); - const res: Response = context.switchToHttp().getResponse(); - const redirectTarget = req.cookies["redirect"]; - this.logger.error(`Login with KOMBIT failed, got error: ${err}`, err); + handleRequest(err: any, user: any, info: any, context: ExecutionContext, status: any) { + if (err || !user) { + const req: expressRequest = context.switchToHttp().getRequest(); + const res: Response = context.switchToHttp().getResponse(); + const redirectTarget = req.cookies["redirect"]; + this.logger.error(`Login with KOMBIT failed, got error: ${err}`, err); - if (redirectTarget) { - const redirectError = - err?.message == ErrorCodes.UserInactive - ? ErrorCodes.UserInactive - : ErrorCodes.KOMBITLoginFailed; - throw new RedirectingException( - `${redirectTarget}?error=${redirectError}` - ); - } else { - throw new UnauthorizedException(ErrorCodes.MissingRole); - } - } - return user; + if (redirectTarget) { + const redirectError = + err?.message == ErrorCodes.UserInactive ? ErrorCodes.UserInactive : ErrorCodes.KOMBITLoginFailed; + throw new RedirectingException(`${redirectTarget}?error=${redirectError}`); + } else { + throw new UnauthorizedException(ErrorCodes.MissingRole); + } } + return user; + } } diff --git a/src/auth/kombit.strategy.ts b/src/auth/kombit.strategy.ts index 153788d8..d13fd53d 100644 --- a/src/auth/kombit.strategy.ts +++ b/src/auth/kombit.strategy.ts @@ -8,48 +8,40 @@ import { Profile, Strategy } from "passport-saml"; @Injectable() export class KombitStrategy extends PassportStrategy(Strategy, "kombit") { - constructor(private readonly authService: AuthService) { - super({ - issuer: `${ - configuration()["backend"]["baseurl"] - }/api/v1/auth/kombit/metadata`, - audience: `${ - configuration()["backend"]["baseurl"] - }/api/v1/auth/kombit/metadata`, + constructor(private readonly authService: AuthService) { + super({ + issuer: `${configuration()["backend"]["baseurl"]}/api/v1/auth/kombit/metadata`, + audience: `${configuration()["backend"]["baseurl"]}/api/v1/auth/kombit/metadata`, - callbackUrl: `${ - configuration()["backend"]["baseurl"] - }/api/v1/auth/kombit/login/callback`, - logoutCallbackUrl: `${ - configuration()["backend"]["baseurl"] - }/api/v1/auth/kombit/logout/callback`, - logoutUrl: configuration()["kombit"]["entryPoint"], - entryPoint: configuration()["kombit"]["entryPoint"], - identifierFormat: "", - cert: configuration()["kombit"]["certificatePublicKey"], - privateCert: configuration()["kombit"]["certificatePrivateKey"], - decryptionPvk: configuration()["kombit"]["certificatePrivateKey"], - signatureAlgorithm: "sha256", - disableRequestedAuthnContext: true, - authnRequestBinding: "HTTP-Redirect", - acceptedClockSkewMs: 1000, // Allow some slack in clock sync - }); - } + callbackUrl: `${configuration()["backend"]["baseurl"]}/api/v1/auth/kombit/login/callback`, + logoutCallbackUrl: `${configuration()["backend"]["baseurl"]}/api/v1/auth/kombit/logout/callback`, + logoutUrl: configuration()["kombit"]["entryPoint"], + entryPoint: configuration()["kombit"]["entryPoint"], + identifierFormat: "", + cert: configuration()["kombit"]["certificatePublicKey"], + privateCert: configuration()["kombit"]["certificatePrivateKey"], + decryptionPvk: configuration()["kombit"]["certificatePrivateKey"], + signatureAlgorithm: "sha256", + disableRequestedAuthnContext: true, + authnRequestBinding: "HTTP-Redirect", + acceptedClockSkewMs: 1000, // Allow some slack in clock sync + }); + } - private readonly logger = new Logger(KombitStrategy.name); + private readonly logger = new Logger(KombitStrategy.name); - // eslint-disable-next-line @typescript-eslint/ban-types - async validate(profile: Profile, done: Function): Promise { - try { - const exists = await this.authService.validateKombitUser(profile); - done(null, exists); - return exists; - } catch (err) { - if (err?.message == ErrorCodes.MissingRole) { - done(null, ErrorCodes.MissingRole); - return null; - } - done(err, false); - } + // eslint-disable-next-line @typescript-eslint/ban-types + async validate(profile: Profile, done: Function): Promise { + try { + const exists = await this.authService.validateKombitUser(profile); + done(null, exists); + return exists; + } catch (err) { + if (err?.message == ErrorCodes.MissingRole) { + done(null, ErrorCodes.MissingRole); + return null; + } + done(err, false); } + } } diff --git a/src/auth/local.strategy.ts b/src/auth/local.strategy.ts index be24368b..cd83017e 100644 --- a/src/auth/local.strategy.ts +++ b/src/auth/local.strategy.ts @@ -6,15 +6,15 @@ import { LocalStrategyName } from "./constants"; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy, LocalStrategyName) { - constructor(private authService: AuthService) { - super(); - } + constructor(private authService: AuthService) { + super(); + } - async validate(username: string, password: string): Promise { - const user = await this.authService.validateUser(username, password); - if (!user) { - throw new UnauthorizedException(); - } - return user; + async validate(username: string, password: string): Promise { + const user = await this.authService.validateUser(username, password); + if (!user) { + throw new UnauthorizedException(); } + return user; + } } diff --git a/src/auth/redirecting-exception.ts b/src/auth/redirecting-exception.ts index 2e296cc9..1099e1a0 100644 --- a/src/auth/redirecting-exception.ts +++ b/src/auth/redirecting-exception.ts @@ -1,7 +1,7 @@ import { HttpException } from "@nestjs/common"; export class RedirectingException extends HttpException { - constructor(public url: string) { - super("Redirect to " + url, 302); - } + constructor(public url: string) { + super("Redirect to " + url, 302); + } } diff --git a/src/auth/roles.guard.ts b/src/auth/roles.guard.ts index 53e73790..610f450c 100644 --- a/src/auth/roles.guard.ts +++ b/src/auth/roles.guard.ts @@ -6,71 +6,65 @@ import { RolesMetaData } from "./constants"; @Injectable() export class RolesGuard implements CanActivate { - constructor(private reflector: Reflector) {} + constructor(private reflector: Reflector) {} - private readonly logger = new Logger(RolesGuard.name); + private readonly logger = new Logger(RolesGuard.name); - canActivate(context: ExecutionContext): boolean { - const roleRequiredMethod = this.reflector.get( - RolesMetaData, - context.getHandler() - ); - const roleRequiredClass = this.reflector.get( - RolesMetaData, - context.getClass() - ); - const roleRequired = roleRequiredMethod ?? roleRequiredClass; + canActivate(context: ExecutionContext): boolean { + const roleRequiredMethod = this.reflector.get(RolesMetaData, context.getHandler()); + const roleRequiredClass = this.reflector.get(RolesMetaData, context.getClass()); + const roleRequired = roleRequiredMethod ?? roleRequiredClass; - if (!roleRequired) { - return true; - } - - const request = context.switchToHttp().getRequest(); - const user: AuthenticatedUser = request.user; - this.logger.verbose( - JSON.stringify({ - msg: "Authorized user using JWT", - userId: user.userId, - userName: user.username, - }) - ); - return this.hasAccess(user, roleRequired); + if (!roleRequired) { + return true; } - hasAccess(user: AuthenticatedUser, roleRequired: string): boolean { - if (user.permissions.isGlobalAdmin) { - return true; - } else if (roleRequired == PermissionType.OrganizationApplicationAdmin) { - return this.hasOrganizationApplicationAdminAccess(user); - } else if (roleRequired == PermissionType.OrganizationUserAdmin) { - return this.hasOrganizationUserAdminAccess(user); - } else if (roleRequired == PermissionType.OrganizationGatewayAdmin) { - return this.hasOrganizationGatewayAdminAccess(user); - } else if (roleRequired == PermissionType.Read) { - return ( - this.hasOrganizationApplicationAdminAccess(user) || - this.hasOrganizationUserAdminAccess(user) || - this.hasOrganizationGatewayAdminAccess(user) || - this.hasReadAccess(user) - ); - } + const request = context.switchToHttp().getRequest(); + const user: AuthenticatedUser = request.user; + this.logger.verbose( + JSON.stringify({ + msg: "Authorized user using JWT", + userId: user.userId, + userName: user.username, + }) + ); + return this.hasAccess(user, roleRequired); + } - return false; + hasAccess(user: AuthenticatedUser, roleRequired: string): boolean { + if (user.permissions.isGlobalAdmin) { + return true; + } else if (roleRequired == PermissionType.OrganizationApplicationAdmin) { + return this.hasOrganizationApplicationAdminAccess(user); + } else if (roleRequired == PermissionType.OrganizationUserAdmin) { + return this.hasOrganizationUserAdminAccess(user); + } else if (roleRequired == PermissionType.OrganizationGatewayAdmin) { + return this.hasOrganizationGatewayAdminAccess(user); + } else if (roleRequired == PermissionType.Read) { + return ( + this.hasOrganizationApplicationAdminAccess(user) || + this.hasOrganizationUserAdminAccess(user) || + this.hasOrganizationGatewayAdminAccess(user) || + this.hasReadAccess(user) + ); } - hasOrganizationApplicationAdminAccess(user: AuthenticatedUser): boolean { - return user.permissions.orgToApplicationAdminPermissions.size > 0; - } + return false; + } - hasOrganizationUserAdminAccess(user: AuthenticatedUser): boolean { - return user.permissions.orgToUserAdminPermissions.size > 0; - } + hasOrganizationApplicationAdminAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToApplicationAdminPermissions.size > 0; + } - hasOrganizationGatewayAdminAccess(user: AuthenticatedUser): boolean { - return user.permissions.orgToGatewayAdminPermissions.size > 0; - } + hasOrganizationUserAdminAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToUserAdminPermissions.size > 0; + } - hasReadAccess(user: AuthenticatedUser): boolean { - return user.permissions.orgToReadPermissions.size > 0; - } + hasOrganizationGatewayAdminAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToGatewayAdminPermissions.size > 0; + } + + hasReadAccess(user: AuthenticatedUser): boolean { + return user.permissions.orgToReadPermissions.size > 0; + } } diff --git a/src/auth/swagger-auth-decorator.ts b/src/auth/swagger-auth-decorator.ts index cc5938fa..37fb4de7 100644 --- a/src/auth/swagger-auth-decorator.ts +++ b/src/auth/swagger-auth-decorator.ts @@ -2,5 +2,5 @@ import { ApiBearerAuth, ApiSecurity } from "@nestjs/swagger"; export function ApiAuth() { - return applyDecorators(ApiBearerAuth(), ApiSecurity("X-API-KEY")); + return applyDecorators(ApiBearerAuth(), ApiSecurity("X-API-KEY")); } diff --git a/src/config/configuration.ts b/src/config/configuration.ts index b26cfe02..cecf68ed 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -1,50 +1,50 @@ import { GetLogLevels, formatEmail } from "@helpers/env-variable-helper"; export default (): any => { - return { - port: parseInt(process.env.PORT, 10) || 3000, - database: { - host: process.env.DATABASE_HOSTNAME || "host.docker.internal", - port: parseInt(process.env.DATABASE_PORT, 10) || 5433, - username: process.env.DATABASE_USERNAME || "os2iot", - password: process.env.DATABASE_PASSWORD || "toi2so", - ssl: process.env.DATABASE_ENABLE_SSL === "true", - }, - jwt: { - secret: process.env.JWT_SECRET || "secretKey-os2iot-secretKey", - expiresIn: process.env.JWT_EXPIRESIN || "9h", - }, - backend: { - baseurl: process.env.BACKEND_BASEURL || "https://localhost:3000", - deviceStatsIntervalInDays: parseInt(process.env.DEVICE_STATS_INTERVAL_IN_DAYS, 10) || 29, - }, - kombit: { - entryPoint: - process.env.KOMBIT_ENTRYPOINT || - "https://adgangsstyring.eksterntest-stoettesystemerne.dk/runtime/saml2/issue.idp", - certificatePublicKey: process.env.KOMBIT_CERTIFICATEPUBLICKEY || "INSERT_KOMBIT_CERT", // Public certificate from Kombit Test server - certificatePrivateKey: process.env.KOMBIT_CERTIFICATEPRIVATEKEY || null, - roleUri: process.env.KOMBIT_ROLE_NAME || "http://os2iot.dk/roles/usersystemrole/adgang/", - }, - chirpstack: { - apikey: process.env.CHIRPSTACK_API_KEY || "apikey", - hostname: process.env.CHIRPSTACK_HOSTNAME || "localhost", - port: process.env.CHIRPSTACK_PORT || 8080, - }, - logLevels: process.env.LOG_LEVEL ? GetLogLevels(process.env.LOG_LEVEL) : GetLogLevels("debug"), - email: { - host: process.env.EMAIL_HOST || "smtp.ethereal.email", - port: process.env.EMAIL_PORT || 587, - user: process.env.EMAIL_USER || "tremayne38@ethereal.email", - pass: process.env.EMAIL_PASS || "UjbMtRZNkrUcVJsvTM", - /** - * Can be formatted to show a user-friendly name before the e-mail. - * E.g. "OS2iot " - */ - from: process.env.EMAIL_FROM ? formatEmail(process.env.EMAIL_FROM) : "OS2iot tremayne38@ethereal.email", - }, - frontend: { - baseurl: process.env.FRONTEND_BASEURL || "http://localhost:8081", - }, - }; + return { + port: parseInt(process.env.PORT, 10) || 3000, + database: { + host: process.env.DATABASE_HOSTNAME || "host.docker.internal", + port: parseInt(process.env.DATABASE_PORT, 10) || 5433, + username: process.env.DATABASE_USERNAME || "os2iot", + password: process.env.DATABASE_PASSWORD || "toi2so", + ssl: process.env.DATABASE_ENABLE_SSL === "true", + }, + jwt: { + secret: process.env.JWT_SECRET || "secretKey-os2iot-secretKey", + expiresIn: process.env.JWT_EXPIRESIN || "9h", + }, + backend: { + baseurl: process.env.BACKEND_BASEURL || "https://localhost:3000", + deviceStatsIntervalInDays: parseInt(process.env.DEVICE_STATS_INTERVAL_IN_DAYS, 10) || 29, + }, + kombit: { + entryPoint: + process.env.KOMBIT_ENTRYPOINT || + "https://adgangsstyring.eksterntest-stoettesystemerne.dk/runtime/saml2/issue.idp", + certificatePublicKey: process.env.KOMBIT_CERTIFICATEPUBLICKEY || "INSERT_KOMBIT_CERT", // Public certificate from Kombit Test server + certificatePrivateKey: process.env.KOMBIT_CERTIFICATEPRIVATEKEY || null, + roleUri: process.env.KOMBIT_ROLE_NAME || "http://os2iot.dk/roles/usersystemrole/adgang/", + }, + chirpstack: { + apikey: process.env.CHIRPSTACK_API_KEY || "apikey", + hostname: process.env.CHIRPSTACK_HOSTNAME || "localhost", + port: process.env.CHIRPSTACK_PORT || 8080, + }, + logLevels: process.env.LOG_LEVEL ? GetLogLevels(process.env.LOG_LEVEL) : GetLogLevels("debug"), + email: { + host: process.env.EMAIL_HOST || "smtp.ethereal.email", + port: process.env.EMAIL_PORT || 587, + user: process.env.EMAIL_USER || "tremayne38@ethereal.email", + pass: process.env.EMAIL_PASS || "UjbMtRZNkrUcVJsvTM", + /** + * Can be formatted to show a user-friendly name before the e-mail. + * E.g. "OS2iot " + */ + from: process.env.EMAIL_FROM ? formatEmail(process.env.EMAIL_FROM) : "OS2iot tremayne38@ethereal.email", + }, + frontend: { + baseurl: process.env.FRONTEND_BASEURL || "http://localhost:8081", + }, + }; }; diff --git a/src/config/constants/translations.ts b/src/config/constants/translations.ts index ac5aa98d..505cc4bc 100644 --- a/src/config/constants/translations.ts +++ b/src/config/constants/translations.ts @@ -1,6 +1,6 @@ // OS2iot won't be translated in any other language than Danish in the foreseeable future export enum Translations { - OrganizationAdmin = "Organisationsadministrator", - ApplicationAdmin = "Applikationsadministrator", - ReadLevel = "Læserettigheder", + OrganizationAdmin = "Organisationsadministrator", + ApplicationAdmin = "Applikationsadministrator", + ReadLevel = "Læserettigheder", } diff --git a/src/controllers/admin-controller/application.controller.ts b/src/controllers/admin-controller/application.controller.ts index 2c77bd1e..089b91d7 100644 --- a/src/controllers/admin-controller/application.controller.ts +++ b/src/controllers/admin-controller/application.controller.ts @@ -1,29 +1,29 @@ import { - BadRequestException, - Body, - Controller, - Delete, - Get, - Header, - Logger, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Delete, + Get, + Header, + Logger, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiProduces, - ApiResponse, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiProduces, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { ApplicationAdmin, Read } from "@auth/roles.decorator"; @@ -37,10 +37,10 @@ import { Application } from "@entities/application.entity"; import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-request"; import { ErrorCodes } from "@enum/error-codes.enum"; import { - ApplicationAccessScope, - checkIfUserHasAccessToApplication, - checkIfUserHasAccessToOrganization, - OrganizationAccessScope, + ApplicationAccessScope, + checkIfUserHasAccessToApplication, + checkIfUserHasAccessToOrganization, + OrganizationAccessScope, } from "@helpers/security-helper"; import { ApplicationService } from "@services/device-management/application.service"; import { AuditLog } from "@services/audit-log.service"; @@ -59,184 +59,184 @@ import { IoTDevicesListToMapResponseDto } from "@dto/list-all-iot-devices-to-map @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class ApplicationController { - constructor(private applicationService: ApplicationService) {} - - private readonly logger = new Logger(ApplicationController.name); - - @Read() - @Get() - @ApiProduces("application/json") - @ApiOperation({ summary: "Find all Applications (paginated)" }) - @ApiResponse({ - status: 200, - description: "Success", - type: ListAllApplicationsResponseDto, - }) - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllApplicationsDto - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return this.applicationService.findAndCountWithPagination( - query, - query.organizationId ? [+query.organizationId] : null - ); - } - - return await this.getApplicationsForNonGlobalAdmin(req, query); + constructor(private applicationService: ApplicationService) {} + + private readonly logger = new Logger(ApplicationController.name); + + @Read() + @Get() + @ApiProduces("application/json") + @ApiOperation({ summary: "Find all Applications (paginated)" }) + @ApiResponse({ + status: 200, + description: "Success", + type: ListAllApplicationsResponseDto, + }) + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllApplicationsDto + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return this.applicationService.findAndCountWithPagination( + query, + query.organizationId ? [+query.organizationId] : null + ); } - @Read() - @Get(":id") - @ApiOperation({ summary: "Find one Application by id" }) - @ApiNotFoundResponse() - async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { - checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Read); - - try { - return await this.applicationService.findOne(id); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - } + return await this.getApplicationsForNonGlobalAdmin(req, query); + } - @Read() - @Get(":id/iot-devices") - @ApiOperation({ summary: "Find the IoTDevice of an Application" }) - @ApiNotFoundResponse() - async findIoTDevicesForApplication( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) applicationId: number, - @Query() query?: ListAllEntitiesDto - ): Promise { - checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); - - try { - return await this.applicationService.findDevicesForApplication(applicationId, query); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - } + @Read() + @Get(":id") + @ApiOperation({ summary: "Find one Application by id" }) + @ApiNotFoundResponse() + async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Read); - @Read() - @Get(":id/iot-devices-map") - @ApiOperation({ summary: "Find the IoTDevices of an Application" }) - @ApiNotFoundResponse() - async findIoTDevicesForApplicationMap( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) applicationId: number, - ): Promise { - checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); - - try { - return await this.applicationService.findDevicesForApplicationMap(applicationId); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + try { + return await this.applicationService.findOne(id); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - - @ApplicationAdmin() - @Post() - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Create a new Application" }) - @ApiBadRequestResponse() - async create( - @Req() req: AuthenticatedRequest, - @Body() createApplicationDto: CreateApplicationDto - ): Promise { - checkIfUserHasAccessToOrganization( - req, - createApplicationDto?.organizationId, - OrganizationAccessScope.ApplicationWrite - ); - - const isValid = await this.applicationService.isNameValidAndNotUsed(createApplicationDto?.name); - - if (!isValid) { - this.logger.error(`Tried to create an application with name: '${createApplicationDto.name}'`); - AuditLog.fail(ActionType.CREATE, Application.name, req.user.userId); - throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); - } - - const application = await this.applicationService.create(createApplicationDto, req.user.userId); - AuditLog.success(ActionType.CREATE, Application.name, req.user.userId, application.id, application.name); - return application; + } + + @Read() + @Get(":id/iot-devices") + @ApiOperation({ summary: "Find the IoTDevice of an Application" }) + @ApiNotFoundResponse() + async findIoTDevicesForApplication( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) applicationId: number, + @Query() query?: ListAllEntitiesDto + ): Promise { + checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); + + try { + return await this.applicationService.findDevicesForApplication(applicationId, query); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - - @ApplicationAdmin() - @Put(":id") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Update an existing Application" }) - @ApiBadRequestResponse() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() updateApplicationDto: UpdateApplicationDto - ): Promise { - checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Write); - if (!(await this.applicationService.isNameValidAndNotUsed(updateApplicationDto?.name, id))) { - this.logger.error(`Tried to change an application with name: '${updateApplicationDto.name}'`); - AuditLog.fail(ActionType.UPDATE, Application.name, req.user.userId); - throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); - } - - const application = await this.applicationService.update(id, updateApplicationDto, req.user.userId); - - AuditLog.success(ActionType.UPDATE, Application.name, req.user.userId, application.id, application.name); - return application; + } + + @Read() + @Get(":id/iot-devices-map") + @ApiOperation({ summary: "Find the IoTDevices of an Application" }) + @ApiNotFoundResponse() + async findIoTDevicesForApplicationMap( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) applicationId: number + ): Promise { + checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); + + try { + return await this.applicationService.findDevicesForApplicationMap(applicationId); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } + + @ApplicationAdmin() + @Post() + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Create a new Application" }) + @ApiBadRequestResponse() + async create( + @Req() req: AuthenticatedRequest, + @Body() createApplicationDto: CreateApplicationDto + ): Promise { + checkIfUserHasAccessToOrganization( + req, + createApplicationDto?.organizationId, + OrganizationAccessScope.ApplicationWrite + ); + + const isValid = await this.applicationService.isNameValidAndNotUsed(createApplicationDto?.name); + + if (!isValid) { + this.logger.error(`Tried to create an application with name: '${createApplicationDto.name}'`); + AuditLog.fail(ActionType.CREATE, Application.name, req.user.userId); + throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); } - @ApplicationAdmin() - @Delete(":id") - @ApiOperation({ summary: "Delete an existing Application" }) - @ApiBadRequestResponse() - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Write); - - try { - const result = await this.applicationService.delete(id); - - if (result.affected === 0) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - AuditLog.success(ActionType.DELETE, Application.name, req.user.userId, id); - return new DeleteResponseDto(result.affected); - } catch (err) { - AuditLog.fail(ActionType.DELETE, Application.name, req.user.userId, id); - if (err.message == ErrorCodes.DeleteNotAllowedHasSigfoxDevice) { - throw err; - } - throw new NotFoundException(err); - } + const application = await this.applicationService.create(createApplicationDto, req.user.userId); + AuditLog.success(ActionType.CREATE, Application.name, req.user.userId, application.id, application.name); + return application; + } + + @ApplicationAdmin() + @Put(":id") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update an existing Application" }) + @ApiBadRequestResponse() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateApplicationDto: UpdateApplicationDto + ): Promise { + checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Write); + if (!(await this.applicationService.isNameValidAndNotUsed(updateApplicationDto?.name, id))) { + this.logger.error(`Tried to change an application with name: '${updateApplicationDto.name}'`); + AuditLog.fail(ActionType.UPDATE, Application.name, req.user.userId); + throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); } - private async getApplicationsForNonGlobalAdmin(req: AuthenticatedRequest, query: ListAllApplicationsDto) { - if (query?.organizationId) { - checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); - return await this.getApplicationsInOrganization(req, query); - } - - const allFromOrg = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); - const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); - const applications = await this.applicationService.findAndCountApplicationInWhitelistOrOrganization( - query, - allowedApplications, - query.organizationId ? [query.organizationId] : allFromOrg - ); - return applications; + const application = await this.applicationService.update(id, updateApplicationDto, req.user.userId); + + AuditLog.success(ActionType.UPDATE, Application.name, req.user.userId, application.id, application.name); + return application; + } + + @ApplicationAdmin() + @Delete(":id") + @ApiOperation({ summary: "Delete an existing Application" }) + @ApiBadRequestResponse() + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + checkIfUserHasAccessToApplication(req, id, ApplicationAccessScope.Write); + + try { + const result = await this.applicationService.delete(id); + + if (result.affected === 0) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + AuditLog.success(ActionType.DELETE, Application.name, req.user.userId, id); + return new DeleteResponseDto(result.affected); + } catch (err) { + AuditLog.fail(ActionType.DELETE, Application.name, req.user.userId, id); + if (err.message == ErrorCodes.DeleteNotAllowedHasSigfoxDevice) { + throw err; + } + throw new NotFoundException(err); } + } - private async getApplicationsInOrganization(req: AuthenticatedRequest, query: ListAllApplicationsDto) { - // User admins have access to all applications in the organization - const allFromOrg = req.user.permissions.getAllOrganizationsWithUserAdmin(); - if (allFromOrg.some(x => x === query?.organizationId)) { - return await this.applicationService.findAndCountWithPagination(query, [query.organizationId]); - } + private async getApplicationsForNonGlobalAdmin(req: AuthenticatedRequest, query: ListAllApplicationsDto) { + if (query?.organizationId) { + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); + return await this.getApplicationsInOrganization(req, query); + } - const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); - return await this.applicationService.findAndCountInList(query, allowedApplications, [query.organizationId]); + const allFromOrg = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); + const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); + const applications = await this.applicationService.findAndCountApplicationInWhitelistOrOrganization( + query, + allowedApplications, + query.organizationId ? [query.organizationId] : allFromOrg + ); + return applications; + } + + private async getApplicationsInOrganization(req: AuthenticatedRequest, query: ListAllApplicationsDto) { + // User admins have access to all applications in the organization + const allFromOrg = req.user.permissions.getAllOrganizationsWithUserAdmin(); + if (allFromOrg.some(x => x === query?.organizationId)) { + return await this.applicationService.findAndCountWithPagination(query, [query.organizationId]); } + + const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); + return await this.applicationService.findAndCountInList(query, allowedApplications, [query.organizationId]); + } } diff --git a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts index 854ae640..22d49da0 100644 --- a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts +++ b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts @@ -1,16 +1,16 @@ import { - BadRequestException, - Body, - Controller, - Delete, - Get, - InternalServerErrorException, - Param, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Delete, + Get, + InternalServerErrorException, + Param, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { ApiBadRequestResponse, ApiOperation, ApiProduces, ApiTags } from "@nestjs/swagger"; @@ -36,109 +36,99 @@ import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways-re @UseGuards(ComposeAuthGuard, RolesGuard) @ApiAuth() export class ChirpstackGatewayController { - constructor(private chirpstackGatewayService: ChirpstackGatewayService) {} + constructor(private chirpstackGatewayService: ChirpstackGatewayService) {} - @Post() - @ApiProduces("application/json") - @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) - @ApiBadRequestResponse() - @GatewayAdmin() - async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateGatewayDto): Promise { - checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.GatewayWrite); - try { - const gateway = await this.chirpstackGatewayService.createNewGateway(dto, req.user.userId); - AuditLog.success( - ActionType.CREATE, - "ChirpstackGateway", - req.user.userId, - dto.gateway.gatewayId, - dto.gateway.name - ); - return gateway; - } catch (err) { - AuditLog.fail( - ActionType.CREATE, - "ChirpstackGateway", - req.user.userId, - dto.gateway.gatewayId, - dto.gateway.name - ); - if (err?.response?.data?.message == "object already exists") { - throw new BadRequestException(ErrorCodes.IdInvalidOrAlreadyInUse); - } + @Post() + @ApiProduces("application/json") + @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) + @ApiBadRequestResponse() + @GatewayAdmin() + async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateGatewayDto): Promise { + checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.GatewayWrite); + try { + const gateway = await this.chirpstackGatewayService.createNewGateway(dto, req.user.userId); + AuditLog.success( + ActionType.CREATE, + "ChirpstackGateway", + req.user.userId, + dto.gateway.gatewayId, + dto.gateway.name + ); + return gateway; + } catch (err) { + AuditLog.fail(ActionType.CREATE, "ChirpstackGateway", req.user.userId, dto.gateway.gatewayId, dto.gateway.name); + if (err?.response?.data?.message == "object already exists") { + throw new BadRequestException(ErrorCodes.IdInvalidOrAlreadyInUse); + } - throw new InternalServerErrorException(err); - } + throw new InternalServerErrorException(err); } + } - @Get() - @ApiProduces("application/json") - @ApiOperation({ summary: "List all Chirpstack gateways" }) - @Read() - async getAll(@Query() query?: ListAllGatewaysDto): Promise { - return await this.chirpstackGatewayService.getWithPaginationAndSorting(query, query.organizationId); - } - - @Get(":gatewayId") - @ApiProduces("application/json") - @ApiOperation({ summary: "Single Chirpstack gateway" }) - @Read() - async getOne(@Param("gatewayId") gatewayId: string): Promise { - if (gatewayId?.length != 16) { - throw new BadRequestException(ErrorCodes.WrongLength); - } + @Get() + @ApiProduces("application/json") + @ApiOperation({ summary: "List all Chirpstack gateways" }) + @Read() + async getAll(@Query() query?: ListAllGatewaysDto): Promise { + return await this.chirpstackGatewayService.getWithPaginationAndSorting(query, query.organizationId); + } - if (!/[0-9A-Fa-f]{16}/.test(gatewayId)) { - throw new BadRequestException(ErrorCodes.NotValidFormat); - } + @Get(":gatewayId") + @ApiProduces("application/json") + @ApiOperation({ summary: "Single Chirpstack gateway" }) + @Read() + async getOne(@Param("gatewayId") gatewayId: string): Promise { + if (gatewayId?.length != 16) { + throw new BadRequestException(ErrorCodes.WrongLength); + } - return await this.chirpstackGatewayService.getOne(gatewayId); + if (!/[0-9A-Fa-f]{16}/.test(gatewayId)) { + throw new BadRequestException(ErrorCodes.NotValidFormat); } - @Put(":gatewayId") - @ApiProduces("application/json") - @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) - @ApiBadRequestResponse() - @GatewayAdmin() - async update( - @Req() req: AuthenticatedRequest, - @Param("gatewayId") gatewayId: string, - @Body() dto: UpdateGatewayDto - ): Promise { - try { - if (dto.gateway.gatewayId) { - throw new BadRequestException(ErrorCodes.GatewayIdNotAllowedInUpdate); - } - const gateway = await this.chirpstackGatewayService.modifyGateway(gatewayId, dto, req); - AuditLog.success(ActionType.UPDATE, "ChirpstackGateway", req.user.userId, gatewayId, dto.gateway.name); - return gateway; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, "ChirpstackGateway", req.user.userId, gatewayId, dto.gateway.name); - throw err; - } + return await this.chirpstackGatewayService.getOne(gatewayId); + } + + @Put(":gatewayId") + @ApiProduces("application/json") + @ApiOperation({ summary: "Create a new Chirpstack Gateway" }) + @ApiBadRequestResponse() + @GatewayAdmin() + async update( + @Req() req: AuthenticatedRequest, + @Param("gatewayId") gatewayId: string, + @Body() dto: UpdateGatewayDto + ): Promise { + try { + if (dto.gateway.gatewayId) { + throw new BadRequestException(ErrorCodes.GatewayIdNotAllowedInUpdate); + } + const gateway = await this.chirpstackGatewayService.modifyGateway(gatewayId, dto, req); + AuditLog.success(ActionType.UPDATE, "ChirpstackGateway", req.user.userId, gatewayId, dto.gateway.name); + return gateway; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, "ChirpstackGateway", req.user.userId, gatewayId, dto.gateway.name); + throw err; } + } - @Delete(":gatewayId") - @GatewayAdmin() - async delete( - @Req() req: AuthenticatedRequest, - @Param("gatewayId") gatewayId: string - ): Promise { - try { - const gw = await this.chirpstackGatewayService.getOne(gatewayId); - if (gw.gateway.organizationId != null) { - checkIfUserHasAccessToOrganization( - req, - +gw.gateway.organizationId, - OrganizationAccessScope.GatewayWrite - ); - } - const deleteResult = await this.chirpstackGatewayService.deleteGateway(gatewayId); - AuditLog.success(ActionType.DELETE, "ChirpstackGateway", req.user.userId, gatewayId); - return deleteResult; - } catch (err) { - AuditLog.fail(ActionType.DELETE, "ChirpstackGateway", req.user.userId, gatewayId); - throw err; - } + @Delete(":gatewayId") + @GatewayAdmin() + async delete( + @Req() req: AuthenticatedRequest, + @Param("gatewayId") gatewayId: string + ): Promise { + try { + const gw = await this.chirpstackGatewayService.getOne(gatewayId); + if (gw.gateway.organizationId != null) { + checkIfUserHasAccessToOrganization(req, +gw.gateway.organizationId, OrganizationAccessScope.GatewayWrite); + } + const deleteResult = await this.chirpstackGatewayService.deleteGateway(gatewayId); + AuditLog.success(ActionType.DELETE, "ChirpstackGateway", req.user.userId, gatewayId); + return deleteResult; + } catch (err) { + AuditLog.fail(ActionType.DELETE, "ChirpstackGateway", req.user.userId, gatewayId); + throw err; } + } } diff --git a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts index 26d63a03..0b0ac03d 100644 --- a/src/controllers/admin-controller/chirpstack/device-profile.controller.ts +++ b/src/controllers/admin-controller/chirpstack/device-profile.controller.ts @@ -1,19 +1,19 @@ import { - BadRequestException, - Body, - Controller, - Delete, - Get, - HttpCode, - InternalServerErrorException, - Logger, - NotFoundException, - Param, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Delete, + Get, + HttpCode, + InternalServerErrorException, + Logger, + NotFoundException, + Param, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { ApiBadRequestResponse, ApiNotFoundResponse, ApiOperation, ApiProduces, ApiTags } from "@nestjs/swagger"; @@ -40,149 +40,145 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiAuth() @ApplicationAdmin() export class DeviceProfileController { - constructor(private deviceProfileService: DeviceProfileService) {} + constructor(private deviceProfileService: DeviceProfileService) {} - CHIRPSTACK_IN_USE_ERROR = "this object is used by other objects, remove them first"; - private readonly logger = new Logger(DeviceProfileController.name); + CHIRPSTACK_IN_USE_ERROR = "this object is used by other objects, remove them first"; + private readonly logger = new Logger(DeviceProfileController.name); - @Post() - @ApiProduces("application/json") - @ApiOperation({ summary: "Create a new DeviceProfile" }) - @ApiBadRequestResponse() - @ApplicationAdmin() - async create(@Req() req: AuthenticatedRequest, @Body() createDto: CreateDeviceProfileDto): Promise { - checkIfUserHasAccessToOrganization( - req, - createDto.internalOrganizationId, - OrganizationAccessScope.ApplicationWrite - ); + @Post() + @ApiProduces("application/json") + @ApiOperation({ summary: "Create a new DeviceProfile" }) + @ApiBadRequestResponse() + @ApplicationAdmin() + async create(@Req() req: AuthenticatedRequest, @Body() createDto: CreateDeviceProfileDto): Promise { + checkIfUserHasAccessToOrganization(req, createDto.internalOrganizationId, OrganizationAccessScope.ApplicationWrite); - try { - const result = await this.deviceProfileService.createDeviceProfile(createDto, req.user.userId); + try { + const result = await this.deviceProfileService.createDeviceProfile(createDto, req.user.userId); - AuditLog.success( - ActionType.CREATE, - "ChirpstackDeviceProfile", - req.user.userId, - result.id, - createDto.deviceProfile.name - ); + AuditLog.success( + ActionType.CREATE, + "ChirpstackDeviceProfile", + req.user.userId, + result.id, + createDto.deviceProfile.name + ); - return result; - } catch (err) { - AuditLog.fail( - ActionType.CREATE, - "ChirpstackDeviceProfile", - req.user.userId, - createDto.deviceProfile.id, - createDto.deviceProfile.name - ); - throw err; - } + return result; + } catch (err) { + AuditLog.fail( + ActionType.CREATE, + "ChirpstackDeviceProfile", + req.user.userId, + createDto.deviceProfile.id, + createDto.deviceProfile.name + ); + throw err; } + } - @Put(":id") - @ApiProduces("application/json") - @ApiOperation({ summary: "Update an existing DeviceProfile" }) - @ApiBadRequestResponse() - @HttpCode(204) - @ApplicationAdmin() - async update( - @Req() req: AuthenticatedRequest, - @Param("id") id: string, - @Body() updateDto: UpdateDeviceProfileDto - ): Promise { - checkIfUserHasAccessToOrganization( - req, - updateDto.deviceProfile.internalOrganizationId, - OrganizationAccessScope.ApplicationWrite - ); + @Put(":id") + @ApiProduces("application/json") + @ApiOperation({ summary: "Update an existing DeviceProfile" }) + @ApiBadRequestResponse() + @HttpCode(204) + @ApplicationAdmin() + async update( + @Req() req: AuthenticatedRequest, + @Param("id") id: string, + @Body() updateDto: UpdateDeviceProfileDto + ): Promise { + checkIfUserHasAccessToOrganization( + req, + updateDto.deviceProfile.internalOrganizationId, + OrganizationAccessScope.ApplicationWrite + ); - try { - await this.deviceProfileService.updateDeviceProfile(updateDto, id, req); - AuditLog.success( - ActionType.UPDATE, - "ChirpstackDeviceProfile", - req.user.userId, - updateDto.deviceProfile.id, - updateDto.deviceProfile.name - ); - } catch (err) { - this.logger.error(`Error occured during put: '${JSON.stringify(err)}'`); - AuditLog.fail( - ActionType.CREATE, - "ChirpstackDeviceProfile", - req.user.userId, - updateDto.deviceProfile.id, - updateDto.deviceProfile.name - ); - if (err?.status >= 400 && err?.status < 500) { - throw err; - } - throw new InternalServerErrorException(err?.response?.data); - } + try { + await this.deviceProfileService.updateDeviceProfile(updateDto, id, req); + AuditLog.success( + ActionType.UPDATE, + "ChirpstackDeviceProfile", + req.user.userId, + updateDto.deviceProfile.id, + updateDto.deviceProfile.name + ); + } catch (err) { + this.logger.error(`Error occured during put: '${JSON.stringify(err)}'`); + AuditLog.fail( + ActionType.CREATE, + "ChirpstackDeviceProfile", + req.user.userId, + updateDto.deviceProfile.id, + updateDto.deviceProfile.name + ); + if (err?.status >= 400 && err?.status < 500) { + throw err; + } + throw new InternalServerErrorException(err?.response?.data); } + } - @Get("adr-algorithms") - @ApiProduces("application/json") - @ApiOperation({ summary: "Find all ADR algorithms for the default network server" }) - @Read() - async getAllAdrAlgorithms(): Promise { - return await this.deviceProfileService.getAdrAlgorithmsForChirpstack(); - } + @Get("adr-algorithms") + @ApiProduces("application/json") + @ApiOperation({ summary: "Find all ADR algorithms for the default network server" }) + @Read() + async getAllAdrAlgorithms(): Promise { + return await this.deviceProfileService.getAdrAlgorithmsForChirpstack(); + } - @Get(":id") - @ApiProduces("application/json") - @ApiOperation({ summary: "Find one DeviceProfile by id" }) - @ApiNotFoundResponse() - @Read() - async findOne(@Param("id") id: string): Promise { - let result = undefined; + @Get(":id") + @ApiProduces("application/json") + @ApiOperation({ summary: "Find one DeviceProfile by id" }) + @ApiNotFoundResponse() + @Read() + async findOne(@Param("id") id: string): Promise { + let result = undefined; - try { - result = await this.deviceProfileService.findOneDeviceProfileById(id); - } catch (err) { - this.logger.error(`Error occured during get/:id : '${JSON.stringify(err)}'`); - } - if (!result) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - return result; + try { + result = await this.deviceProfileService.findOneDeviceProfileById(id); + } catch (err) { + this.logger.error(`Error occured during get/:id : '${JSON.stringify(err)}'`); + } + if (!result) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + return result; + } - @Get() - @ApiProduces("application/json") - @ApiOperation({ summary: "Find all DeviceProfile" }) - @Read() - async getAll( - @Query("limit") limit: number, - @Query("offset") offset: number - ): Promise { - let result = undefined; - try { - this.logger.debug(`Limit: '${limit}' Offset:'${offset}'`); - result = await this.deviceProfileService.findAllDeviceProfiles(limit || 50, offset || 0); - } catch (err) { - this.logger.error(`Error occured during Find all: '${JSON.stringify(err?.response?.data)}'`); - } - return result; + @Get() + @ApiProduces("application/json") + @ApiOperation({ summary: "Find all DeviceProfile" }) + @Read() + async getAll( + @Query("limit") limit: number, + @Query("offset") offset: number + ): Promise { + let result = undefined; + try { + this.logger.debug(`Limit: '${limit}' Offset:'${offset}'`); + result = await this.deviceProfileService.findAllDeviceProfiles(limit || 50, offset || 0); + } catch (err) { + this.logger.error(`Error occured during Find all: '${JSON.stringify(err?.response?.data)}'`); } + return result; + } - @Delete(":id") - @ApiOperation({ summary: "Delete one DeviceProfile by id" }) - @ApiNotFoundResponse() - @ApplicationAdmin() - async deleteOne(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise { - try { - await this.deviceProfileService.deleteDeviceProfile(id, req); - AuditLog.success(ActionType.DELETE, "ChirpstackDeviceProfile", req.user.userId, id); - return new DeleteResponseDto(1); - } catch (err) { - AuditLog.fail(ActionType.DELETE, "ChirpstackDeviceProfile", req.user.userId, id); - if (err?.message == this.CHIRPSTACK_IN_USE_ERROR) { - throw new BadRequestException(ErrorCodes.IsUsed); - } - throw err; - } + @Delete(":id") + @ApiOperation({ summary: "Delete one DeviceProfile by id" }) + @ApiNotFoundResponse() + @ApplicationAdmin() + async deleteOne(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise { + try { + await this.deviceProfileService.deleteDeviceProfile(id, req); + AuditLog.success(ActionType.DELETE, "ChirpstackDeviceProfile", req.user.userId, id); + return new DeleteResponseDto(1); + } catch (err) { + AuditLog.fail(ActionType.DELETE, "ChirpstackDeviceProfile", req.user.userId, id); + if (err?.message == this.CHIRPSTACK_IN_USE_ERROR) { + throw new BadRequestException(ErrorCodes.IsUsed); + } + throw err; } + } } diff --git a/src/controllers/admin-controller/data-target.controller.ts b/src/controllers/admin-controller/data-target.controller.ts index f566226b..686368bc 100644 --- a/src/controllers/admin-controller/data-target.controller.ts +++ b/src/controllers/admin-controller/data-target.controller.ts @@ -1,25 +1,25 @@ import { - Body, - Controller, - Delete, - Get, - Header, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + Body, + Controller, + Delete, + Get, + Header, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; @@ -49,150 +49,148 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class DataTargetController { - constructor(private dataTargetService: DataTargetService, private organizationService: OrganizationService) {} + constructor(private dataTargetService: DataTargetService, private organizationService: OrganizationService) {} - @Get() - @ApiOperation({ summary: "Find all DataTargets" }) - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllDataTargetsDto - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.dataTargetService.findAndCountAllWithPagination(query); - } else { - if (query.applicationId) { - query.applicationId = +query.applicationId; - } + @Get() + @ApiOperation({ summary: "Find all DataTargets" }) + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllDataTargetsDto + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return await this.dataTargetService.findAndCountAllWithPagination(query); + } else { + if (query.applicationId) { + query.applicationId = +query.applicationId; + } - checkIfUserHasAccessToApplication(req, query.applicationId, ApplicationAccessScope.Read); - const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); + checkIfUserHasAccessToApplication(req, query.applicationId, ApplicationAccessScope.Read); + const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); - return await this.dataTargetService.findAndCountAllWithPagination(query, allowed); - } + return await this.dataTargetService.findAndCountAllWithPagination(query, allowed); } + } - @Get(":id") - @ApiOperation({ summary: "Find DataTarget by id" }) - async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { - try { - const dataTarget = await this.dataTargetService.findOne(id); - checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read); - return dataTarget; - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + @Get(":id") + @ApiOperation({ summary: "Find DataTarget by id" }) + async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + try { + const dataTarget = await this.dataTargetService.findOne(id); + checkIfUserHasAccessToApplication(req, dataTarget.application.id, ApplicationAccessScope.Read); + return dataTarget; + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + } - @Post() - @ApiOperation({ summary: "Create a new DataTargets" }) - @ApiBadRequestResponse() - async create( - @Req() req: AuthenticatedRequest, - @Body() createDataTargetDto: CreateDataTargetDto - ): Promise { - try { - checkIfUserHasAccessToApplication(req, createDataTargetDto.applicationId, ApplicationAccessScope.Write); - const dataTarget = await this.dataTargetService.create(createDataTargetDto, req.user.userId); - AuditLog.success(ActionType.CREATE, DataTarget.name, req.user.userId, dataTarget.id, dataTarget.name); - return dataTarget; - } catch (err) { - AuditLog.fail(ActionType.CREATE, DataTarget.name, req.user.userId); - throw err; - } + @Post() + @ApiOperation({ summary: "Create a new DataTargets" }) + @ApiBadRequestResponse() + async create( + @Req() req: AuthenticatedRequest, + @Body() createDataTargetDto: CreateDataTargetDto + ): Promise { + try { + checkIfUserHasAccessToApplication(req, createDataTargetDto.applicationId, ApplicationAccessScope.Write); + const dataTarget = await this.dataTargetService.create(createDataTargetDto, req.user.userId); + AuditLog.success(ActionType.CREATE, DataTarget.name, req.user.userId, dataTarget.id, dataTarget.name); + return dataTarget; + } catch (err) { + AuditLog.fail(ActionType.CREATE, DataTarget.name, req.user.userId); + throw err; } + } - @Put(":id") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Update an existing DataTarget" }) - @ApiBadRequestResponse() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() updateDto: UpdateDataTargetDto - ): Promise { - const oldDataTarget = await this.dataTargetService.findOne(id); - try { - checkIfUserHasAccessToApplication(req, oldDataTarget.application.id, ApplicationAccessScope.Write); - if (oldDataTarget.application.id !== updateDto.applicationId) { - checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write); - } - } catch (err) { - AuditLog.fail(ActionType.UPDATE, DataTarget.name, req.user.userId, oldDataTarget.id, oldDataTarget.name); - throw err; - } - - const dataTarget = await this.dataTargetService.update(id, updateDto, req.user.userId); - AuditLog.success(ActionType.UPDATE, DataTarget.name, req.user.userId, dataTarget.id, dataTarget.name); - return dataTarget; + @Put(":id") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update an existing DataTarget" }) + @ApiBadRequestResponse() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateDto: UpdateDataTargetDto + ): Promise { + const oldDataTarget = await this.dataTargetService.findOne(id); + try { + checkIfUserHasAccessToApplication(req, oldDataTarget.application.id, ApplicationAccessScope.Write); + if (oldDataTarget.application.id !== updateDto.applicationId) { + checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write); + } + } catch (err) { + AuditLog.fail(ActionType.UPDATE, DataTarget.name, req.user.userId, oldDataTarget.id, oldDataTarget.name); + throw err; } - @Delete(":id") - @ApiOperation({ summary: "Delete an existing DataTarget" }) - @ApiBadRequestResponse() - @ApplicationAdmin() - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const dt = await this.dataTargetService.findOne(id); - checkIfUserHasAccessToApplication(req, dt.application.id, ApplicationAccessScope.Write); - const result = await this.dataTargetService.delete(id); + const dataTarget = await this.dataTargetService.update(id, updateDto, req.user.userId); + AuditLog.success(ActionType.UPDATE, DataTarget.name, req.user.userId, dataTarget.id, dataTarget.name); + return dataTarget; + } - if (result.affected === 0) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - AuditLog.success(ActionType.DELETE, DataTarget.name, req.user.userId, id); - return new DeleteResponseDto(result.affected); - } catch (err) { - AuditLog.fail(ActionType.DELETE, DataTarget.name, req.user.userId, id); - if (err?.status === 403) { - throw err; - } - throw new NotFoundException(err); - } - } + @Delete(":id") + @ApiOperation({ summary: "Delete an existing DataTarget" }) + @ApiBadRequestResponse() + @ApplicationAdmin() + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const dt = await this.dataTargetService.findOne(id); + checkIfUserHasAccessToApplication(req, dt.application.id, ApplicationAccessScope.Write); + const result = await this.dataTargetService.delete(id); - @Get("getOpenDataDkRegistered/:organizationId") - @ApiOperation({ summary: "Get OpenDataDkRegistered-status for given OrganizationId" }) - @ApiNotFoundResponse() - @Read() - async getOpenDataDkRegistered( - @Param("organizationId", new ParseIntPipe()) organizationId: number - ): Promise { - try { - return (await this.organizationService.findById(organizationId))?.openDataDkRegistered; - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + if (result.affected === 0) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + AuditLog.success(ActionType.DELETE, DataTarget.name, req.user.userId, id); + return new DeleteResponseDto(result.affected); + } catch (err) { + AuditLog.fail(ActionType.DELETE, DataTarget.name, req.user.userId, id); + if (err?.status === 403) { + throw err; + } + throw new NotFoundException(err); } + } - @Put("updateOpenDataDkRegistered/:organizationId") - @ApiOperation({ - summary: - "Update the OpenDataDkRegistered to true, for the given OrganizationId - to stop showing the dialog for sending ODDK-mail on creation of new datatargets", - }) - @ApiNotFoundResponse() - async updateOpenDataDkRegistered( - @Req() req: AuthenticatedRequest, - @Param("organizationId", new ParseIntPipe()) organizationId: number - ): Promise { - try { - await this.organizationService.updateOpenDataDkRegistered(organizationId, req.user.userId); - return true; - } catch (err) { - if (err.name == "EntityNotFound") { - throw new NotFoundException(); - } - throw err; - } + @Get("getOpenDataDkRegistered/:organizationId") + @ApiOperation({ summary: "Get OpenDataDkRegistered-status for given OrganizationId" }) + @ApiNotFoundResponse() + @Read() + async getOpenDataDkRegistered(@Param("organizationId", new ParseIntPipe()) organizationId: number): Promise { + try { + return (await this.organizationService.findById(organizationId))?.openDataDkRegistered; + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + } - @Post("sendOpenDataDkMail") - @ApiOperation({ summary: "Send mail for registering datatargets to Open Data DK, for the given OrganizationId" }) - async sendOpenDataDkMail(@Req() req: AuthenticatedRequest, @Body() mailInfoDto: OddkMailInfo): Promise { - await this.dataTargetService.sendOpenDataDkMail(mailInfoDto, req.user.userId); - await this.organizationService.updateOpenDataDkRegistered(mailInfoDto.organizationId, req.user.userId); - return true; + @Put("updateOpenDataDkRegistered/:organizationId") + @ApiOperation({ + summary: + "Update the OpenDataDkRegistered to true, for the given OrganizationId - to stop showing the dialog for sending ODDK-mail on creation of new datatargets", + }) + @ApiNotFoundResponse() + async updateOpenDataDkRegistered( + @Req() req: AuthenticatedRequest, + @Param("organizationId", new ParseIntPipe()) organizationId: number + ): Promise { + try { + await this.organizationService.updateOpenDataDkRegistered(organizationId, req.user.userId); + return true; + } catch (err) { + if (err.name == "EntityNotFound") { + throw new NotFoundException(); + } + throw err; } + } + + @Post("sendOpenDataDkMail") + @ApiOperation({ summary: "Send mail for registering datatargets to Open Data DK, for the given OrganizationId" }) + async sendOpenDataDkMail(@Req() req: AuthenticatedRequest, @Body() mailInfoDto: OddkMailInfo): Promise { + await this.dataTargetService.sendOpenDataDkMail(mailInfoDto, req.user.userId); + await this.organizationService.updateOpenDataDkRegistered(mailInfoDto.organizationId, req.user.userId); + return true; + } } diff --git a/src/controllers/admin-controller/default.controller.ts b/src/controllers/admin-controller/default.controller.ts index 0d44919c..0089b55a 100644 --- a/src/controllers/admin-controller/default.controller.ts +++ b/src/controllers/admin-controller/default.controller.ts @@ -5,22 +5,22 @@ import { HealthCheckService } from "@services/health/health-check.service"; @ApiTags("os2iot") @Controller() export class DefaultController { - constructor(private healthCheckService: HealthCheckService) {} + constructor(private healthCheckService: HealthCheckService) {} - @Get() - getDefault(): string { - return "OS2IoT backend - See /api/v1/docs for Swagger"; - } + @Get() + getDefault(): string { + return "OS2IoT backend - See /api/v1/docs for Swagger"; + } - @Get("/healthcheck") - @ApiOkResponse() - @ApiInternalServerErrorResponse() - getHealthCheck(): string { - const isKafkaOk = this.healthCheckService.isKafkaOk(); - // This is the healthcheck for k8s - if (!isKafkaOk) { - throw new InternalServerErrorException("Kafka failed! :'("); - } - return "OK"; + @Get("/healthcheck") + @ApiOkResponse() + @ApiInternalServerErrorResponse() + getHealthCheck(): string { + const isKafkaOk = this.healthCheckService.isKafkaOk(); + // This is the healthcheck for k8s + if (!isKafkaOk) { + throw new InternalServerErrorException("Kafka failed! :'("); } + return "OK"; + } } diff --git a/src/controllers/admin-controller/device-model.controller.ts b/src/controllers/admin-controller/device-model.controller.ts index b71ceaa2..a5562d09 100644 --- a/src/controllers/admin-controller/device-model.controller.ts +++ b/src/controllers/admin-controller/device-model.controller.ts @@ -12,27 +12,27 @@ import { DeviceModel } from "@entities/device-model.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { - BadRequestException, - Body, - Controller, - Delete, - Get, - Header, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Delete, + Get, + Header, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { AuditLog } from "@services/audit-log.service"; import { DeviceModelService } from "@services/device-management/device-model.service"; @@ -46,93 +46,93 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class DeviceModelController { - constructor(private service: DeviceModelService) {} + constructor(private service: DeviceModelService) {} - @Get() - @ApiOperation({ summary: "Get all device models" }) - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllDeviceModelsDto - ): Promise { - if (query?.organizationId != null) { - checkIfUserHasAccessToOrganization(req, query?.organizationId, OrganizationAccessScope.ApplicationRead); - return this.service.getAllDeviceModelsByOrgIds([query?.organizationId], query); - } - - const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); - return this.service.getAllDeviceModelsByOrgIds(orgIds, query); + @Get() + @ApiOperation({ summary: "Get all device models" }) + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllDeviceModelsDto + ): Promise { + if (query?.organizationId != null) { + checkIfUserHasAccessToOrganization(req, query?.organizationId, OrganizationAccessScope.ApplicationRead); + return this.service.getAllDeviceModelsByOrgIds([query?.organizationId], query); } - @Get(":id") - @ApiOperation({ summary: "Get one device model" }) - async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { - const deviceModel = await this.service.getById(id); - if (!deviceModel) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + const orgIds = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); + return this.service.getAllDeviceModelsByOrgIds(orgIds, query); + } - checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationRead); - return deviceModel; + @Get(":id") + @ApiOperation({ summary: "Get one device model" }) + async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + const deviceModel = await this.service.getById(id); + if (!deviceModel) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - @Post() - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Create a new device model" }) - @ApiBadRequestResponse() - async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateDeviceModelDto): Promise { - try { - checkIfUserHasAccessToOrganization(req, dto.belongsToId, OrganizationAccessScope.ApplicationWrite); + checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationRead); + return deviceModel; + } + + @Post() + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Create a new device model" }) + @ApiBadRequestResponse() + async create(@Req() req: AuthenticatedRequest, @Body() dto: CreateDeviceModelDto): Promise { + try { + checkIfUserHasAccessToOrganization(req, dto.belongsToId, OrganizationAccessScope.ApplicationWrite); - const res = await this.service.create(dto, req.user.userId); - AuditLog.success(ActionType.CREATE, DeviceModel.name, req.user.userId, res.id); - return res; - } catch (err) { - AuditLog.fail(ActionType.CREATE, DeviceModel.name, req.user.userId); - throw err; - } + const res = await this.service.create(dto, req.user.userId); + AuditLog.success(ActionType.CREATE, DeviceModel.name, req.user.userId, res.id); + return res; + } catch (err) { + AuditLog.fail(ActionType.CREATE, DeviceModel.name, req.user.userId); + throw err; } + } - @Put(":id") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Update a device model" }) - @ApiBadRequestResponse() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() dto: UpdateDeviceModelDto - ): Promise { - try { - const deviceModel = await this.service.getByIdWithRelations(id); - checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationWrite); - const res = await this.service.update(deviceModel, dto, req.user.userId); - AuditLog.success(ActionType.UPDATE, DeviceModel.name, req.user.userId, id); - return res; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, DeviceModel.name, req.user.userId, id); - throw err; - } + @Put(":id") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update a device model" }) + @ApiBadRequestResponse() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() dto: UpdateDeviceModelDto + ): Promise { + try { + const deviceModel = await this.service.getByIdWithRelations(id); + checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationWrite); + const res = await this.service.update(deviceModel, dto, req.user.userId); + AuditLog.success(ActionType.UPDATE, DeviceModel.name, req.user.userId, id); + return res; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, DeviceModel.name, req.user.userId, id); + throw err; } + } - @Delete(":id") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Delete a device model" }) - @ApiBadRequestResponse() - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const deviceModel = await this.service.getByIdWithRelations(id); - checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationWrite); - const res = await this.service.delete(id); - AuditLog.success(ActionType.DELETE, DeviceModel.name, req.user.userId, id); - return new DeleteResponseDto(res.affected); - } catch (err) { - AuditLog.fail(ActionType.DELETE, DeviceModel.name, req.user.userId, id); - if (err?.name == "QueryFailedError") { - throw new BadRequestException(ErrorCodes.DeleteNotAllowedItemIsInUse); - } - throw err; - } + @Delete(":id") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Delete a device model" }) + @ApiBadRequestResponse() + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const deviceModel = await this.service.getByIdWithRelations(id); + checkIfUserHasAccessToOrganization(req, deviceModel.belongsTo.id, OrganizationAccessScope.ApplicationWrite); + const res = await this.service.delete(id); + AuditLog.success(ActionType.DELETE, DeviceModel.name, req.user.userId, id); + return new DeleteResponseDto(res.affected); + } catch (err) { + AuditLog.fail(ActionType.DELETE, DeviceModel.name, req.user.userId, id); + if (err?.name == "QueryFailedError") { + throw new BadRequestException(ErrorCodes.DeleteNotAllowedItemIsInUse); + } + throw err; } + } } diff --git a/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts b/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts index afa41bec..e8c5c5ed 100644 --- a/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts +++ b/src/controllers/admin-controller/iot-device-payload-decoder-data-target-connection.controller.ts @@ -1,26 +1,26 @@ import { - Body, - Controller, - Delete, - Get, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + Body, + Controller, + Delete, + Get, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiProduces, - ApiResponse, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiProduces, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; @@ -50,198 +50,185 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class IoTDevicePayloadDecoderDataTargetConnectionController { - constructor( - private service: IoTDevicePayloadDecoderDataTargetConnectionService, - private iotDeviceService: IoTDeviceService - ) {} + constructor( + private service: IoTDevicePayloadDecoderDataTargetConnectionService, + private iotDeviceService: IoTDeviceService + ) {} - @Get() - @ApiProduces("application/json") - @ApiOperation({ - summary: "Find all connections between IoT-Devices, PayloadDecoders and DataTargets (paginated)", - }) - @ApiResponse({ - status: 200, - description: "Success", - type: ListAllApplicationsResponseDto, - }) - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllEntitiesDto - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.service.findAndCountWithPagination(query); - } else { - const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); - return await this.service.findAndCountWithPagination(query, allowed); - } + @Get() + @ApiProduces("application/json") + @ApiOperation({ + summary: "Find all connections between IoT-Devices, PayloadDecoders and DataTargets (paginated)", + }) + @ApiResponse({ + status: 200, + description: "Success", + type: ListAllApplicationsResponseDto, + }) + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllEntitiesDto + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return await this.service.findAndCountWithPagination(query); + } else { + const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); + return await this.service.findAndCountWithPagination(query, allowed); } + } - @Get(":id") - @ApiNotFoundResponse({ - description: "If the id of the entity doesn't exist", - }) - async findOne( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - return await this.service.findOne(id); - } + @Get(":id") + @ApiNotFoundResponse({ + description: "If the id of the entity doesn't exist", + }) + async findOne( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + return await this.service.findOne(id); + } - @Get("byIoTDevice/:id") - @ApiOperation({ - summary: "Find all connections by IoT-Device id", - }) - async findByIoTDeviceId( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.service.findAllByIoTDeviceId(id); - } else { - return await this.service.findAllByIoTDeviceId( - id, - req.user.permissions.getAllApplicationsWithAtLeastRead() - ); - } + @Get("byIoTDevice/:id") + @ApiOperation({ + summary: "Find all connections by IoT-Device id", + }) + async findByIoTDeviceId( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return await this.service.findAllByIoTDeviceId(id); + } else { + return await this.service.findAllByIoTDeviceId(id, req.user.permissions.getAllApplicationsWithAtLeastRead()); } + } - @Get("byPayloadDecoder/:id") - @ApiOperation({ - summary: "Find all connections by PayloadDecoder id", - }) - async findByPayloadDecoderId( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.service.findAllByPayloadDecoderId(id); - } else { - return await this.service.findAllByPayloadDecoderId( - id, - req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead() - ); - } + @Get("byPayloadDecoder/:id") + @ApiOperation({ + summary: "Find all connections by PayloadDecoder id", + }) + async findByPayloadDecoderId( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return await this.service.findAllByPayloadDecoderId(id); + } else { + return await this.service.findAllByPayloadDecoderId( + id, + req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead() + ); } + } - @Get("byDataTarget/:id") - @ApiOperation({ - summary: "Find all connections by DataTarget id", - }) - async findByDataTargetId( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.service.findAllByDataTargetId(id); - } else { - const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); - return await this.service.findAllByDataTargetId(id, allowed); - } + @Get("byDataTarget/:id") + @ApiOperation({ + summary: "Find all connections by DataTarget id", + }) + async findByDataTargetId( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return await this.service.findAllByDataTargetId(id); + } else { + const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); + return await this.service.findAllByDataTargetId(id, allowed); } + } - @Post() - @ApplicationAdmin() - @ApiOperation({ - summary: "Create new connection", - }) - @ApiBadRequestResponse({ - description: "If one or more of the id's are invalid references.", - }) - async create( - @Req() req: AuthenticatedRequest, - @Body() - createDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto - ): Promise { - try { - await this.checkUserHasWriteAccessToAllIotDevices(createDto.iotDeviceIds, req); - const result = await this.service.create(createDto, req.user.userId); + @Post() + @ApplicationAdmin() + @ApiOperation({ + summary: "Create new connection", + }) + @ApiBadRequestResponse({ + description: "If one or more of the id's are invalid references.", + }) + async create( + @Req() req: AuthenticatedRequest, + @Body() + createDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto + ): Promise { + try { + await this.checkUserHasWriteAccessToAllIotDevices(createDto.iotDeviceIds, req); + const result = await this.service.create(createDto, req.user.userId); - AuditLog.success( - ActionType.CREATE, - IoTDevicePayloadDecoderDataTargetConnection.name, - req.user.userId, - result.id - ); - return result; - } catch (err) { - AuditLog.fail(ActionType.CREATE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId); - throw err; - } + AuditLog.success(ActionType.CREATE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, result.id); + return result; + } catch (err) { + AuditLog.fail(ActionType.CREATE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId); + throw err; } + } - private async checkUserHasWriteAccessToAllIotDevices(ids: number[], req: AuthenticatedRequest) { - const iotDevices = await this.iotDeviceService.findManyByIds(ids); - iotDevices.forEach(x => { - checkIfUserHasAccessToApplication(req, x.application.id, ApplicationAccessScope.Write); - }); - } + private async checkUserHasWriteAccessToAllIotDevices(ids: number[], req: AuthenticatedRequest) { + const iotDevices = await this.iotDeviceService.findManyByIds(ids); + iotDevices.forEach(x => { + checkIfUserHasAccessToApplication(req, x.application.id, ApplicationAccessScope.Write); + }); + } - @Put(":id") - @ApplicationAdmin() - @ApiNotFoundResponse({ - description: "If the id of the entity doesn't exist", - }) - @ApiBadRequestResponse({ - description: "If one or more of the id's are invalid references.", - }) - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() updateDto: UpdateConnectionDto - ): Promise { - try { - await this.checkIfUpdateIsAllowed(updateDto, req, id); - const result = await this.service.update(id, updateDto, req.user.userId); + @Put(":id") + @ApplicationAdmin() + @ApiNotFoundResponse({ + description: "If the id of the entity doesn't exist", + }) + @ApiBadRequestResponse({ + description: "If one or more of the id's are invalid references.", + }) + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateDto: UpdateConnectionDto + ): Promise { + try { + await this.checkIfUpdateIsAllowed(updateDto, req, id); + const result = await this.service.update(id, updateDto, req.user.userId); - AuditLog.success( - ActionType.UPDATE, - IoTDevicePayloadDecoderDataTargetConnection.name, - req.user.userId, - result.id - ); - return result; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, id); - throw err; - } + AuditLog.success(ActionType.UPDATE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, result.id); + return result; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, id); + throw err; } + } - private async checkIfUpdateIsAllowed(updateDto: UpdateConnectionDto, req: AuthenticatedRequest, id: number) { - const newIotDevice = await this.iotDeviceService.findOne(updateDto.iotDeviceIds[0]); - checkIfUserHasAccessToApplication(req, newIotDevice.application.id, ApplicationAccessScope.Write); - const oldConnection = await this.service.findOne(id); - await this.checkUserHasWriteAccessToAllIotDevices(updateDto.iotDeviceIds, req); - const oldIds = oldConnection.iotDevices.map(x => x.id); - if (updateDto.iotDeviceIds != oldIds) { - await this.checkUserHasWriteAccessToAllIotDevices(oldIds, req); - } + private async checkIfUpdateIsAllowed(updateDto: UpdateConnectionDto, req: AuthenticatedRequest, id: number) { + const newIotDevice = await this.iotDeviceService.findOne(updateDto.iotDeviceIds[0]); + checkIfUserHasAccessToApplication(req, newIotDevice.application.id, ApplicationAccessScope.Write); + const oldConnection = await this.service.findOne(id); + await this.checkUserHasWriteAccessToAllIotDevices(updateDto.iotDeviceIds, req); + const oldIds = oldConnection.iotDevices.map(x => x.id); + if (updateDto.iotDeviceIds != oldIds) { + await this.checkUserHasWriteAccessToAllIotDevices(oldIds, req); } + } - @Delete(":id") - @ApplicationAdmin() - @ApiNotFoundResponse({ - description: "If the id of the entity doesn't exist", - }) - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const oldConnection = await this.service.findOne(id); - await this.checkUserHasWriteAccessToAllIotDevices( - oldConnection.iotDevices.map(x => x.id), - req - ); - const result = await this.service.delete(id); - if (result.affected === 0) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - AuditLog.success(ActionType.DELETE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, id); - return new DeleteResponseDto(result.affected); - } catch (err) { - AuditLog.fail(ActionType.DELETE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, id); - throw err; - } + @Delete(":id") + @ApplicationAdmin() + @ApiNotFoundResponse({ + description: "If the id of the entity doesn't exist", + }) + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const oldConnection = await this.service.findOne(id); + await this.checkUserHasWriteAccessToAllIotDevices( + oldConnection.iotDevices.map(x => x.id), + req + ); + const result = await this.service.delete(id); + if (result.affected === 0) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + AuditLog.success(ActionType.DELETE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, id); + return new DeleteResponseDto(result.affected); + } catch (err) { + AuditLog.fail(ActionType.DELETE, IoTDevicePayloadDecoderDataTargetConnection.name, req.user.userId, id); + throw err; } + } } diff --git a/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts b/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts index 80bf0f82..8761e50f 100644 --- a/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts +++ b/src/controllers/admin-controller/iot-device-payload-decoder.controller.ts @@ -6,8 +6,8 @@ import { Read } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import { - ListAllIoTDevicesMinimalResponseDto, - PayloadDecoderIoDeviceMinimalQuery, + ListAllIoTDevicesMinimalResponseDto, + PayloadDecoderIoDeviceMinimalQuery, } from "@dto/list-all-iot-devices-minimal-response.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @@ -21,24 +21,19 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class IoTDevicePayloadDecoderController { - constructor(private iotDeviceService: IoTDeviceService) {} + constructor(private iotDeviceService: IoTDeviceService) {} - @Get(":payloadDecoderId") - @ApiOperation({ summary: "Get IoT-Devices connected to a given payload decoder" }) - async findAllByPayloadDecoder( - @Req() req: AuthenticatedRequest, - @Param("payloadDecoderId", new ParseIntPipe()) payloadDecoderId: number, - @Query() query: PayloadDecoderIoDeviceMinimalQuery - ): Promise { - try { - return await this.iotDeviceService.findAllByPayloadDecoder( - req, - payloadDecoderId, - +query.limit, - +query.offset - ); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + @Get(":payloadDecoderId") + @ApiOperation({ summary: "Get IoT-Devices connected to a given payload decoder" }) + async findAllByPayloadDecoder( + @Req() req: AuthenticatedRequest, + @Param("payloadDecoderId", new ParseIntPipe()) payloadDecoderId: number, + @Query() query: PayloadDecoderIoDeviceMinimalQuery + ): Promise { + try { + return await this.iotDeviceService.findAllByPayloadDecoder(req, payloadDecoderId, +query.limit, +query.offset); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + } } diff --git a/src/controllers/admin-controller/iot-device.controller.ts b/src/controllers/admin-controller/iot-device.controller.ts index b5ba8617..528d2592 100644 --- a/src/controllers/admin-controller/iot-device.controller.ts +++ b/src/controllers/admin-controller/iot-device.controller.ts @@ -1,27 +1,27 @@ import { - BadRequestException, - Body, - Controller, - Delete, - Get, - Header, - Logger, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Req, - StreamableFile, - UseGuards, + BadRequestException, + Body, + Controller, + Delete, + Get, + Header, + Logger, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Req, + StreamableFile, + UseGuards, } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; @@ -50,8 +50,8 @@ import { IotDeviceBatchResponseDto } from "@dto/iot-device/iot-device-batch-resp import { CreateIoTDeviceBatchDto } from "@dto/iot-device/create-iot-device-batch.dto"; import { UpdateIoTDeviceBatchDto } from "@dto/iot-device/update-iot-device-batch.dto"; import { - buildIoTDeviceCreateUpdateAuditData, - ensureUpdatePayload as ensureIoTDeviceUpdatePayload, + buildIoTDeviceCreateUpdateAuditData, + ensureUpdatePayload as ensureIoTDeviceUpdatePayload, } from "@helpers/iot-device.helper"; import { DeviceStatsResponseDto } from "@dto/chirpstack/device/device-stats.response.dto"; import { GenericHTTPDevice } from "@entities/generic-http-device.entity"; @@ -68,293 +68,293 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class IoTDeviceController { - constructor( - private iotDeviceService: IoTDeviceService, - private downlinkService: IoTDeviceDownlinkService, - private chirpstackDeviceService: ChirpstackDeviceService - ) {} + constructor( + private iotDeviceService: IoTDeviceService, + private downlinkService: IoTDeviceDownlinkService, + private chirpstackDeviceService: ChirpstackDeviceService + ) {} - private readonly logger = new Logger(IoTDeviceController.name); + private readonly logger = new Logger(IoTDeviceController.name); - @Get(":id") - @ApiOperation({ summary: "Find one IoT-Device by id" }) - @ApiNotFoundResponse() - async findOne( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise< - | IoTDevice - | LoRaWANDeviceWithChirpstackDataDto - | SigFoxDeviceWithBackendDataDto - | MQTTInternalBrokerDeviceDTO - | MQTTExternalBrokerDeviceDTO - > { - let result = undefined; - try { - result = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, true); - } catch (err) { - this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); - } + @Get(":id") + @ApiOperation({ summary: "Find one IoT-Device by id" }) + @ApiNotFoundResponse() + async findOne( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise< + | IoTDevice + | LoRaWANDeviceWithChirpstackDataDto + | SigFoxDeviceWithBackendDataDto + | MQTTInternalBrokerDeviceDTO + | MQTTExternalBrokerDeviceDTO + > { + let result = undefined; + try { + result = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, true); + } catch (err) { + this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); + } - if (!result) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + if (!result) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } - checkIfUserHasAccessToApplication(req, result.application.id, ApplicationAccessScope.Read); + checkIfUserHasAccessToApplication(req, result.application.id, ApplicationAccessScope.Read); - return result; - } + return result; + } - @Get(":id/downlink") - @ApiOperation({ summary: "Get downlink queue for a LoRaWAN/SigFox device" }) - async findDownlinkQueue( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - let device = undefined; - try { - device = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, true); - } catch (err) { - this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); - } + @Get(":id/downlink") + @ApiOperation({ summary: "Get downlink queue for a LoRaWAN/SigFox device" }) + async findDownlinkQueue( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + let device = undefined; + try { + device = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, true); + } catch (err) { + this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); + } - if (!device) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); - if (device.type === IoTDeviceType.LoRaWAN) { - return this.chirpstackDeviceService.getDownlinkQueue((device as LoRaWANDevice).deviceEUI); - } else if (device.type === IoTDeviceType.SigFox) { - return this.iotDeviceService.getDownlinkForSigfox(device as SigFoxDevice); - } else { - throw new BadRequestException(ErrorCodes.OnlyAllowedForLoRaWANAndSigfox); - } + if (!device) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); + if (device.type === IoTDeviceType.LoRaWAN) { + return this.chirpstackDeviceService.getDownlinkQueue((device as LoRaWANDevice).deviceEUI); + } else if (device.type === IoTDeviceType.SigFox) { + return this.iotDeviceService.getDownlinkForSigfox(device as SigFoxDevice); + } else { + throw new BadRequestException(ErrorCodes.OnlyAllowedForLoRaWANAndSigfox); } + } - @Get("/stats/:id") - @ApiOperation({ - summary: "Get statistics of several key values over the past period", - }) - async findStats( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - const device = await this.iotDeviceService.findOne(id); - checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); + @Get("/stats/:id") + @ApiOperation({ + summary: "Get statistics of several key values over the past period", + }) + async findStats( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + const device = await this.iotDeviceService.findOne(id); + checkIfUserHasAccessToApplication(req, device.application.id, ApplicationAccessScope.Read); - return this.iotDeviceService.findStats(device); - } + return this.iotDeviceService.findStats(device); + } - @Post() - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Create a new IoTDevice" }) - @ApiBadRequestResponse() - async create(@Req() req: AuthenticatedRequest, @Body() createDto: CreateIoTDeviceDto): Promise { - try { - checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write); - const device = await this.iotDeviceService.create(createDto, req.user.userId); - AuditLog.success(ActionType.CREATE, IoTDevice.name, req.user.userId, device.id, device.name); - return device; - } catch (err) { - AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); - this.logger.error(`Failed to create IoTDevice from dto: ${JSON.stringify(createDto)}. Error: ${err}`); - throw err; - } + @Post() + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Create a new IoTDevice" }) + @ApiBadRequestResponse() + async create(@Req() req: AuthenticatedRequest, @Body() createDto: CreateIoTDeviceDto): Promise { + try { + checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write); + const device = await this.iotDeviceService.create(createDto, req.user.userId); + AuditLog.success(ActionType.CREATE, IoTDevice.name, req.user.userId, device.id, device.name); + return device; + } catch (err) { + AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); + this.logger.error(`Failed to create IoTDevice from dto: ${JSON.stringify(createDto)}. Error: ${err}`); + throw err; } + } - @Post(":id/downlink") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Schedule downlink for SigFox or LoRaWAN device" }) - @ApiBadRequestResponse() - async createDownlink( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() dto: CreateIoTDeviceDownlinkDto - ): Promise { - try { - const device = await this.iotDeviceService.findOneWithApplicationAndMetadata(id); - if (!device) { - throw new NotFoundException(); - } - checkIfUserHasAccessToApplication(req, device?.application?.id, ApplicationAccessScope.Write); - const result = await this.downlinkService.createDownlink(dto, device); - AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); - return result; - } catch (err) { - AuditLog.fail(ActionType.CREATE, "Downlink", req.user.userId); - throw err; - } + @Post(":id/downlink") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Schedule downlink for SigFox or LoRaWAN device" }) + @ApiBadRequestResponse() + async createDownlink( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() dto: CreateIoTDeviceDownlinkDto + ): Promise { + try { + const device = await this.iotDeviceService.findOneWithApplicationAndMetadata(id); + if (!device) { + throw new NotFoundException(); + } + checkIfUserHasAccessToApplication(req, device?.application?.id, ApplicationAccessScope.Write); + const result = await this.downlinkService.createDownlink(dto, device); + AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); + return result; + } catch (err) { + AuditLog.fail(ActionType.CREATE, "Downlink", req.user.userId); + throw err; } + } - @Put(":id") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Update an existing IoT-Device" }) - @ApiBadRequestResponse() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() updateDto: UpdateIoTDeviceDto - ): Promise { - // Old application - const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, false); - try { - checkIfUserHasAccessToApplication(req, oldIotDevice.application.id, ApplicationAccessScope.Write); - if (updateDto.applicationId !== oldIotDevice.application.id) { - // New application - checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write); - } - } catch (err) { - AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); - throw err; - } - - const iotDevice = await this.iotDeviceService.update(id, updateDto, req.user.userId); - AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, iotDevice.id, iotDevice.name); - return iotDevice; + @Put(":id") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update an existing IoT-Device" }) + @ApiBadRequestResponse() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateDto: UpdateIoTDeviceDto + ): Promise { + // Old application + const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, false); + try { + checkIfUserHasAccessToApplication(req, oldIotDevice.application.id, ApplicationAccessScope.Write); + if (updateDto.applicationId !== oldIotDevice.application.id) { + // New application + checkIfUserHasAccessToApplication(req, updateDto.applicationId, ApplicationAccessScope.Write); + } + } catch (err) { + AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); + throw err; } - @Post("createMany") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Create many IoT-Devices" }) - @ApiBadRequestResponse() - async createMany( - @Req() req: AuthenticatedRequest, - @Body() createDto: CreateIoTDeviceBatchDto - ): Promise { - try { - createDto.data.forEach(createDto => - checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write) - ); + const iotDevice = await this.iotDeviceService.update(id, updateDto, req.user.userId); + AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, iotDevice.id, iotDevice.name); + return iotDevice; + } - const devices = await this.iotDeviceService.createMany(createDto.data, req.user.userId); + @Post("createMany") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Create many IoT-Devices" }) + @ApiBadRequestResponse() + async createMany( + @Req() req: AuthenticatedRequest, + @Body() createDto: CreateIoTDeviceBatchDto + ): Promise { + try { + createDto.data.forEach(createDto => + checkIfUserHasAccessToApplication(req, createDto.applicationId, ApplicationAccessScope.Write) + ); - // Iterate through the devices once, splitting it into a tuple with the data we want to log - const { deviceIds, deviceNames } = buildIoTDeviceCreateUpdateAuditData(devices); + const devices = await this.iotDeviceService.createMany(createDto.data, req.user.userId); - if (!deviceIds.length) { - AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); - } else { - AuditLog.success( - ActionType.CREATE, - IoTDevice.name, - req.user.userId, - deviceIds.join(", "), - deviceNames.join(", ") - ); - } - return devices; - } catch (err) { - AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); - this.logger.error(`Failed to create IoTDevice from dto: ${JSON.stringify(createDto)}. Error: ${err}`); - throw err; - } - } + // Iterate through the devices once, splitting it into a tuple with the data we want to log + const { deviceIds, deviceNames } = buildIoTDeviceCreateUpdateAuditData(devices); - @Post("updateMany") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Update existing IoT-Devices" }) - @ApiBadRequestResponse() - async updateMany( - @Req() req: AuthenticatedRequest, - @Body() updateDto: UpdateIoTDeviceBatchDto - ): Promise { - const oldIotDevices = await this.iotDeviceService.findManyWithApplicationAndMetadata( - updateDto.data.map(iotDevice => iotDevice.id) + if (!deviceIds.length) { + AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); + } else { + AuditLog.success( + ActionType.CREATE, + IoTDevice.name, + req.user.userId, + deviceIds.join(", "), + deviceNames.join(", ") ); - const devicesNotFound: IotDeviceBatchResponseDto[] = []; - const validDevices: typeof updateDto = { data: [] }; + } + return devices; + } catch (err) { + AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); + this.logger.error(`Failed to create IoTDevice from dto: ${JSON.stringify(createDto)}. Error: ${err}`); + throw err; + } + } - try { - validDevices.data = updateDto.data.reduce( - ensureIoTDeviceUpdatePayload(validDevices, oldIotDevices, devicesNotFound, req), - [] - ); - } catch (err) { - AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId); - throw err; - } + @Post("updateMany") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update existing IoT-Devices" }) + @ApiBadRequestResponse() + async updateMany( + @Req() req: AuthenticatedRequest, + @Body() updateDto: UpdateIoTDeviceBatchDto + ): Promise { + const oldIotDevices = await this.iotDeviceService.findManyWithApplicationAndMetadata( + updateDto.data.map(iotDevice => iotDevice.id) + ); + const devicesNotFound: IotDeviceBatchResponseDto[] = []; + const validDevices: typeof updateDto = { data: [] }; + + try { + validDevices.data = updateDto.data.reduce( + ensureIoTDeviceUpdatePayload(validDevices, oldIotDevices, devicesNotFound, req), + [] + ); + } catch (err) { + AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId); + throw err; + } - const response = validDevices.data.length - ? await this.iotDeviceService.updateMany(validDevices, req.user.userId) - : []; - response.push(...devicesNotFound); + const response = validDevices.data.length + ? await this.iotDeviceService.updateMany(validDevices, req.user.userId) + : []; + response.push(...devicesNotFound); - const { deviceIds, deviceNames } = buildIoTDeviceCreateUpdateAuditData(response); + const { deviceIds, deviceNames } = buildIoTDeviceCreateUpdateAuditData(response); - if (!deviceIds.length) { - AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); - } else { - AuditLog.success( - ActionType.CREATE, - IoTDevice.name, - req.user.userId, - deviceIds.join(", "), - deviceNames.join(", ") - ); - } - return response; + if (!deviceIds.length) { + AuditLog.fail(ActionType.CREATE, IoTDevice.name, req.user.userId); + } else { + AuditLog.success( + ActionType.CREATE, + IoTDevice.name, + req.user.userId, + deviceIds.join(", "), + deviceNames.join(", ") + ); } + return response; + } - @Delete(":id") - @ApiOperation({ summary: "Delete an existing IoT-Device" }) - @ApiBadRequestResponse() - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, false); - checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write); - const result = await this.iotDeviceService.delete(oldIotDevice); - AuditLog.success(ActionType.DELETE, IoTDevice.name, req.user.userId, id); - return new DeleteResponseDto(result.affected); - } catch (err) { - AuditLog.fail(ActionType.DELETE, IoTDevice.name, req.user.userId, id); - throw err; - } + @Delete(":id") + @ApiOperation({ summary: "Delete an existing IoT-Device" }) + @ApiBadRequestResponse() + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const oldIotDevice = await this.iotDeviceService.findOneWithApplicationAndMetadata(id, false); + checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write); + const result = await this.iotDeviceService.delete(oldIotDevice); + AuditLog.success(ActionType.DELETE, IoTDevice.name, req.user.userId, id); + return new DeleteResponseDto(result.affected); + } catch (err) { + AuditLog.fail(ActionType.DELETE, IoTDevice.name, req.user.userId, id); + throw err; } + } - @Put("resetHttpDeviceApiKey/:id") - @ApiOperation({ summary: "Reset the API key of a generic HTTP device" }) - @ApiBadRequestResponse() - async resetHttpDeviceApiKey( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise> { - try { - const oldIotDevice = await this.iotDeviceService.findOne(id); - checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write); + @Put("resetHttpDeviceApiKey/:id") + @ApiOperation({ summary: "Reset the API key of a generic HTTP device" }) + @ApiBadRequestResponse() + async resetHttpDeviceApiKey( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise> { + try { + const oldIotDevice = await this.iotDeviceService.findOne(id); + checkIfUserHasAccessToApplication(req, oldIotDevice?.application?.id, ApplicationAccessScope.Write); - if (oldIotDevice.type !== IoTDeviceType.GenericHttp) { - throw new BadRequestException("The requested device is not a generic HTTP device"); - } + if (oldIotDevice.type !== IoTDeviceType.GenericHttp) { + throw new BadRequestException("The requested device is not a generic HTTP device"); + } - const result = await this.iotDeviceService.resetHttpDeviceApiKey(oldIotDevice as GenericHTTPDevice); - AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); - return { - apiKey: result.apiKey, - }; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); - throw err; - } + const result = await this.iotDeviceService.resetHttpDeviceApiKey(oldIotDevice as GenericHTTPDevice); + AuditLog.success(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); + return { + apiKey: result.apiKey, + }; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, IoTDevice.name, req.user.userId, id); + throw err; } + } - @Get("getDevicesMetadataCsv/:applicationId") - @ApiOperation({ - summary: "Get csv containing metadata for all devices in an application", - }) - @ApiBadRequestResponse() - async getDevicesMetadataCsv( - @Req() req: AuthenticatedRequest, - @Param("applicationId", new ParseIntPipe()) applicationId: number - ): Promise { - try { - checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); - const csvFile = await this.iotDeviceService.getDevicesMetadataCsv(applicationId); - return new StreamableFile(csvFile); - } catch (err) { - this.logger.error(err); - } + @Get("getDevicesMetadataCsv/:applicationId") + @ApiOperation({ + summary: "Get csv containing metadata for all devices in an application", + }) + @ApiBadRequestResponse() + async getDevicesMetadataCsv( + @Req() req: AuthenticatedRequest, + @Param("applicationId", new ParseIntPipe()) applicationId: number + ): Promise { + try { + checkIfUserHasAccessToApplication(req, applicationId, ApplicationAccessScope.Read); + const csvFile = await this.iotDeviceService.getDevicesMetadataCsv(applicationId); + return new StreamableFile(csvFile); + } catch (err) { + this.logger.error(err); } + } } diff --git a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts index 3e3cefe7..08ad570e 100644 --- a/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts +++ b/src/controllers/admin-controller/lorawan/lorawan-gateway.controller.ts @@ -2,8 +2,8 @@ import { ComposeAuthGuard } from "@auth/compose-auth.guard"; import { Read } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { - GatewayGetAllStatusResponseDto, - ListAllGatewayStatusDto, + GatewayGetAllStatusResponseDto, + ListAllGatewayStatusDto, } from "@dto/chirpstack/backend/gateway-all-status.dto"; import { GatewayStatus, GetGatewayStatusQuery } from "@dto/chirpstack/backend/gateway-status.dto"; import { Controller, Get, Param, Query, UseGuards } from "@nestjs/common"; @@ -17,26 +17,26 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @UseGuards(ComposeAuthGuard, RolesGuard) @ApiAuth() export class LoRaWANGatewayController { - constructor( - private statusHistoryService: GatewayStatusHistoryService, - private chirpstackGatewayService: ChirpstackGatewayService - ) {} + constructor( + private statusHistoryService: GatewayStatusHistoryService, + private chirpstackGatewayService: ChirpstackGatewayService + ) {} - @Get("/status") - @ApiProduces("application/json") - @ApiOperation({ summary: "Get the status for all LoRaWAN gateways" }) - @Read() - async getAllStatus(@Query() query: ListAllGatewayStatusDto): Promise { - // Currently, everyone is allowed to get the status - return this.statusHistoryService.findAllWithChirpstack(query); - } + @Get("/status") + @ApiProduces("application/json") + @ApiOperation({ summary: "Get the status for all LoRaWAN gateways" }) + @Read() + async getAllStatus(@Query() query: ListAllGatewayStatusDto): Promise { + // Currently, everyone is allowed to get the status + return this.statusHistoryService.findAllWithChirpstack(query); + } - @Get("/status/:id") - @ApiProduces("application/json") - @ApiOperation({ summary: "Get the status for a LoRaWAN gateway" }) - async getStatus(@Param("id") id: string, @Query() query: GetGatewayStatusQuery): Promise { - // Currently, everyone is allowed to get the status - const gatewayDto = await this.chirpstackGatewayService.getOne(id); - return this.statusHistoryService.findOne(gatewayDto.gateway, query.timeInterval); - } + @Get("/status/:id") + @ApiProduces("application/json") + @ApiOperation({ summary: "Get the status for a LoRaWAN gateway" }) + async getStatus(@Param("id") id: string, @Query() query: GetGatewayStatusQuery): Promise { + // Currently, everyone is allowed to get the status + const gatewayDto = await this.chirpstackGatewayService.getOne(id); + return this.statusHistoryService.findOne(gatewayDto.gateway, query.timeInterval); + } } diff --git a/src/controllers/admin-controller/multicast.controller.ts b/src/controllers/admin-controller/multicast.controller.ts index e64f0642..cc729332 100644 --- a/src/controllers/admin-controller/multicast.controller.ts +++ b/src/controllers/admin-controller/multicast.controller.ts @@ -1,28 +1,28 @@ import { - Body, - Controller, - Delete, - Get, - Header, - Logger, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UnauthorizedException, - UseGuards, + Body, + Controller, + Delete, + Get, + Header, + Logger, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UnauthorizedException, + UseGuards, } from "@nestjs/common"; import { CreateMulticastDto } from "../../entities/dto/create-multicast.dto"; import { UpdateMulticastDto } from "../../entities/dto/update-multicast.dto"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { Multicast } from "@entities/multicast.entity"; @@ -50,165 +50,165 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiUnauthorizedResponse() @Controller("multicast") export class MulticastController { - constructor(private readonly multicastService: MulticastService) {} - private readonly logger = new Logger(MulticastController.name); + constructor(private readonly multicastService: MulticastService) {} + private readonly logger = new Logger(MulticastController.name); - @Post() - @ApiOperation({ summary: "Create a new multicast" }) - @ApiBadRequestResponse() - async create(@Req() req: AuthenticatedRequest, @Body() createMulticastDto: CreateMulticastDto): Promise { - try { - checkIfUserHasAccessToApplication(req, createMulticastDto.applicationID, ApplicationAccessScope.Write); - const multicast = await this.multicastService.create(createMulticastDto, req.user.userId); - AuditLog.success(ActionType.CREATE, Multicast.name, req.user.userId, multicast.id, multicast.groupName); - return multicast; - } catch (err) { - AuditLog.fail(ActionType.CREATE, Multicast.name, req.user.userId); - throw err; - } + @Post() + @ApiOperation({ summary: "Create a new multicast" }) + @ApiBadRequestResponse() + async create(@Req() req: AuthenticatedRequest, @Body() createMulticastDto: CreateMulticastDto): Promise { + try { + checkIfUserHasAccessToApplication(req, createMulticastDto.applicationID, ApplicationAccessScope.Write); + const multicast = await this.multicastService.create(createMulticastDto, req.user.userId); + AuditLog.success(ActionType.CREATE, Multicast.name, req.user.userId, multicast.id, multicast.groupName); + return multicast; + } catch (err) { + AuditLog.fail(ActionType.CREATE, Multicast.name, req.user.userId); + throw err; } + } - @Get() - @ApiOperation({ summary: "Find all Multicasts" }) - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllMulticastsDto - ): Promise { - const applicationId = +query.applicationId; - if (req.user.permissions.isGlobalAdmin) { - return await this.multicastService.findAndCountAllWithPagination(query); - } else { - if (query.applicationId) { - query.applicationId = applicationId; - } + @Get() + @ApiOperation({ summary: "Find all Multicasts" }) + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllMulticastsDto + ): Promise { + const applicationId = +query.applicationId; + if (req.user.permissions.isGlobalAdmin) { + return await this.multicastService.findAndCountAllWithPagination(query); + } else { + if (query.applicationId) { + query.applicationId = applicationId; + } - const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); - if (applicationId && !allowed.some(x => x === applicationId)) { - throw new UnauthorizedException(); - } + const allowed = req.user.permissions.getAllApplicationsWithAtLeastRead(); + if (applicationId && !allowed.some(x => x === applicationId)) { + throw new UnauthorizedException(); + } - return await this.multicastService.findAndCountAllWithPagination(query, allowed); - } + return await this.multicastService.findAndCountAllWithPagination(query, allowed); } + } - @Get(":id") - @ApiOperation({ summary: "Find Multicast by id" }) - async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { - try { - const multicast = await this.multicastService.findOne(id); - checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Read); - return multicast; - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + @Get(":id") + @ApiOperation({ summary: "Find Multicast by id" }) + async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + try { + const multicast = await this.multicastService.findOne(id); + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Read); + return multicast; + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + } - @Get(":id/downlink-multicast") - @ApiOperation({ summary: "Get downlink queue for multicast" }) - async findMulticastDownlinkQueue( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - let multicast = undefined; - try { - multicast = await this.multicastService.findOne(id); - } catch (err) { - this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); - } - - if (!multicast) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Read); - - return this.multicastService.getDownlinkQueue(multicast.lorawanMulticastDefinition.chirpstackGroupId); + @Get(":id/downlink-multicast") + @ApiOperation({ summary: "Get downlink queue for multicast" }) + async findMulticastDownlinkQueue( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + let multicast = undefined; + try { + multicast = await this.multicastService.findOne(id); + } catch (err) { + this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); } - @Post(":id/downlink-multicast") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Schedule downlink multicast" }) - @ApiBadRequestResponse() - async createDownlink( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() dto: CreateMulticastDownlinkDto - ): Promise { - try { - const multicast = await this.multicastService.findOne(id); - if (!multicast) { - throw new NotFoundException(); - } - checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Write); - const result = await this.multicastService.createDownlink(dto, multicast); - AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); - return result; - } catch (err) { - AuditLog.fail(ActionType.CREATE, "Downlink", req.user.userId); - throw err; - } + if (!multicast) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Read); - @Put(":id") - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Update an existing Multicast" }) - @ApiBadRequestResponse() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() updateDto: UpdateMulticastDto - ): Promise { - const oldMulticast = await this.multicastService.findOne(id); - try { - checkIfUserHasAccessToApplication(req, oldMulticast.application.id, ApplicationAccessScope.Write); - if (oldMulticast.application.id !== updateDto.applicationID) { - checkIfUserHasAccessToApplication(req, updateDto.applicationID, ApplicationAccessScope.Write); - } - } catch (err) { - AuditLog.fail( - ActionType.UPDATE, - Multicast.name, - req.user.userId, - oldMulticast.lorawanMulticastDefinition.chirpstackGroupId, - oldMulticast.groupName - ); - throw err; - } + return this.multicastService.getDownlinkQueue(multicast.lorawanMulticastDefinition.chirpstackGroupId); + } - const multicast = await this.multicastService.update(oldMulticast, updateDto, req.user.userId); - AuditLog.success( - ActionType.UPDATE, - Multicast.name, - req.user.userId, - multicast.lorawanMulticastDefinition.chirpstackGroupId, - multicast.groupName - ); - return multicast; + @Post(":id/downlink-multicast") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Schedule downlink multicast" }) + @ApiBadRequestResponse() + async createDownlink( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() dto: CreateMulticastDownlinkDto + ): Promise { + try { + const multicast = await this.multicastService.findOne(id); + if (!multicast) { + throw new NotFoundException(); + } + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Write); + const result = await this.multicastService.createDownlink(dto, multicast); + AuditLog.success(ActionType.CREATE, "Downlink", req.user.userId); + return result; + } catch (err) { + AuditLog.fail(ActionType.CREATE, "Downlink", req.user.userId); + throw err; } + } + + @Put(":id") + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update an existing Multicast" }) + @ApiBadRequestResponse() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateDto: UpdateMulticastDto + ): Promise { + const oldMulticast = await this.multicastService.findOne(id); + try { + checkIfUserHasAccessToApplication(req, oldMulticast.application.id, ApplicationAccessScope.Write); + if (oldMulticast.application.id !== updateDto.applicationID) { + checkIfUserHasAccessToApplication(req, updateDto.applicationID, ApplicationAccessScope.Write); + } + } catch (err) { + AuditLog.fail( + ActionType.UPDATE, + Multicast.name, + req.user.userId, + oldMulticast.lorawanMulticastDefinition.chirpstackGroupId, + oldMulticast.groupName + ); + throw err; + } + + const multicast = await this.multicastService.update(oldMulticast, updateDto, req.user.userId); + AuditLog.success( + ActionType.UPDATE, + Multicast.name, + req.user.userId, + multicast.lorawanMulticastDefinition.chirpstackGroupId, + multicast.groupName + ); + return multicast; + } - @Delete(":id") - @ApiOperation({ summary: "Delete an existing multicast" }) - @ApiBadRequestResponse() - @ApplicationAdmin() - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const multicast = await this.multicastService.findOne(id); - checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Write); - const result = await this.multicastService.deleteMulticast(id, multicast); + @Delete(":id") + @ApiOperation({ summary: "Delete an existing multicast" }) + @ApiBadRequestResponse() + @ApplicationAdmin() + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const multicast = await this.multicastService.findOne(id); + checkIfUserHasAccessToApplication(req, multicast.application.id, ApplicationAccessScope.Write); + const result = await this.multicastService.deleteMulticast(id, multicast); - if (result.affected === 0) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - AuditLog.success(ActionType.DELETE, Multicast.name, req.user.userId, id); - return new DeleteResponseDto(result.affected); - } catch (err) { - AuditLog.fail(ActionType.DELETE, Multicast.name, req.user.userId, id); - if (err?.status === 403) { - throw err; - } - throw new NotFoundException(err); - } + if (result.affected === 0) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + AuditLog.success(ActionType.DELETE, Multicast.name, req.user.userId, id); + return new DeleteResponseDto(result.affected); + } catch (err) { + AuditLog.fail(ActionType.DELETE, Multicast.name, req.user.userId, id); + if (err?.status === 403) { + throw err; + } + throw new NotFoundException(err); } + } } diff --git a/src/controllers/admin-controller/open-data-dk-sharing.controller.ts b/src/controllers/admin-controller/open-data-dk-sharing.controller.ts index 8cb458ab..67e5e17c 100644 --- a/src/controllers/admin-controller/open-data-dk-sharing.controller.ts +++ b/src/controllers/admin-controller/open-data-dk-sharing.controller.ts @@ -8,49 +8,38 @@ import { OrganizationService } from "@services/user-management/organization.serv @ApiTags("OpenData.dk") @Controller("open-data-dk-sharing") export class OpenDataDkSharingController { - constructor( - private service: OpenDataDkSharingService, - private organizationService: OrganizationService - ) {} + constructor(private service: OpenDataDkSharingService, private organizationService: OrganizationService) {} - @Get(":organizationId") - async getCatalog( - @Param("organizationId", new ParseIntPipe()) orgId: number - ): Promise { - let organization; - try { - organization = await this.organizationService.findById(orgId); - } catch (err) { - throw new NotFoundException( - `Could not find an organization with the id: ${orgId}` - ); - } - - return this.service.createDCAT(organization); + @Get(":organizationId") + async getCatalog(@Param("organizationId", new ParseIntPipe()) orgId: number): Promise { + let organization; + try { + organization = await this.organizationService.findById(orgId); + } catch (err) { + throw new NotFoundException(`Could not find an organization with the id: ${orgId}`); } - @Get(":organizationId/data/:shareId") - async getData( - @Param("organizationId", new ParseIntPipe()) orgId: number, - @Param("shareId", new ParseIntPipe()) shareId: number - ): Promise { - let organization; - try { - organization = await this.organizationService.findById(orgId); - } catch (err) { - throw new NotFoundException( - `Could not find an organization with the id: ${orgId}` - ); - } + return this.service.createDCAT(organization); + } - const dataset = await this.service.findById(shareId, organization.id); - if (!dataset) { - throw new NotFoundException( - `Could not find dataset with id: ${shareId} on organization: ${organization.id}` - ); - } + @Get(":organizationId/data/:shareId") + async getData( + @Param("organizationId", new ParseIntPipe()) orgId: number, + @Param("shareId", new ParseIntPipe()) shareId: number + ): Promise { + let organization; + try { + organization = await this.organizationService.findById(orgId); + } catch (err) { + throw new NotFoundException(`Could not find an organization with the id: ${orgId}`); + } - // TODO: Add caching pr. shareId (https://docs.nestjs.com/techniques/caching) - return this.service.getDecodedDataInDataset(dataset); + const dataset = await this.service.findById(shareId, organization.id); + if (!dataset) { + throw new NotFoundException(`Could not find dataset with id: ${shareId} on organization: ${organization.id}`); } + + // TODO: Add caching pr. shareId (https://docs.nestjs.com/techniques/caching) + return this.service.getDecodedDataInDataset(dataset); + } } diff --git a/src/controllers/admin-controller/payload-decoder.controller.ts b/src/controllers/admin-controller/payload-decoder.controller.ts index a82e013e..010deb0a 100644 --- a/src/controllers/admin-controller/payload-decoder.controller.ts +++ b/src/controllers/admin-controller/payload-decoder.controller.ts @@ -1,27 +1,27 @@ import { - BadRequestException, - Body, - Controller, - Delete, - Get, - Header, - Logger, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Delete, + Get, + Header, + Logger, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; @@ -49,135 +49,112 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class PayloadDecoderController { - constructor(private payloadDecoderService: PayloadDecoderService) {} + constructor(private payloadDecoderService: PayloadDecoderService) {} - private readonly logger = new Logger(PayloadDecoderController.name); + private readonly logger = new Logger(PayloadDecoderController.name); - @Get(":id") - @ApiOperation({ summary: "Find one Payload Decoder by id" }) - @ApiNotFoundResponse() - async findOne( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - let result = undefined; - try { - result = await this.payloadDecoderService.findOne(id); - } catch (err) { - this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); - } - - if (!result) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - return result; + @Get(":id") + @ApiOperation({ summary: "Find one Payload Decoder by id" }) + @ApiNotFoundResponse() + async findOne( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + let result = undefined; + try { + result = await this.payloadDecoderService.findOne(id); + } catch (err) { + this.logger.error(`Error occured during findOne: '${JSON.stringify(err)}'`); } - @Get() - @ApiOperation({ summary: "Find all Payload Decoders" }) - @ApiNotFoundResponse() - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllPayloadDecoderDto - ): Promise { - return await this.payloadDecoderService.findAndCountWithPagination(query, query.organizationId); + if (!result) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + return result; + } + + @Get() + @ApiOperation({ summary: "Find all Payload Decoders" }) + @ApiNotFoundResponse() + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllPayloadDecoderDto + ): Promise { + return await this.payloadDecoderService.findAndCountWithPagination(query, query.organizationId); + } - @Post() - @ApplicationAdmin() - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Create a new Payload Decoder" }) - @ApiBadRequestResponse() - async create( - @Req() req: AuthenticatedRequest, - @Body() createDto: CreatePayloadDecoderDto - ): Promise { - try { - checkIfUserHasAccessToOrganization(req, createDto.organizationId, OrganizationAccessScope.ApplicationWrite); + @Post() + @ApplicationAdmin() + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Create a new Payload Decoder" }) + @ApiBadRequestResponse() + async create(@Req() req: AuthenticatedRequest, @Body() createDto: CreatePayloadDecoderDto): Promise { + try { + checkIfUserHasAccessToOrganization(req, createDto.organizationId, OrganizationAccessScope.ApplicationWrite); - // TODO: Valider at funktionen er gyldig - const payloadDecoder = await this.payloadDecoderService.create(createDto, req.user.userId); - AuditLog.success( - ActionType.CREATE, - PayloadDecoder.name, - req.user.userId, - payloadDecoder.id, - payloadDecoder.name - ); - return payloadDecoder; - } catch (err) { - AuditLog.fail(ActionType.CREATE, PayloadDecoder.name, req.user.userId); - throw err; - } + // TODO: Valider at funktionen er gyldig + const payloadDecoder = await this.payloadDecoderService.create(createDto, req.user.userId); + AuditLog.success(ActionType.CREATE, PayloadDecoder.name, req.user.userId, payloadDecoder.id, payloadDecoder.name); + return payloadDecoder; + } catch (err) { + AuditLog.fail(ActionType.CREATE, PayloadDecoder.name, req.user.userId); + throw err; } + } - @Put(":id") - @ApplicationAdmin() - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Update an existing Payload Decoder" }) - @ApiBadRequestResponse() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() updateDto: UpdatePayloadDecoderDto - ): Promise { - try { - checkIfUserHasAccessToOrganization(req, updateDto.organizationId, OrganizationAccessScope.ApplicationWrite); - const oldDecoder = await this.payloadDecoderService.findOne(id); - if (oldDecoder?.organization?.id) { - checkIfUserHasAccessToOrganization( - req, - oldDecoder.organization.id, - OrganizationAccessScope.ApplicationWrite - ); - } - // TODO: Valider at funktionen er gyldig - const payloadDecoder = await this.payloadDecoderService.update(id, updateDto, req.user.userId); - AuditLog.success( - ActionType.UPDATE, - PayloadDecoder.name, - req.user.userId, - payloadDecoder.id, - payloadDecoder.name - ); + @Put(":id") + @ApplicationAdmin() + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Update an existing Payload Decoder" }) + @ApiBadRequestResponse() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateDto: UpdatePayloadDecoderDto + ): Promise { + try { + checkIfUserHasAccessToOrganization(req, updateDto.organizationId, OrganizationAccessScope.ApplicationWrite); + const oldDecoder = await this.payloadDecoderService.findOne(id); + if (oldDecoder?.organization?.id) { + checkIfUserHasAccessToOrganization(req, oldDecoder.organization.id, OrganizationAccessScope.ApplicationWrite); + } + // TODO: Valider at funktionen er gyldig + const payloadDecoder = await this.payloadDecoderService.update(id, updateDto, req.user.userId); + AuditLog.success(ActionType.UPDATE, PayloadDecoder.name, req.user.userId, payloadDecoder.id, payloadDecoder.name); - return payloadDecoder; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, PayloadDecoder.name, req.user.userId); - throw err; - } + return payloadDecoder; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, PayloadDecoder.name, req.user.userId); + throw err; } + } - @Delete(":id") - @ApplicationAdmin() - @ApiOperation({ summary: "Delete an existing Payload Decoder" }) - @ApiNotFoundResponse() - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const oldDecoder = await this.payloadDecoderService.findOne(id); - if (oldDecoder?.organization?.id) { - checkIfUserHasAccessToOrganization( - req, - oldDecoder.organization.id, - OrganizationAccessScope.ApplicationWrite - ); - } + @Delete(":id") + @ApplicationAdmin() + @ApiOperation({ summary: "Delete an existing Payload Decoder" }) + @ApiNotFoundResponse() + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const oldDecoder = await this.payloadDecoderService.findOne(id); + if (oldDecoder?.organization?.id) { + checkIfUserHasAccessToOrganization(req, oldDecoder.organization.id, OrganizationAccessScope.ApplicationWrite); + } - const result = await this.payloadDecoderService.delete(id); - if (result.affected == 0) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - AuditLog.success(ActionType.DELETE, PayloadDecoder.name, req.user.userId, id); - return new DeleteResponseDto(result.affected); - } catch (err) { - AuditLog.fail(ActionType.DELETE, PayloadDecoder.name, req.user.userId); - if (err?.name == "QueryFailedError") { - throw new BadRequestException(ErrorCodes.DeleteNotAllowedItemIsInUse); - } - throw new NotFoundException(err); - } + const result = await this.payloadDecoderService.delete(id); + if (result.affected == 0) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + AuditLog.success(ActionType.DELETE, PayloadDecoder.name, req.user.userId, id); + return new DeleteResponseDto(result.affected); + } catch (err) { + AuditLog.fail(ActionType.DELETE, PayloadDecoder.name, req.user.userId); + if (err?.name == "QueryFailedError") { + throw new BadRequestException(ErrorCodes.DeleteNotAllowedItemIsInUse); + } + throw new NotFoundException(err); } + } } diff --git a/src/controllers/admin-controller/search.controller.ts b/src/controllers/admin-controller/search.controller.ts index 04433928..d23c8611 100644 --- a/src/controllers/admin-controller/search.controller.ts +++ b/src/controllers/admin-controller/search.controller.ts @@ -19,27 +19,27 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiForbiddenResponse() @ApiUnauthorizedResponse() export class SearchController { - constructor(private service: SearchService) {} + constructor(private service: SearchService) {} - private readonly logger = new Logger(SearchController.name); + private readonly logger = new Logger(SearchController.name); - @Get() - @ApiOperation({ - summary: - "Search for " + - Object.values(SearchResultType) - .filter(x => !isNumber(x)) - .join(", "), - }) - async search( - @Req() req: AuthenticatedRequest, - @Query("q") query?: string, - @Query("limit", new ParseIntPipe()) limit?: number, - @Query("offset", new ParseIntPipe()) offset?: number - ): Promise { - if (query == null || query.trim() === "") { - throw new BadRequestException(ErrorCodes.QueryMustNotBeEmpty); - } - return await this.service.findByQuery(req, query, limit, offset); + @Get() + @ApiOperation({ + summary: + "Search for " + + Object.values(SearchResultType) + .filter(x => !isNumber(x)) + .join(", "), + }) + async search( + @Req() req: AuthenticatedRequest, + @Query("q") query?: string, + @Query("limit", new ParseIntPipe()) limit?: number, + @Query("offset", new ParseIntPipe()) offset?: number + ): Promise { + if (query == null || query.trim() === "") { + throw new BadRequestException(ErrorCodes.QueryMustNotBeEmpty); } + return await this.service.findByQuery(req, query, limit, offset); + } } diff --git a/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts index faa4a972..afe5669e 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-api-contract.controller.ts @@ -18,18 +18,18 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @Read() @ApiForbiddenResponse() export class SigFoxApiContractController { - constructor(private service: SigFoxApiContractService, private sigfoxGroupService: SigFoxGroupService) {} + constructor(private service: SigFoxApiContractService, private sigfoxGroupService: SigFoxGroupService) {} - @Get() - @ApiProduces("application/json") - @ApiOperation({ summary: "List all SigFox Contracts for a SigFox Group" }) - async getAll( - @Req() req: AuthenticatedRequest, - @Query("groupId", new ParseIntPipe()) groupId: number - ): Promise { - const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); - checkIfUserHasAccessToOrganization(req, group?.belongsTo?.id, OrganizationAccessScope.ApplicationRead); + @Get() + @ApiProduces("application/json") + @ApiOperation({ summary: "List all SigFox Contracts for a SigFox Group" }) + async getAll( + @Req() req: AuthenticatedRequest, + @Query("groupId", new ParseIntPipe()) groupId: number + ): Promise { + const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); + checkIfUserHasAccessToOrganization(req, group?.belongsTo?.id, OrganizationAccessScope.ApplicationRead); - return await this.service.getContractInfos(group); - } + return await this.service.getContractInfos(group); + } } diff --git a/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts index 78bc28a3..2dbda7ef 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-api-device.controller.ts @@ -18,20 +18,20 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @Read() @ApiForbiddenResponse() export class SigFoxApiDeviceController { - constructor(private sigfoxGroupService: SigFoxGroupService, private iotDeviceService: IoTDeviceService) {} + constructor(private sigfoxGroupService: SigFoxGroupService, private iotDeviceService: IoTDeviceService) {} - @Get() - @ApiProduces("application/json") - @ApiOperation({ - summary: "List all SigFox Devices for a SigFox Group, that are not already created in OS2IoT", - }) - async getAll( - @Req() req: AuthenticatedRequest, - @Query("groupId", new ParseIntPipe()) groupId: number - ): Promise { - const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); - checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); + @Get() + @ApiProduces("application/json") + @ApiOperation({ + summary: "List all SigFox Devices for a SigFox Group, that are not already created in OS2IoT", + }) + async getAll( + @Req() req: AuthenticatedRequest, + @Query("groupId", new ParseIntPipe()) groupId: number + ): Promise { + const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); - return await this.iotDeviceService.getAllSigfoxDevicesByGroup(group, true); - } + return await this.iotDeviceService.getAllSigfoxDevicesByGroup(group, true); + } } diff --git a/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts index 3a5578cd..d8107514 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-device-type.controller.ts @@ -4,8 +4,8 @@ import { RolesGuard } from "@auth/roles.guard"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { CreateSigFoxApiDeviceTypeRequestDto } from "@dto/sigfox/external/create-sigfox-api-device-type-request.dto"; import { - SigFoxApiDeviceTypeContent, - SigFoxApiDeviceTypeResponse, + SigFoxApiDeviceTypeContent, + SigFoxApiDeviceTypeResponse, } from "@dto/sigfox/external/sigfox-api-device-type-response.dto"; import { SigFoxApiIdReferenceDto } from "@dto/sigfox/external/sigfox-api-id-reference.dto"; import { UpdateSigFoxApiDeviceTypeRequestDto } from "@dto/sigfox/external/update-sigfox-api-device-type-request.dto"; @@ -14,13 +14,13 @@ import { SigFoxGroup } from "@entities/sigfox-group.entity"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { Body, Controller, Get, HttpCode, Param, ParseIntPipe, Post, Put, Query, Req, UseGuards } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiCreatedResponse, - ApiForbiddenResponse, - ApiNoContentResponse, - ApiOperation, - ApiProduces, - ApiTags, + ApiBadRequestResponse, + ApiCreatedResponse, + ApiForbiddenResponse, + ApiNoContentResponse, + ApiOperation, + ApiProduces, + ApiTags, } from "@nestjs/swagger"; import { AuditLog } from "@services/audit-log.service"; import { SigFoxApiDeviceTypeService } from "@services/sigfox/sigfox-api-device-type.service"; @@ -35,80 +35,80 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @Read() @ApiForbiddenResponse() export class SigfoxDeviceTypeController { - constructor( - private service: SigFoxApiDeviceTypeService, - private usersService: SigfoxApiUsersService, - private sigfoxGroupService: SigFoxGroupService - ) {} + constructor( + private service: SigFoxApiDeviceTypeService, + private usersService: SigfoxApiUsersService, + private sigfoxGroupService: SigFoxGroupService + ) {} - @Get() - @ApiProduces("application/json") - @ApiOperation({ summary: "List all SigFox Device Types for a SigFox Group" }) - async getAll( - @Req() req: AuthenticatedRequest, - @Query("groupId", new ParseIntPipe()) groupId: number - ): Promise { - const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); - checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); - const sigfoxApiGroup = await this.usersService.getByUserId(group.username, group); + @Get() + @ApiProduces("application/json") + @ApiOperation({ summary: "List all SigFox Device Types for a SigFox Group" }) + async getAll( + @Req() req: AuthenticatedRequest, + @Query("groupId", new ParseIntPipe()) groupId: number + ): Promise { + const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); + const sigfoxApiGroup = await this.usersService.getByUserId(group.username, group); - return await this.service.getAllByGroupIds(group, [sigfoxApiGroup.group.id]); - } + return await this.service.getAllByGroupIds(group, [sigfoxApiGroup.group.id]); + } - @Get(":id") - @ApiProduces("application/json") - @ApiOperation({ summary: "List one SigFox Device Type" }) - async getOne( - @Req() req: AuthenticatedRequest, - @Param("id") id: string, - @Query("groupId", new ParseIntPipe()) groupId: number - ): Promise { - const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); - checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); + @Get(":id") + @ApiProduces("application/json") + @ApiOperation({ summary: "List one SigFox Device Type" }) + async getOne( + @Req() req: AuthenticatedRequest, + @Param("id") id: string, + @Query("groupId", new ParseIntPipe()) groupId: number + ): Promise { + const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); - return await this.service.getById(group, id); - } + return await this.service.getById(group, id); + } - @Post() - @ApplicationAdmin() - @ApiCreatedResponse() - @ApiBadRequestResponse() - async create( - @Req() req: AuthenticatedRequest, - @Body() dto: CreateSigFoxApiDeviceTypeRequestDto, - @Query("groupId", new ParseIntPipe()) groupId: number - ): Promise { - try { - const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); - checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); - const res = await this.service.create(group, dto); - AuditLog.success(ActionType.CREATE, "SigfoxDeviceType", req.user.userId, res.id, dto.name); - return res; - } catch (err) { - AuditLog.fail(ActionType.CREATE, "SigfoxDeviceType", req.user.userId); - throw err; - } + @Post() + @ApplicationAdmin() + @ApiCreatedResponse() + @ApiBadRequestResponse() + async create( + @Req() req: AuthenticatedRequest, + @Body() dto: CreateSigFoxApiDeviceTypeRequestDto, + @Query("groupId", new ParseIntPipe()) groupId: number + ): Promise { + try { + const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); + const res = await this.service.create(group, dto); + AuditLog.success(ActionType.CREATE, "SigfoxDeviceType", req.user.userId, res.id, dto.name); + return res; + } catch (err) { + AuditLog.fail(ActionType.CREATE, "SigfoxDeviceType", req.user.userId); + throw err; } + } - @Put(":id") - @ApplicationAdmin() - @ApiNoContentResponse() - @ApiBadRequestResponse() - @HttpCode(204) - async update( - @Req() req: AuthenticatedRequest, - @Param("id") id: string, - @Body() dto: UpdateSigFoxApiDeviceTypeRequestDto, - @Query("groupId", new ParseIntPipe()) groupId: number - ): Promise { - try { - const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); - checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); - await this.service.update(group, id, dto); - AuditLog.success(ActionType.UPDATE, "SigfoxDeviceType", req.user.userId, id, dto.name); - } catch (err) { - AuditLog.fail(ActionType.UPDATE, "SigfoxDeviceType", req.user.userId, id, dto.name); - throw err; - } + @Put(":id") + @ApplicationAdmin() + @ApiNoContentResponse() + @ApiBadRequestResponse() + @HttpCode(204) + async update( + @Req() req: AuthenticatedRequest, + @Param("id") id: string, + @Body() dto: UpdateSigFoxApiDeviceTypeRequestDto, + @Query("groupId", new ParseIntPipe()) groupId: number + ): Promise { + try { + const group: SigFoxGroup = await this.sigfoxGroupService.findOneWithPassword(groupId); + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); + await this.service.update(group, id, dto); + AuditLog.success(ActionType.UPDATE, "SigfoxDeviceType", req.user.userId, id, dto.name); + } catch (err) { + AuditLog.fail(ActionType.UPDATE, "SigfoxDeviceType", req.user.userId, id, dto.name); + throw err; } + } } diff --git a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts index 7fdf6e90..18b54127 100644 --- a/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts +++ b/src/controllers/admin-controller/sigfox/sigfox-group.controller.ts @@ -1,25 +1,25 @@ import { - BadRequestException, - Body, - Controller, - Get, - HttpCode, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Get, + HttpCode, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiCreatedResponse, - ApiForbiddenResponse, - ApiOkResponse, - ApiOperation, - ApiProduces, - ApiTags, + ApiCreatedResponse, + ApiForbiddenResponse, + ApiOkResponse, + ApiOperation, + ApiProduces, + ApiTags, } from "@nestjs/swagger"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; @@ -47,106 +47,106 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @Read() @ApiForbiddenResponse() export class SigfoxGroupController { - constructor( - private service: SigFoxGroupService, + constructor( + private service: SigFoxGroupService, - private sigfoxApiService: GenericSigfoxAdministationService - ) {} - DUPLICATE_KEY_ERROR = "duplicate key value violates unique constraint"; + private sigfoxApiService: GenericSigfoxAdministationService + ) {} + DUPLICATE_KEY_ERROR = "duplicate key value violates unique constraint"; - @Get() - @ApiProduces("application/json") - @ApiOperation({ summary: "List all SigFox Groups" }) - @Read() - async getAll( - @Req() req: AuthenticatedRequest, - @Query() query: SigFoxGetAllRequestDto - ): Promise { - checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); - return await this.service.findAllForOrganization(query.organizationId); - } + @Get() + @ApiProduces("application/json") + @ApiOperation({ summary: "List all SigFox Groups" }) + @Read() + async getAll( + @Req() req: AuthenticatedRequest, + @Query() query: SigFoxGetAllRequestDto + ): Promise { + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); + return await this.service.findAllForOrganization(query.organizationId); + } - @Get(":id") - @ApiProduces("application/json") - @ApiOperation({ summary: "Get one group by ID" }) - @Read() - async getOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { - let group: SigFoxGroup; - try { - group = await this.service.findOne(id); - } catch (err) { - return null; - } - checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); - return group; + @Get(":id") + @ApiProduces("application/json") + @ApiOperation({ summary: "Get one group by ID" }) + @Read() + async getOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + let group: SigFoxGroup; + try { + group = await this.service.findOne(id); + } catch (err) { + return null; } + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationRead); + return group; + } - @Post() - @ApiProduces("application/json") - @ApiOperation({ summary: "Create a SigFox Group connection" }) - @ApiCreatedResponse() - @ApplicationAdmin() - async create(@Req() req: AuthenticatedRequest, @Body() query: CreateSigFoxGroupRequestDto): Promise { - try { - checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationWrite); - const group = await this.service.create(query, req.user.userId); + @Post() + @ApiProduces("application/json") + @ApiOperation({ summary: "Create a SigFox Group connection" }) + @ApiCreatedResponse() + @ApplicationAdmin() + async create(@Req() req: AuthenticatedRequest, @Body() query: CreateSigFoxGroupRequestDto): Promise { + try { + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationWrite); + const group = await this.service.create(query, req.user.userId); - AuditLog.success(ActionType.CREATE, SigFoxGroup.name, req.user.userId, group.id); - return group; - } catch (err) { - AuditLog.fail(ActionType.CREATE, SigFoxGroup.name, req.user.userId); - if (err.message.startsWith(this.DUPLICATE_KEY_ERROR)) { - throw new BadRequestException(ErrorCodes.GroupCanOnlyBeCreatedOncePrOrganization); - } - throw err; - } + AuditLog.success(ActionType.CREATE, SigFoxGroup.name, req.user.userId, group.id); + return group; + } catch (err) { + AuditLog.fail(ActionType.CREATE, SigFoxGroup.name, req.user.userId); + if (err.message.startsWith(this.DUPLICATE_KEY_ERROR)) { + throw new BadRequestException(ErrorCodes.GroupCanOnlyBeCreatedOncePrOrganization); + } + throw err; } + } - @Put(":id") - @ApiProduces("application/json") - @ApiOperation({ summary: "Update a SigFox Groups" }) - @ApplicationAdmin() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() dto: UpdateSigFoxGroupRequestDto - ): Promise { - let group: SigFoxGroup; - try { - group = await this.service.findOneForPermissionCheck(id); - } catch (err) { - AuditLog.fail(ActionType.CREATE, SigFoxGroup.name, req.user.userId, id); - throw new NotFoundException(); - } - checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); - try { - const changeGroup = await this.service.update(group, dto, req.user.userId); - AuditLog.success(ActionType.UPDATE, SigFoxGroup.name, req.user.userId, group.id); - return changeGroup; - } catch (err) { - AuditLog.fail(ActionType.CREATE, SigFoxGroup.name, req.user.userId, id); - if (err.message.startsWith(this.DUPLICATE_KEY_ERROR)) { - throw new BadRequestException(ErrorCodes.GroupCanOnlyBeCreatedOncePrOrganization); - } - throw err; - } + @Put(":id") + @ApiProduces("application/json") + @ApiOperation({ summary: "Update a SigFox Groups" }) + @ApplicationAdmin() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() dto: UpdateSigFoxGroupRequestDto + ): Promise { + let group: SigFoxGroup; + try { + group = await this.service.findOneForPermissionCheck(id); + } catch (err) { + AuditLog.fail(ActionType.CREATE, SigFoxGroup.name, req.user.userId, id); + throw new NotFoundException(); + } + checkIfUserHasAccessToOrganization(req, group.belongsTo.id, OrganizationAccessScope.ApplicationWrite); + try { + const changeGroup = await this.service.update(group, dto, req.user.userId); + AuditLog.success(ActionType.UPDATE, SigFoxGroup.name, req.user.userId, group.id); + return changeGroup; + } catch (err) { + AuditLog.fail(ActionType.CREATE, SigFoxGroup.name, req.user.userId, id); + if (err.message.startsWith(this.DUPLICATE_KEY_ERROR)) { + throw new BadRequestException(ErrorCodes.GroupCanOnlyBeCreatedOncePrOrganization); + } + throw err; } + } - @Post("test-connection") - @ApiOkResponse() - @HttpCode(200) - async testConnection( - @Req() req: AuthenticatedRequest, - @Body() dto: CreateSigFoxGroupRequestDto - ): Promise { - checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.ApplicationWrite); + @Post("test-connection") + @ApiOkResponse() + @HttpCode(200) + async testConnection( + @Req() req: AuthenticatedRequest, + @Body() dto: CreateSigFoxGroupRequestDto + ): Promise { + checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.ApplicationWrite); - const group = new SigFoxGroup(); - group.username = dto.username; - group.password = dto.password; + const group = new SigFoxGroup(); + group.username = dto.username; + group.password = dto.password; - return { - status: await this.sigfoxApiService.testConnection(group), - }; - } + return { + status: await this.sigfoxApiService.testConnection(group), + }; + } } diff --git a/src/controllers/admin-controller/test-payload-decoder.controller.ts b/src/controllers/admin-controller/test-payload-decoder.controller.ts index eded6e1a..d9d50754 100644 --- a/src/controllers/admin-controller/test-payload-decoder.controller.ts +++ b/src/controllers/admin-controller/test-payload-decoder.controller.ts @@ -6,21 +6,21 @@ import { PayloadDecoderExecutorService } from "@services/data-management/payload @ApiTags("Test PayloadDecoder") @Controller("test-payload-decoder") export class TestPayloadDecoderController { - constructor(private payloadDecoderExecutorService: PayloadDecoderExecutorService) {} + constructor(private payloadDecoderExecutorService: PayloadDecoderExecutorService) {} - @Post() - @ApiOperation({ - summary: `Test a payload decoder by uploading the code in javascript (as a string), and the IoTDevice (as a string) and the raw payload (as a string). \nUse JSON.stringify() on the objects to get a string representation`, - }) - async decode(@Body() body: TestPayloadDecoderDto): Promise { - try { - return await this.payloadDecoderExecutorService.allUntrustedCodeWithJsonStrings( - body.code, - body.iotDeviceJsonString, - body.rawPayloadJsonString - ); - } catch (err) { - throw new BadRequestException(`Got error: ${err}`); - } + @Post() + @ApiOperation({ + summary: `Test a payload decoder by uploading the code in javascript (as a string), and the IoTDevice (as a string) and the raw payload (as a string). \nUse JSON.stringify() on the objects to get a string representation`, + }) + async decode(@Body() body: TestPayloadDecoderDto): Promise { + try { + return await this.payloadDecoderExecutorService.allUntrustedCodeWithJsonStrings( + body.code, + body.iotDeviceJsonString, + body.rawPayloadJsonString + ); + } catch (err) { + throw new BadRequestException(`Got error: ${err}`); } + } } diff --git a/src/controllers/api-key/api-key-info.controller.ts b/src/controllers/api-key/api-key-info.controller.ts index 4b6ffc75..83ecc312 100644 --- a/src/controllers/api-key/api-key-info.controller.ts +++ b/src/controllers/api-key/api-key-info.controller.ts @@ -15,25 +15,25 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiTags("API key info") @Controller("api-key-info") export class ApiKeyInfoController { - constructor(private apiKeyInfoService: ApiKeyInfoService) {} - private readonly logger = new Logger(ApiKeyInfoController.name); + constructor(private apiKeyInfoService: ApiKeyInfoService) {} + private readonly logger = new Logger(ApiKeyInfoController.name); - @Get("organization") - @ApiOperation({ summary: "Get the organization of an API key" }) - async findApiKeyOrganization( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllEntitiesDto - ): Promise { - // The API Key will have access to at least read from a specific - const allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); + @Get("organization") + @ApiOperation({ summary: "Get the organization of an API key" }) + async findApiKeyOrganization( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllEntitiesDto + ): Promise { + // The API Key will have access to at least read from a specific + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); - if (allowedOrganizations.length !== 1) { - this.logger.error( - "API key is possibly tied to more than one organization. API key system user id: " + req.user.userId - ); - throw new BadRequestException(); - } - - return this.apiKeyInfoService.findOrganization(allowedOrganizations[0]); + if (allowedOrganizations.length !== 1) { + this.logger.error( + "API key is possibly tied to more than one organization. API key system user id: " + req.user.userId + ); + throw new BadRequestException(); } + + return this.apiKeyInfoService.findOrganization(allowedOrganizations[0]); + } } diff --git a/src/controllers/api-key/api-key.controller.ts b/src/controllers/api-key/api-key.controller.ts index 66ec62ea..c2e07a68 100644 --- a/src/controllers/api-key/api-key.controller.ts +++ b/src/controllers/api-key/api-key.controller.ts @@ -11,27 +11,27 @@ import { ApiKey } from "@entities/api-key.entity"; import { ActionType } from "@entities/audit-log-entry"; import { ErrorCodes } from "@enum/error-codes.enum"; import { - Body, - Controller, - Delete, - ForbiddenException, - Get, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + Body, + Controller, + Delete, + ForbiddenException, + Get, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiBadRequestResponse, - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { ApiKeyService } from "@services/api-key-management/api-key.service"; import { AuditLog } from "@services/audit-log.service"; @@ -48,110 +48,110 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiTags("API Key Management") @Controller("api-key") export class ApiKeyController { - constructor(private apiKeyService: ApiKeyService, private organizationService: OrganizationService) {} - - @Post() - @ApiOperation({ summary: "Create new API key" }) - async createApiKey(@Req() req: AuthenticatedRequest, @Body() dto: CreateApiKeyDto): Promise { - await this.checkIfUserHasAccessToPermissions(req, dto.permissionIds); - - try { - const result = await this.apiKeyService.create(dto, req.user.userId); - - AuditLog.success(ActionType.CREATE, ApiKey.name, req.user.userId, result.id, result.name); - return result; - } catch (err) { - AuditLog.fail(ActionType.CREATE, ApiKey.name, req.user.userId); - throw err; - } - } + constructor(private apiKeyService: ApiKeyService, private organizationService: OrganizationService) {} - @Put(":id") - @ApiOperation({ summary: "Update API key" }) - @ApiBadRequestResponse() - async updateApiKey( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() dto: UpdateApiKeyDto - ): Promise { - await this.checkIfUserHasAccessToPermissions(req, dto.permissionIds); - - try { - const result = await this.apiKeyService.update(id, dto, req.user.userId); - - AuditLog.success(ActionType.UPDATE, ApiKey.name, req.user.userId, result.id, result.name); - return result; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, ApiKey.name, req.user.userId, id); - throw err; - } - } + @Post() + @ApiOperation({ summary: "Create new API key" }) + async createApiKey(@Req() req: AuthenticatedRequest, @Body() dto: CreateApiKeyDto): Promise { + await this.checkIfUserHasAccessToPermissions(req, dto.permissionIds); - @Delete(":id") - @ApiOperation({ summary: "Delete an API key entity" }) - async deleteApiKey( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - await this.checkIfUserHasAccessToApiKey(req, id); - - const result = await this.apiKeyService.delete(id); - - AuditLog.success(ActionType.DELETE, ApiKey.name, req.user.userId, id); - return result; - } catch (err) { - AuditLog.fail(ActionType.DELETE, ApiKey.name, req.user.userId, id); - throw err; - } - } + try { + const result = await this.apiKeyService.create(dto, req.user.userId); - @Get() - @ApiOperation({ summary: "Get list of all API keys in organization" }) - getAllApiKeysInOrganization( - @Req() req: AuthenticatedRequest, - @Query() query: ListAllApiKeysDto - ): Promise { - checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.UserAdministrationWrite); - - try { - return this.apiKeyService.findAllByOrganizationId(query); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + AuditLog.success(ActionType.CREATE, ApiKey.name, req.user.userId, result.id, result.name); + return result; + } catch (err) { + AuditLog.fail(ActionType.CREATE, ApiKey.name, req.user.userId); + throw err; } - - @Get(":id") - @ApiOperation({ summary: "Get API key" }) - @ApiNotFoundResponse() - async getOneApiKey( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - await this.checkIfUserHasAccessToApiKey(req, id); - - try { - return await this.apiKeyService.findOneByIdWithPermissions(id); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + } + + @Put(":id") + @ApiOperation({ summary: "Update API key" }) + @ApiBadRequestResponse() + async updateApiKey( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() dto: UpdateApiKeyDto + ): Promise { + await this.checkIfUserHasAccessToPermissions(req, dto.permissionIds); + + try { + const result = await this.apiKeyService.update(id, dto, req.user.userId); + + AuditLog.success(ActionType.UPDATE, ApiKey.name, req.user.userId, result.id, result.name); + return result; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, ApiKey.name, req.user.userId, id); + throw err; } - - private async checkIfUserHasAccessToApiKey(req: AuthenticatedRequest, id: number) { - const apiKey = await this.apiKeyService.findOneByIdWithRelations(id); - await this.checkIfUserHasAccessToPermissions( - req, - apiKey.permissions.map(x => x.id) - ); + } + + @Delete(":id") + @ApiOperation({ summary: "Delete an API key entity" }) + async deleteApiKey( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + await this.checkIfUserHasAccessToApiKey(req, id); + + const result = await this.apiKeyService.delete(id); + + AuditLog.success(ActionType.DELETE, ApiKey.name, req.user.userId, id); + return result; + } catch (err) { + AuditLog.fail(ActionType.DELETE, ApiKey.name, req.user.userId, id); + throw err; + } + } + + @Get() + @ApiOperation({ summary: "Get list of all API keys in organization" }) + getAllApiKeysInOrganization( + @Req() req: AuthenticatedRequest, + @Query() query: ListAllApiKeysDto + ): Promise { + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.UserAdministrationWrite); + + try { + return this.apiKeyService.findAllByOrganizationId(query); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + } + + @Get(":id") + @ApiOperation({ summary: "Get API key" }) + @ApiNotFoundResponse() + async getOneApiKey( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + await this.checkIfUserHasAccessToApiKey(req, id); + + try { + return await this.apiKeyService.findOneByIdWithPermissions(id); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } + + private async checkIfUserHasAccessToApiKey(req: AuthenticatedRequest, id: number) { + const apiKey = await this.apiKeyService.findOneByIdWithRelations(id); + await this.checkIfUserHasAccessToPermissions( + req, + apiKey.permissions.map(x => x.id) + ); + } - private async checkIfUserHasAccessToPermissions(req: AuthenticatedRequest, permissionIds: number[]) { - if (!permissionIds?.length) throw new ForbiddenException(); + private async checkIfUserHasAccessToPermissions(req: AuthenticatedRequest, permissionIds: number[]) { + if (!permissionIds?.length) throw new ForbiddenException(); - const apiKeyOrganizations = await this.organizationService.findByPermissionIds(permissionIds); + const apiKeyOrganizations = await this.organizationService.findByPermissionIds(permissionIds); - for (const id of apiKeyOrganizations.map(org => org.id)) { - checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); - } + for (const id of apiKeyOrganizations.map(org => org.id)) { + checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationWrite); } + } } diff --git a/src/controllers/device-data-controller/receive-data.controller.ts b/src/controllers/device-data-controller/receive-data.controller.ts index be52563b..8c34f34b 100644 --- a/src/controllers/device-data-controller/receive-data.controller.ts +++ b/src/controllers/device-data-controller/receive-data.controller.ts @@ -1,13 +1,4 @@ -import { - Body, - Controller, - ForbiddenException, - Header, - HttpCode, - Logger, - Post, - Query, -} from "@nestjs/common"; +import { Body, Controller, ForbiddenException, Header, HttpCode, Logger, Post, Query } from "@nestjs/common"; import { ApiBadRequestResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; import { ReceiveDataDto } from "@dto/receive-data.dto"; @@ -19,53 +10,41 @@ import { IoTDeviceService } from "@services/device-management/iot-device.service @ApiTags("Receive Data") @Controller("receive-data") export class ReceiveDataController { - constructor( - private iotDeviceService: IoTDeviceService, - private receiveDataService: ReceiveDataService - ) {} - - private readonly logger = new Logger(ReceiveDataController.name); - - @Post() - @Header("Cache-Control", "none") - @ApiOperation({ summary: "Receive generic JSON data from edge devices" }) - @ApiBadRequestResponse() - @HttpCode(204) - async receive( - @Query("apiKey") apiKey: string, - @Body() data: ReceiveDataDto - ): Promise { - this.logger.debug( - `Got request. Apikey: '${apiKey}'. Data: '${JSON.stringify(data)}'` - ); - - const iotDevice = await this.checkIfDeviceIsValid(apiKey); - - // @HACK: Convert the 'data' back to a string. - // NestJS / BodyParser always converts the input to an object for us. - const dataAsString = JSON.stringify(data); - await this.receiveDataService.sendRawIotDeviceRequestToKafka( - iotDevice, - dataAsString, - IoTDeviceType.GenericHttp.toString() - ); - - return; - } - - private async checkIfDeviceIsValid(apiKey: string) { - const iotDevice = await this.iotDeviceService.findGenericHttpDeviceByApiKey( - apiKey - ); - - if (!iotDevice) { - const exception = new ForbiddenException(ErrorCodes.InvalidApiKey); - this.logger.error( - "No device has been registered by the following API key " + apiKey - ); - throw exception; - } - this.logger.debug(`Found device id: ${iotDevice.id}`); - return iotDevice; + constructor(private iotDeviceService: IoTDeviceService, private receiveDataService: ReceiveDataService) {} + + private readonly logger = new Logger(ReceiveDataController.name); + + @Post() + @Header("Cache-Control", "none") + @ApiOperation({ summary: "Receive generic JSON data from edge devices" }) + @ApiBadRequestResponse() + @HttpCode(204) + async receive(@Query("apiKey") apiKey: string, @Body() data: ReceiveDataDto): Promise { + this.logger.debug(`Got request. Apikey: '${apiKey}'. Data: '${JSON.stringify(data)}'`); + + const iotDevice = await this.checkIfDeviceIsValid(apiKey); + + // @HACK: Convert the 'data' back to a string. + // NestJS / BodyParser always converts the input to an object for us. + const dataAsString = JSON.stringify(data); + await this.receiveDataService.sendRawIotDeviceRequestToKafka( + iotDevice, + dataAsString, + IoTDeviceType.GenericHttp.toString() + ); + + return; + } + + private async checkIfDeviceIsValid(apiKey: string) { + const iotDevice = await this.iotDeviceService.findGenericHttpDeviceByApiKey(apiKey); + + if (!iotDevice) { + const exception = new ForbiddenException(ErrorCodes.InvalidApiKey); + this.logger.error("No device has been registered by the following API key " + apiKey); + throw exception; } + this.logger.debug(`Found device id: ${iotDevice.id}`); + return iotDevice; + } } diff --git a/src/controllers/device-data-controller/sigfox-listener.controller.ts b/src/controllers/device-data-controller/sigfox-listener.controller.ts index df51bd91..c12a6cad 100644 --- a/src/controllers/device-data-controller/sigfox-listener.controller.ts +++ b/src/controllers/device-data-controller/sigfox-listener.controller.ts @@ -1,20 +1,5 @@ -import { - Body, - Controller, - Logger, - NotFoundException, - Post, - Query, - BadRequestException, - Res, -} from "@nestjs/common"; -import { - ApiBadRequestResponse, - ApiNoContentResponse, - ApiOkResponse, - ApiOperation, - ApiTags, -} from "@nestjs/swagger"; +import { Body, Controller, Logger, NotFoundException, Post, Query, BadRequestException, Res } from "@nestjs/common"; +import { ApiBadRequestResponse, ApiNoContentResponse, ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; import { SigFoxCallbackDto } from "@dto/sigfox/sigfox-callback.dto"; import { IoTDeviceType } from "@enum/device-type.enum"; @@ -27,93 +12,82 @@ import { Response } from "express"; @ApiTags("SigFox") @Controller("sigfox-callback") export class SigFoxListenerController { - constructor( - private receiveDataService: ReceiveDataService, - private iotDeviceService: IoTDeviceService - ) {} - - private readonly logger = new Logger(SigFoxListenerController.name); - - @Post("data/bidir") - @ApiOperation({ summary: "SigFox data callback endpoint." }) - @ApiOkResponse() - @ApiNoContentResponse() - @ApiBadRequestResponse() - async sigfoxCallback( - @Query("apiKey") apiKey: string, - @Body() data: SigFoxCallbackDto, - @Res() res: Response - ): Promise { - this.verifyDeviceType(apiKey, data); - - const sigfoxDevice = await this.findSigFoxDevice(data); - - const dataAsString = JSON.stringify(data); - await this.receiveDataService.sendRawIotDeviceRequestToKafka( - sigfoxDevice, - dataAsString, - IoTDeviceType.SigFox.toString(), - data.time * 1000 // Timestamp passed must be in millis, sigfox uses seconds. - ); - - if (this.shouldSendDownlink(sigfoxDevice, data)) { - const payload = await this.doDownlink(sigfoxDevice); - return res.status(200).json(payload); - } - - return res.status(204).send(); + constructor(private receiveDataService: ReceiveDataService, private iotDeviceService: IoTDeviceService) {} + + private readonly logger = new Logger(SigFoxListenerController.name); + + @Post("data/bidir") + @ApiOperation({ summary: "SigFox data callback endpoint." }) + @ApiOkResponse() + @ApiNoContentResponse() + @ApiBadRequestResponse() + async sigfoxCallback( + @Query("apiKey") apiKey: string, + @Body() data: SigFoxCallbackDto, + @Res() res: Response + ): Promise { + this.verifyDeviceType(apiKey, data); + + const sigfoxDevice = await this.findSigFoxDevice(data); + + const dataAsString = JSON.stringify(data); + await this.receiveDataService.sendRawIotDeviceRequestToKafka( + sigfoxDevice, + dataAsString, + IoTDeviceType.SigFox.toString(), + data.time * 1000 // Timestamp passed must be in millis, sigfox uses seconds. + ); + + if (this.shouldSendDownlink(sigfoxDevice, data)) { + const payload = await this.doDownlink(sigfoxDevice); + return res.status(200).json(payload); } - private async doDownlink( - sigfoxDevice: SigFoxDevice - ): Promise { - this.logger.log( - `Time to downlink for device(${sigfoxDevice.id}) sigfoxId(${sigfoxDevice.deviceId})` - ); + return res.status(204).send(); + } - const dto: SigFoxDownlinkCallbackDto = {}; - dto[sigfoxDevice.deviceId] = { - downlinkData: sigfoxDevice.downlinkPayload, - }; + private async doDownlink(sigfoxDevice: SigFoxDevice): Promise { + this.logger.log(`Time to downlink for device(${sigfoxDevice.id}) sigfoxId(${sigfoxDevice.deviceId})`); - await this.iotDeviceService.removeDownlink(sigfoxDevice); + const dto: SigFoxDownlinkCallbackDto = {}; + dto[sigfoxDevice.deviceId] = { + downlinkData: sigfoxDevice.downlinkPayload, + }; - return dto; - } + await this.iotDeviceService.removeDownlink(sigfoxDevice); - private verifyDeviceType(apiKey: string, data: SigFoxCallbackDto) { - if (apiKey != data?.deviceTypeId) { - this.logger.error( - `ApiKey(${apiKey}) did not match DeviceTypeId(${data?.deviceTypeId})` - ); - throw new BadRequestException(); - } - } + return dto; + } - private shouldSendDownlink(iotDevice: SigFoxDevice, data: SigFoxCallbackDto) { - if (iotDevice.downlinkPayload != null) { - this.logger.debug(`Wanting to send downlink to ${iotDevice.deviceId}`); - } - if (!data.ack) { - this.logger.debug( - `Device ${iotDevice.deviceId} is not ready for downlink ('ack' == false)` - ); - } - return data.ack && iotDevice.downlinkPayload != null; + private verifyDeviceType(apiKey: string, data: SigFoxCallbackDto) { + if (apiKey != data?.deviceTypeId) { + this.logger.error(`ApiKey(${apiKey}) did not match DeviceTypeId(${data?.deviceTypeId})`); + throw new BadRequestException(); } + } - private async findSigFoxDevice(data: SigFoxCallbackDto) { - const iotDevice = await this.iotDeviceService.findSigFoxDeviceByDeviceIdAndDeviceTypeId( - data.deviceId, - data.deviceTypeId - ); - - if (!iotDevice) { - this.logger.error( - `Could not find SigFox device with id: '${data.deviceId}' and deviceType id: '${data.deviceTypeId}'` - ); - throw new NotFoundException(); - } - return iotDevice; + private shouldSendDownlink(iotDevice: SigFoxDevice, data: SigFoxCallbackDto) { + if (iotDevice.downlinkPayload != null) { + this.logger.debug(`Wanting to send downlink to ${iotDevice.deviceId}`); + } + if (!data.ack) { + this.logger.debug(`Device ${iotDevice.deviceId} is not ready for downlink ('ack' == false)`); + } + return data.ack && iotDevice.downlinkPayload != null; + } + + private async findSigFoxDevice(data: SigFoxCallbackDto) { + const iotDevice = await this.iotDeviceService.findSigFoxDeviceByDeviceIdAndDeviceTypeId( + data.deviceId, + data.deviceTypeId + ); + + if (!iotDevice) { + this.logger.error( + `Could not find SigFox device with id: '${data.deviceId}' and deviceType id: '${data.deviceTypeId}'` + ); + throw new NotFoundException(); } + return iotDevice; + } } diff --git a/src/controllers/user-management/auth.controller.ts b/src/controllers/user-management/auth.controller.ts index 1f8108d2..c9b4a4d8 100644 --- a/src/controllers/user-management/auth.controller.ts +++ b/src/controllers/user-management/auth.controller.ts @@ -1,15 +1,15 @@ import { - Body, - Controller, - Get, - Logger, - Post, - Req, - Request, - Res, - UnauthorizedException, - UseFilters, - UseGuards, + Body, + Controller, + Get, + Logger, + Post, + Req, + Request, + Res, + UnauthorizedException, + UseFilters, + UseGuards, } from "@nestjs/common"; import { ApiOperation, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; import * as _ from "lodash"; @@ -19,9 +19,9 @@ import { JwtAuthGuard } from "@auth/jwt-auth.guard"; import { LocalAuthGuard } from "@auth/local-auth.guard"; import { CurrentUserInfoDto } from "@dto/current-user-info.dto"; import { - AuthenticatedRequest, - AuthenticatedRequestKombitStrategy, - AuthenticatedRequestLocalStrategy, + AuthenticatedRequest, + AuthenticatedRequestKombitStrategy, + AuthenticatedRequestLocalStrategy, } from "@dto/internal/authenticated-request"; import { JwtPayloadDto } from "@dto/internal/jwt-payload.dto"; import { LoginDto } from "@dto/login.dto"; @@ -44,147 +44,145 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiTags("Auth") @Controller("auth") export class AuthController { - constructor( - private authService: AuthService, - private userService: UserService, - private organisationService: OrganizationService, - private strategy: KombitStrategy - ) {} - - private readonly logger = new Logger(AuthController.name); - - @Get("kombit/login") - @ApiOperation({ summary: "Initiate login with Kombit adgangsstyring" }) - @UseGuards(KombitAuthGuard) - async kombitLogin(@Req() req: expressRequest, @Res() res: Response): Promise { - return res.status(401).send("

Login Failure

"); - } - - @Post("kombit/login/callback") - @ApiOperation({ summary: "Login callback from Kombit adgangsstyring" }) - @UseGuards(KombitAuthGuard) - async kombitLoginCallback(@Req() req: AuthenticatedRequestKombitStrategy, @Res() res: Response): Promise { - const redirectTarget = req.cookies["redirect"]; - - // Login without proper roles - if (!(req.user instanceof User)) { - if (req.user === ErrorCodes.MissingRole) { - // Send back to frontend with an error - if (redirectTarget) { - return res.redirect(`${redirectTarget}?error=${ErrorCodes.MissingRole}`); - } else { - throw new UnauthorizedException(ErrorCodes.MissingRole); - } - } - throw new UnauthorizedException(); + constructor( + private authService: AuthService, + private userService: UserService, + private organisationService: OrganizationService, + private strategy: KombitStrategy + ) {} + + private readonly logger = new Logger(AuthController.name); + + @Get("kombit/login") + @ApiOperation({ summary: "Initiate login with Kombit adgangsstyring" }) + @UseGuards(KombitAuthGuard) + async kombitLogin(@Req() req: expressRequest, @Res() res: Response): Promise { + return res.status(401).send("

Login Failure

"); + } + + @Post("kombit/login/callback") + @ApiOperation({ summary: "Login callback from Kombit adgangsstyring" }) + @UseGuards(KombitAuthGuard) + async kombitLoginCallback(@Req() req: AuthenticatedRequestKombitStrategy, @Res() res: Response): Promise { + const redirectTarget = req.cookies["redirect"]; + + // Login without proper roles + if (!(req.user instanceof User)) { + if (req.user === ErrorCodes.MissingRole) { + // Send back to frontend with an error + if (redirectTarget) { + return res.redirect(`${redirectTarget}?error=${ErrorCodes.MissingRole}`); + } else { + throw new UnauthorizedException(ErrorCodes.MissingRole); } - - const { nameId, id } = req.user; - const jwt = await this.authService.issueJwt(nameId, id, true); - const baseUrl = redirectTarget ? redirectTarget : Configuration()["frontend"]["baseurl"]; - if (!baseUrl.includes("applications")) { - return res.redirect(`${baseUrl}/applications?jwt=${jwt.accessToken}`); - } - return res.redirect(`${baseUrl}?jwt=${jwt.accessToken}`); + } + throw new UnauthorizedException(); } - @Get("kombit/logout") - @UseGuards(JwtAuthGuard) - @ApiOperation({ summary: "Initiates the SAML logout flow" }) - public async logout(@Req() req: expressRequest, @Res() res: Response): Promise { - this.logger.debug("Logging out ..."); - const reqConverted: RequestWithUser = req as RequestWithUser; - - // Inspecting the source code (v3.2.1), we gather that - // - ID is unknown. Might be unused or required for @InResponseTo in saml.js - // - nameID is used. Corresponds to user.nameId in DB - // - nameIDFormat is used. Correspond to in the public certificate - reqConverted.samlLogoutRequest = null; // Property must be set, but it is unused in the source code - reqConverted.user.nameIDFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"; - - this.strategy.logout(reqConverted, (err: Error, url: string): void => { - req.logout(err1 => { - this.logger.debug("Inside callback"); - if (Object.keys(err1).length === 0) { - this.logger.debug("No errors"); - res.redirect(url); - } else { - this.logger.error( - `Logout failed with error: ${JSON.stringify(err)} and inner Err: ${JSON.stringify(err1)}` - ); - } - }); - }); + const { nameId, id } = req.user; + const jwt = await this.authService.issueJwt(nameId, id, true); + const baseUrl = redirectTarget ? redirectTarget : Configuration()["frontend"]["baseurl"]; + if (!baseUrl.includes("applications")) { + return res.redirect(`${baseUrl}/applications?jwt=${jwt.accessToken}`); } - - @Get("kombit/logout/callback") - // @UseGuards(KombitAuthGuard) - @ApiOperation({ summary: "Handles the SAML logout" }) - public async logoutCallback(@Req() req: expressRequest, @Res() res: Response): Promise { - this.logger.debug("Get callback Logging out ..."); - // This callback openes in a new window for some reason, without sending something to it a timout error happens - res.send("Logged out ..."); - } - - @Get("kombit/metadata") - async kombitMetadata(@Res() res: Response): Promise { - res.set("Content-Type", "text/xml"); - res.send( - this.strategy.generateServiceProviderMetadata( - fs.readFileSync("secrets/FOCES_PUBLIC.crt", "utf-8"), - fs.readFileSync("secrets/FOCES_PUBLIC.crt", "utf-8") - ) - ); - } - - @Post("login") - @ApiOperation({ summary: "Login using username and password" }) - @ApiUnauthorizedResponse() - @UseGuards(LocalAuthGuard) - async login(@Request() req: AuthenticatedRequestLocalStrategy, @Body() _: LoginDto): Promise { - const { email, id } = req.user; - return this.authService.issueJwt(email, id, false); - } - - @Get("profile") - @ApiOperation({ - summary: "Return id and username (email) of the user logged in", - }) - @ApiAuth() - @UseGuards(JwtAuthGuard) - async getProfile(@Request() req: AuthenticatedRequest): Promise { - return { - sub: req.user.userId, - username: req.user.username, - }; - } - - @Get("me") - @ApiOperation({ - summary: "Get basic info on the current user and the organizations it has some permissions to.", - }) - @ApiAuth() - @UseGuards(JwtAuthGuard) - async getInfoAboutCurrentUser(@Request() req: AuthenticatedRequest): Promise { - const user = await this.userService.findOneWithOrganizations(req.user.userId); - const orgs = await this.getAllowedOrganisations(req, user); - return { - user: user, - organizations: orgs, - }; - } - - private async getAllowedOrganisations(req: AuthenticatedRequest, user: User): Promise { - if (req.user.permissions.isGlobalAdmin) { - return (await this.organisationService.findAll()).data; + return res.redirect(`${baseUrl}?jwt=${jwt.accessToken}`); + } + + @Get("kombit/logout") + @UseGuards(JwtAuthGuard) + @ApiOperation({ summary: "Initiates the SAML logout flow" }) + public async logout(@Req() req: expressRequest, @Res() res: Response): Promise { + this.logger.debug("Logging out ..."); + const reqConverted: RequestWithUser = req as RequestWithUser; + + // Inspecting the source code (v3.2.1), we gather that + // - ID is unknown. Might be unused or required for @InResponseTo in saml.js + // - nameID is used. Corresponds to user.nameId in DB + // - nameIDFormat is used. Correspond to in the public certificate + reqConverted.samlLogoutRequest = null; // Property must be set, but it is unused in the source code + reqConverted.user.nameIDFormat = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName"; + + this.strategy.logout(reqConverted, (err: Error, url: string): void => { + req.logout(err1 => { + this.logger.debug("Inside callback"); + if (Object.keys(err1).length === 0) { + this.logger.debug("No errors"); + res.redirect(url); + } else { + this.logger.error(`Logout failed with error: ${JSON.stringify(err)} and inner Err: ${JSON.stringify(err1)}`); } - - const orgs = user.permissions.reduce((arr, orgP) => { - if (isOrganizationPermission(orgP)) { - arr.push(orgP.organization); - } - return arr; - }, []); - return _.uniqBy(orgs, x => x.id); + }); + }); + } + + @Get("kombit/logout/callback") + // @UseGuards(KombitAuthGuard) + @ApiOperation({ summary: "Handles the SAML logout" }) + public async logoutCallback(@Req() req: expressRequest, @Res() res: Response): Promise { + this.logger.debug("Get callback Logging out ..."); + // This callback openes in a new window for some reason, without sending something to it a timout error happens + res.send("Logged out ..."); + } + + @Get("kombit/metadata") + async kombitMetadata(@Res() res: Response): Promise { + res.set("Content-Type", "text/xml"); + res.send( + this.strategy.generateServiceProviderMetadata( + fs.readFileSync("secrets/FOCES_PUBLIC.crt", "utf-8"), + fs.readFileSync("secrets/FOCES_PUBLIC.crt", "utf-8") + ) + ); + } + + @Post("login") + @ApiOperation({ summary: "Login using username and password" }) + @ApiUnauthorizedResponse() + @UseGuards(LocalAuthGuard) + async login(@Request() req: AuthenticatedRequestLocalStrategy, @Body() _: LoginDto): Promise { + const { email, id } = req.user; + return this.authService.issueJwt(email, id, false); + } + + @Get("profile") + @ApiOperation({ + summary: "Return id and username (email) of the user logged in", + }) + @ApiAuth() + @UseGuards(JwtAuthGuard) + async getProfile(@Request() req: AuthenticatedRequest): Promise { + return { + sub: req.user.userId, + username: req.user.username, + }; + } + + @Get("me") + @ApiOperation({ + summary: "Get basic info on the current user and the organizations it has some permissions to.", + }) + @ApiAuth() + @UseGuards(JwtAuthGuard) + async getInfoAboutCurrentUser(@Request() req: AuthenticatedRequest): Promise { + const user = await this.userService.findOneWithOrganizations(req.user.userId); + const orgs = await this.getAllowedOrganisations(req, user); + return { + user: user, + organizations: orgs, + }; + } + + private async getAllowedOrganisations(req: AuthenticatedRequest, user: User): Promise { + if (req.user.permissions.isGlobalAdmin) { + return (await this.organisationService.findAll()).data; } + + const orgs = user.permissions.reduce((arr, orgP) => { + if (isOrganizationPermission(orgP)) { + arr.push(orgP.organization); + } + return arr; + }, []); + return _.uniqBy(orgs, x => x.id); + } } diff --git a/src/controllers/user-management/new-kombit-creation.controller.ts b/src/controllers/user-management/new-kombit-creation.controller.ts index cd61b685..f225f51f 100644 --- a/src/controllers/user-management/new-kombit-creation.controller.ts +++ b/src/controllers/user-management/new-kombit-creation.controller.ts @@ -11,24 +11,24 @@ import { User } from "@entities/user.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; import { - BadRequestException, - Body, - Controller, - Get, - NotFoundException, - Param, - ParseIntPipe, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Get, + NotFoundException, + Param, + ParseIntPipe, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { AuditLog } from "@services/audit-log.service"; import { OrganizationService } from "@services/user-management/organization.service"; @@ -43,109 +43,109 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiTags("KombitEmailCreation") @Controller("kombitCreation") export class NewKombitCreationController { - constructor( - private organizationService: OrganizationService, - private userService: UserService, - private permissionService: PermissionService - ) {} - - @Put("createNewKombitUser") - @ApiOperation({ summary: "Create kombit-user Email" }) - async newKombitUser(@Req() req: AuthenticatedRequest, @Body() dto: CreateNewKombitUserDto): Promise { - try { - const dbUser: User = await this.userService.findOne(req.user.userId); - const permissions = await this.permissionService.findManyWithRelations(dto.requestedOrganizationIds); - - const requestedOrganizations: Organization[] = await this.organizationService.mapPermissionsToOrganizations( - permissions - ); - - if (!dbUser.email) { - // The desired email is already in use for another user (this would also throw an error in the database) - if (await this.userService.isEmailUsedByAUser(dto.email)) { - throw new BadRequestException(ErrorCodes.EmailAlreadyInUse); - } - - const updatedUser: User = await this.userService.newKombitUser(dto, requestedOrganizations, dbUser); - - for (let index = 0; index < dto.requestedOrganizationIds.length; index++) { - const dbOrg = await this.organizationService.findByIdWithUsers(requestedOrganizations[index].id); - - await this.organizationService.updateAwaitingUsers(dbOrg, updatedUser); - } - - AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); - return updatedUser; - } else { - throw new BadRequestException(ErrorCodes.EmailAlreadyExists); - } - } catch (err) { - AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); - throw err; + constructor( + private organizationService: OrganizationService, + private userService: UserService, + private permissionService: PermissionService + ) {} + + @Put("createNewKombitUser") + @ApiOperation({ summary: "Create kombit-user Email" }) + async newKombitUser(@Req() req: AuthenticatedRequest, @Body() dto: CreateNewKombitUserDto): Promise { + try { + const dbUser: User = await this.userService.findOne(req.user.userId); + const permissions = await this.permissionService.findManyWithRelations(dto.requestedOrganizationIds); + + const requestedOrganizations: Organization[] = await this.organizationService.mapPermissionsToOrganizations( + permissions + ); + + if (!dbUser.email) { + // The desired email is already in use for another user (this would also throw an error in the database) + if (await this.userService.isEmailUsedByAUser(dto.email)) { + throw new BadRequestException(ErrorCodes.EmailAlreadyInUse); } - } - @Get("minimal") - @ApiOperation({ - summary: "Get list of the minimal representation of organizations, i.e. id and name.", - }) - async findAllMinimal(): Promise { - return await this.organizationService.findAllMinimal(); - } + const updatedUser: User = await this.userService.newKombitUser(dto, requestedOrganizations, dbUser); - @Get("minimalUsers") - @ApiOperation({ summary: "Get all id,names of users" }) - async findAllMinimalUsers(): Promise { - return await this.userService.findAllMinimal(); - } + for (let index = 0; index < dto.requestedOrganizationIds.length; index++) { + const dbOrg = await this.organizationService.findByIdWithUsers(requestedOrganizations[index].id); - @Put("updateUserOrgs") - @ApiOperation({ summary: "Updates the users organizations" }) - @ApiNotFoundResponse() - async updateUserOrgs( - @Req() req: AuthenticatedRequest, - @Body() updateUserOrgsDto: UpdateUserOrgsDto - ): Promise { - try { - const user = await this.userService.findOne(req.user.userId); - const permissions = await this.permissionService.findManyWithRelations( - updateUserOrgsDto.requestedOrganizationIds - ); - - const requestedOrganizations = this.organizationService.mapPermissionsToOrganizations(permissions); - - for (let index = 0; index < requestedOrganizations.length; index++) { - await this.userService.sendOrganizationRequestMail(user, requestedOrganizations[index]); - } - - for (const org of requestedOrganizations) { - const dbOrg = await this.organizationService.findByIdWithUsers(org.id); - await this.organizationService.updateAwaitingUsers(dbOrg, user); - } - - AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); - return updateUserOrgsDto; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); - throw err; + await this.organizationService.updateAwaitingUsers(dbOrg, updatedUser); } - } - @Get(":id") - @ApiOperation({ summary: "Get one user" }) - async find( - @Param("id", new ParseIntPipe()) id: number, - @Query("extendedInfo") extendedInfo?: boolean - ): Promise { - const getExtendedInfo = extendedInfo != null ? extendedInfo : false; - try { - // Don't leak the passwordHash - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { passwordHash, ...user } = await this.userService.findOne(id, getExtendedInfo); - - return user; - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); + return updatedUser; + } else { + throw new BadRequestException(ErrorCodes.EmailAlreadyExists); + } + } catch (err) { + AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); + throw err; + } + } + + @Get("minimal") + @ApiOperation({ + summary: "Get list of the minimal representation of organizations, i.e. id and name.", + }) + async findAllMinimal(): Promise { + return await this.organizationService.findAllMinimal(); + } + + @Get("minimalUsers") + @ApiOperation({ summary: "Get all id,names of users" }) + async findAllMinimalUsers(): Promise { + return await this.userService.findAllMinimal(); + } + + @Put("updateUserOrgs") + @ApiOperation({ summary: "Updates the users organizations" }) + @ApiNotFoundResponse() + async updateUserOrgs( + @Req() req: AuthenticatedRequest, + @Body() updateUserOrgsDto: UpdateUserOrgsDto + ): Promise { + try { + const user = await this.userService.findOne(req.user.userId); + const permissions = await this.permissionService.findManyWithRelations( + updateUserOrgsDto.requestedOrganizationIds + ); + + const requestedOrganizations = this.organizationService.mapPermissionsToOrganizations(permissions); + + for (let index = 0; index < requestedOrganizations.length; index++) { + await this.userService.sendOrganizationRequestMail(user, requestedOrganizations[index]); + } + + for (const org of requestedOrganizations) { + const dbOrg = await this.organizationService.findByIdWithUsers(org.id); + await this.organizationService.updateAwaitingUsers(dbOrg, user); + } + + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId); + return updateUserOrgsDto; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId); + throw err; + } + } + + @Get(":id") + @ApiOperation({ summary: "Get one user" }) + async find( + @Param("id", new ParseIntPipe()) id: number, + @Query("extendedInfo") extendedInfo?: boolean + ): Promise { + const getExtendedInfo = extendedInfo != null ? extendedInfo : false; + try { + // Don't leak the passwordHash + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { passwordHash, ...user } = await this.userService.findOne(id, getExtendedInfo); + + return user; + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + } } diff --git a/src/controllers/user-management/organization.controller.ts b/src/controllers/user-management/organization.controller.ts index e38eae51..821c7464 100644 --- a/src/controllers/user-management/organization.controller.ts +++ b/src/controllers/user-management/organization.controller.ts @@ -1,24 +1,24 @@ import { - Body, - Controller, - Delete, - Get, - Logger, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + Body, + Controller, + Delete, + Get, + Logger, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; @@ -27,8 +27,8 @@ import { RolesGuard } from "@auth/roles.guard"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { - ListAllMinimalOrganizationsResponseDto, - ListAllOrganizationsResponseDto, + ListAllMinimalOrganizationsResponseDto, + ListAllOrganizationsResponseDto, } from "@dto/list-all-organizations-response.dto"; import { CreateOrganizationDto } from "@dto/user-management/create-organization.dto"; import { UpdateOrganizationDto } from "@dto/user-management/update-organization.dto"; @@ -50,101 +50,98 @@ import { PermissionType } from "@enum/permission-type.enum"; @Controller("organization") @GlobalAdmin() export class OrganizationController { - constructor(private organizationService: OrganizationService) {} - private readonly logger = new Logger(OrganizationController.name); + constructor(private organizationService: OrganizationService) {} + private readonly logger = new Logger(OrganizationController.name); - @Post() - @ApiOperation({ summary: "Create a new Organization" }) - async create( - @Req() req: AuthenticatedRequest, - @Body() createOrganizationDto: CreateOrganizationDto - ): Promise { - try { - const organization = await this.organizationService.create(createOrganizationDto, req.user.userId); - AuditLog.success(ActionType.CREATE, Organization.name, req.user.userId, organization.id, organization.name); - return organization; - } catch (err) { - AuditLog.fail(ActionType.CREATE, Organization.name, req.user.userId); - throw err; - } + @Post() + @ApiOperation({ summary: "Create a new Organization" }) + async create( + @Req() req: AuthenticatedRequest, + @Body() createOrganizationDto: CreateOrganizationDto + ): Promise { + try { + const organization = await this.organizationService.create(createOrganizationDto, req.user.userId); + AuditLog.success(ActionType.CREATE, Organization.name, req.user.userId, organization.id, organization.name); + return organization; + } catch (err) { + AuditLog.fail(ActionType.CREATE, Organization.name, req.user.userId); + throw err; } + } - @Put(":id") - @ApiOperation({ summary: "Update an Organization" }) - @ApiNotFoundResponse() - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() updateOrganizationDto: UpdateOrganizationDto - ): Promise { - try { - const organization = await this.organizationService.update(id, updateOrganizationDto, req.user.userId); - AuditLog.success(ActionType.UPDATE, Organization.name, req.user.userId, organization.id, organization.name); - return organization; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, Organization.name, req.user.userId, id); - if (err.name == "EntityNotFound") { - throw new NotFoundException(); - } - throw err; - } + @Put(":id") + @ApiOperation({ summary: "Update an Organization" }) + @ApiNotFoundResponse() + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() updateOrganizationDto: UpdateOrganizationDto + ): Promise { + try { + const organization = await this.organizationService.update(id, updateOrganizationDto, req.user.userId); + AuditLog.success(ActionType.UPDATE, Organization.name, req.user.userId, organization.id, organization.name); + return organization; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, Organization.name, req.user.userId, id); + if (err.name == "EntityNotFound") { + throw new NotFoundException(); + } + throw err; } + } - @Get("minimal") - @ApiOperation({ - summary: "Get list of the minimal representation of organizations, i.e. id and name.", - }) - @Read() - async findAllMinimal(): Promise { - return await this.organizationService.findAllMinimal(); - } + @Get("minimal") + @ApiOperation({ + summary: "Get list of the minimal representation of organizations, i.e. id and name.", + }) + @Read() + async findAllMinimal(): Promise { + return await this.organizationService.findAllMinimal(); + } - @Get() - @ApiOperation({ summary: "Get list of all Organizations" }) - @UserAdmin() - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllEntitiesDto - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return this.organizationService.findAllPaginated(query); - } else { - const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); - return this.organizationService.findAllInOrganizationList(allowedOrganizations, query); - } + @Get() + @ApiOperation({ summary: "Get list of all Organizations" }) + @UserAdmin() + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllEntitiesDto + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return this.organizationService.findAllPaginated(query); + } else { + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); + return this.organizationService.findAllInOrganizationList(allowedOrganizations, query); } + } - @Get(":id") - @ApiOperation({ summary: "Get one Organization" }) - @ApiNotFoundResponse() - @UserAdmin() - async findOne( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationRead); - return await this.organizationService.findByIdWithRelations(id); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + @Get(":id") + @ApiOperation({ summary: "Get one Organization" }) + @ApiNotFoundResponse() + @UserAdmin() + async findOne(@Req() req: AuthenticatedRequest, @Param("id", new ParseIntPipe()) id: number): Promise { + try { + checkIfUserHasAccessToOrganization(req, id, OrganizationAccessScope.UserAdministrationRead); + return await this.organizationService.findByIdWithRelations(id); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } + } - @Delete(":id") - @ApiOperation({ summary: "Delete an Organization" }) - @ApiNotFoundResponse() - async delete( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const result = await this.organizationService.delete(id); + @Delete(":id") + @ApiOperation({ summary: "Delete an Organization" }) + @ApiNotFoundResponse() + async delete( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const result = await this.organizationService.delete(id); - AuditLog.success(ActionType.DELETE, Organization.name, req.user.userId, id); - return result; - } catch (err) { - AuditLog.fail(ActionType.DELETE, Organization.name, req.user.userId, id); - throw err; - } + AuditLog.success(ActionType.DELETE, Organization.name, req.user.userId, id); + return result; + } catch (err) { + AuditLog.fail(ActionType.DELETE, Organization.name, req.user.userId, id); + throw err; } + } } diff --git a/src/controllers/user-management/permission.controller.ts b/src/controllers/user-management/permission.controller.ts index 9d533ea1..7661e670 100644 --- a/src/controllers/user-management/permission.controller.ts +++ b/src/controllers/user-management/permission.controller.ts @@ -1,24 +1,24 @@ import { - BadRequestException, - Body, - Controller, - Delete, - Get, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + Delete, + Get, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { - ApiForbiddenResponse, - ApiNotFoundResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, + ApiForbiddenResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, } from "@nestjs/swagger"; import { JwtAuthGuard } from "@auth/jwt-auth.guard"; @@ -31,9 +31,9 @@ import { AuthenticatedRequest } from "@entities/dto/internal/authenticated-reque import { Permission } from "@entities/permissions/permission.entity"; import { PermissionType } from "@enum/permission-type.enum"; import { - checkIfUserHasAccessToOrganization, - checkIfUserIsGlobalAdmin, - OrganizationAccessScope, + checkIfUserHasAccessToOrganization, + checkIfUserIsGlobalAdmin, + OrganizationAccessScope, } from "@helpers/security-helper"; import { PermissionService } from "@services/user-management/permission.service"; import { AuditLog } from "@services/audit-log.service"; @@ -58,235 +58,215 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiTags("Permission") @Controller("permission") export class PermissionController { - constructor( - private permissionService: PermissionService, - private userService: UserService, - private applicationService: ApplicationService, - private organizationService: OrganizationService - ) {} + constructor( + private permissionService: PermissionService, + private userService: UserService, + private applicationService: ApplicationService, + private organizationService: OrganizationService + ) {} - @Post() - @ApiOperation({ summary: "Create new permission entity" }) - @UserAdmin() - async createPermission(@Req() req: AuthenticatedRequest, @Body() dto: CreatePermissionDto): Promise { - try { - checkIfUserHasAccessToOrganization( - req, - dto.organizationId, - OrganizationAccessScope.UserAdministrationWrite - ); + @Post() + @ApiOperation({ summary: "Create new permission entity" }) + @UserAdmin() + async createPermission(@Req() req: AuthenticatedRequest, @Body() dto: CreatePermissionDto): Promise { + try { + checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.UserAdministrationWrite); - const result = await this.permissionService.createNewPermission(dto, req.user.userId); + const result = await this.permissionService.createNewPermission(dto, req.user.userId); - AuditLog.success(ActionType.CREATE, Permission.name, req.user.userId, result.id, result.name); - return result; - } catch (err) { - AuditLog.fail(ActionType.CREATE, Permission.name, req.user.userId); - throw err; - } + AuditLog.success(ActionType.CREATE, Permission.name, req.user.userId, result.id, result.name); + return result; + } catch (err) { + AuditLog.fail(ActionType.CREATE, Permission.name, req.user.userId); + throw err; } + } - @Put("/acceptUser") - @ApiOperation({ summary: "add user to permission" }) - async addUserToPermission( - @Req() req: AuthenticatedRequest, - @Body() dto: PermissionRequestAcceptUser - ): Promise { - try { - checkIfUserHasAccessToOrganization( - req, - dto.organizationId, - OrganizationAccessScope.UserAdministrationWrite - ); + @Put("/acceptUser") + @ApiOperation({ summary: "add user to permission" }) + async addUserToPermission(@Req() req: AuthenticatedRequest, @Body() dto: PermissionRequestAcceptUser): Promise { + try { + checkIfUserHasAccessToOrganization(req, dto.organizationId, OrganizationAccessScope.UserAdministrationWrite); - const permissions = await this.permissionService.findOneWithRelations(dto.organizationId); + const permissions = await this.permissionService.findOneWithRelations(dto.organizationId); - const org: Organization = this.organizationService.mapPermissionsToOneOrganization(permissions); + const org: Organization = this.organizationService.mapPermissionsToOneOrganization(permissions); - const user: User = await this.userService.findOne(dto.userId); - const newUserPermissions: Permission[] = []; + const user: User = await this.userService.findOne(dto.userId); + const newUserPermissions: Permission[] = []; - for (const orgPermission of org.permissions) { - if (dto.permissionIds.includes(orgPermission.id)) { - newUserPermissions.push(orgPermission); - } - } + for (const orgPermission of org.permissions) { + if (dto.permissionIds.includes(orgPermission.id)) { + newUserPermissions.push(orgPermission); + } + } - const resultUser = await this.userService.acceptUser(user, org, newUserPermissions); + const resultUser = await this.userService.acceptUser(user, org, newUserPermissions); - AuditLog.success(ActionType.UPDATE, Permission.name, req.user.userId, resultUser.id, resultUser.name); - return resultUser; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, Permission.name, req.user.userId); - throw err; - } + AuditLog.success(ActionType.UPDATE, Permission.name, req.user.userId, resultUser.id, resultUser.name); + return resultUser; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, Permission.name, req.user.userId); + throw err; } + } - @Put(":id") - @ApiOperation({ summary: "Update permission" }) - @UserAdmin() - async updatePermission( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() dto: UpdatePermissionDto - ): Promise { - try { - const permission = await this.permissionService.getPermission(id); - if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { - checkIfUserIsGlobalAdmin(req); - } else { - checkIfUserHasAccessToOrganization( - req, - permission.organization.id, - OrganizationAccessScope.UserAdministrationWrite - ); - } + @Put(":id") + @ApiOperation({ summary: "Update permission" }) + @UserAdmin() + async updatePermission( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() dto: UpdatePermissionDto + ): Promise { + try { + const permission = await this.permissionService.getPermission(id); + if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { + checkIfUserIsGlobalAdmin(req); + } else { + checkIfUserHasAccessToOrganization( + req, + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite + ); + } - const result = await this.permissionService.updatePermission(id, dto, req.user.userId); + const result = await this.permissionService.updatePermission(id, dto, req.user.userId); - AuditLog.success(ActionType.UPDATE, Permission.name, req.user.userId, result.id, result.name); - return result; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, Permission.name, req.user.userId, id); - throw err; - } + AuditLog.success(ActionType.UPDATE, Permission.name, req.user.userId, result.id, result.name); + return result; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, Permission.name, req.user.userId, id); + throw err; } + } - @Delete(":id") - @ApiOperation({ summary: "Delete a permission entity" }) - @UserAdmin() - async deletePermission( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - try { - const permission = await this.permissionService.getPermission(id); - if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { - throw new BadRequestException("You cannot delete GlobalAdmin"); - } else { - checkIfUserHasAccessToOrganization( - req, - permission.organization.id, - OrganizationAccessScope.UserAdministrationWrite - ); - } + @Delete(":id") + @ApiOperation({ summary: "Delete a permission entity" }) + @UserAdmin() + async deletePermission( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + const permission = await this.permissionService.getPermission(id); + if (permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { + throw new BadRequestException("You cannot delete GlobalAdmin"); + } else { + checkIfUserHasAccessToOrganization( + req, + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite + ); + } - const result = await this.permissionService.deletePermission(id); + const result = await this.permissionService.deletePermission(id); - AuditLog.success(ActionType.DELETE, Permission.name, req.user.userId, id); - return result; - } catch (err) { - AuditLog.fail(ActionType.DELETE, Permission.name, req.user.userId, id); - throw err; - } + AuditLog.success(ActionType.DELETE, Permission.name, req.user.userId, id); + return result; + } catch (err) { + AuditLog.fail(ActionType.DELETE, Permission.name, req.user.userId, id); + throw err; } + } - @Get() - @ApiOperation({ summary: "Get list of all permissions" }) - async getAllPermissions( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllPermissionsDto - ): Promise { - if (!req.user.permissions.isGlobalAdmin && query.organisationId === undefined) { - const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); - return this.permissionService.getAllPermissionsInOrganizations(allowedOrganizations, query); - } - return this.permissionService.getAllPermissions(query); + @Get() + @ApiOperation({ summary: "Get list of all permissions" }) + async getAllPermissions( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllPermissionsDto + ): Promise { + if (!req.user.permissions.isGlobalAdmin && query.organisationId === undefined) { + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); + return this.permissionService.getAllPermissionsInOrganizations(allowedOrganizations, query); } + return this.permissionService.getAllPermissions(query); + } - @Get(":id") - @ApiOperation({ summary: "Get permissions entity" }) - @ApiNotFoundResponse() - async getOnePermissions( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number - ): Promise { - let permission; - try { - permission = await this.permissionService.getPermission(id); - } catch (err) { - throw new NotFoundException(); - } + @Get(":id") + @ApiOperation({ summary: "Get permissions entity" }) + @ApiNotFoundResponse() + async getOnePermissions( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + let permission; + try { + permission = await this.permissionService.getPermission(id); + } catch (err) { + throw new NotFoundException(); + } - if ( - req.user.permissions.isGlobalAdmin || - permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) - ) { - return permission; - } else { - checkIfUserHasAccessToOrganization( - req, - permission.organization.id, - OrganizationAccessScope.UserAdministrationWrite - ); + if (req.user.permissions.isGlobalAdmin || permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { + return permission; + } else { + checkIfUserHasAccessToOrganization( + req, + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite + ); - return permission; - } + return permission; } + } - @Get(":id/applications") - @ApiOperation({ summary: "Get applications of a permissions entity" }) - @ApiNotFoundResponse() - async getApplicationsOnOnePermissions( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Query() query?: ListAllPaginated - ): Promise { - let applicationsPromise; - let permission; - try { - applicationsPromise = this.applicationService.getApplicationsOnPermissionId(id, query); - permission = await this.permissionService.getPermission(id); - } catch (err) { - throw new NotFoundException(); - } + @Get(":id/applications") + @ApiOperation({ summary: "Get applications of a permissions entity" }) + @ApiNotFoundResponse() + async getApplicationsOnOnePermissions( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Query() query?: ListAllPaginated + ): Promise { + let applicationsPromise; + let permission; + try { + applicationsPromise = this.applicationService.getApplicationsOnPermissionId(id, query); + permission = await this.permissionService.getPermission(id); + } catch (err) { + throw new NotFoundException(); + } - if ( - req.user.permissions.isGlobalAdmin || - permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) - ) { - return await applicationsPromise; - } else { - checkIfUserHasAccessToOrganization( - req, - permission.organization.id, - OrganizationAccessScope.UserAdministrationWrite - ); + if (req.user.permissions.isGlobalAdmin || permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { + return await applicationsPromise; + } else { + checkIfUserHasAccessToOrganization( + req, + permission.organization.id, + OrganizationAccessScope.UserAdministrationWrite + ); - return await applicationsPromise; - } + return await applicationsPromise; } + } - @Get(":id/users") - @ApiOperation({ summary: "Get user of a permissions entity" }) - @ApiNotFoundResponse() - async getUserOnOnePermissions( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Query() query?: ListAllPaginated - ): Promise { - let users; - let permission; - try { - users = this.userService.getUsersOnPermissionId(id, query); - permission = await this.permissionService.getPermission(id); - } catch (err) { - throw new NotFoundException(); - } + @Get(":id/users") + @ApiOperation({ summary: "Get user of a permissions entity" }) + @ApiNotFoundResponse() + async getUserOnOnePermissions( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Query() query?: ListAllPaginated + ): Promise { + let users; + let permission; + try { + users = this.userService.getUsersOnPermissionId(id, query); + permission = await this.permissionService.getPermission(id); + } catch (err) { + throw new NotFoundException(); + } - if ( - req.user.permissions.isGlobalAdmin || - permission.type.some(({ type }) => type === PermissionType.GlobalAdmin) - ) { - return await users; - } else { - checkIfUserHasAccessToOrganization( - req, - permission?.organization?.id, - OrganizationAccessScope.UserAdministrationWrite - ); + if (req.user.permissions.isGlobalAdmin || permission.type.some(({ type }) => type === PermissionType.GlobalAdmin)) { + return await users; + } else { + checkIfUserHasAccessToOrganization( + req, + permission?.organization?.id, + OrganizationAccessScope.UserAdministrationWrite + ); - return users; - } + return users; } + } } diff --git a/src/controllers/user-management/user.controller.ts b/src/controllers/user-management/user.controller.ts index 6123e942..d43c60b7 100644 --- a/src/controllers/user-management/user.controller.ts +++ b/src/controllers/user-management/user.controller.ts @@ -1,19 +1,19 @@ import { - BadRequestException, - Body, - Controller, - ForbiddenException, - Get, - InternalServerErrorException, - Logger, - NotFoundException, - Param, - ParseIntPipe, - Post, - Put, - Query, - Req, - UseGuards, + BadRequestException, + Body, + Controller, + ForbiddenException, + Get, + InternalServerErrorException, + Logger, + NotFoundException, + Param, + ParseIntPipe, + Post, + Put, + Query, + Req, + UseGuards, } from "@nestjs/common"; import { ApiForbiddenResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger"; import { QueryFailedError } from "typeorm"; @@ -27,9 +27,9 @@ import { UpdateUserDto } from "@dto/user-management/update-user.dto"; import { UserResponseDto } from "@dto/user-response.dto"; import { ErrorCodes } from "@entities/enum/error-codes.enum"; import { - checkIfUserHasAccessToOrganization, - checkIfUserIsGlobalAdmin, - OrganizationAccessScope, + checkIfUserHasAccessToOrganization, + checkIfUserIsGlobalAdmin, + OrganizationAccessScope, } from "@helpers/security-helper"; import { UserService } from "@services/user-management/user.service"; import { ListAllUsersResponseDto } from "@dto/list-all-users-response.dto"; @@ -51,199 +51,193 @@ import { ApiAuth } from "@auth/swagger-auth-decorator"; @ApiTags("User Management") @Controller("user") export class UserController { - constructor(private userService: UserService, private organizationService: OrganizationService) {} + constructor(private userService: UserService, private organizationService: OrganizationService) {} - private readonly logger = new Logger(UserController.name); + private readonly logger = new Logger(UserController.name); - @Get("minimal") - @ApiOperation({ summary: "Get all id,names of users" }) - async findAllMinimal(): Promise { - return await this.userService.findAllMinimal(); - } + @Get("minimal") + @ApiOperation({ summary: "Get all id,names of users" }) + async findAllMinimal(): Promise { + return await this.userService.findAllMinimal(); + } - @Post() - @ApiOperation({ summary: "Create a new User" }) - async create(@Req() req: AuthenticatedRequest, @Body() createUserDto: CreateUserDto): Promise { - if (createUserDto.globalAdmin) { - checkIfUserIsGlobalAdmin(req); - } - try { - // Don't leak the passwordHash - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { passwordHash, ...user } = await this.userService.createUser(createUserDto, req.user.userId); - AuditLog.success(ActionType.CREATE, User.name, req.user.userId, user.id, user.name); - - return user; - } catch (err) { - AuditLog.fail(ActionType.CREATE, User.name, req.user.userId); - if ( - err instanceof QueryFailedError && - err.message.startsWith("duplicate key value violates unique constraint") - ) { - throw new BadRequestException(ErrorCodes.UserAlreadyExists); - } - - this.logger.error(err); - throw new InternalServerErrorException(); - } + @Post() + @ApiOperation({ summary: "Create a new User" }) + async create(@Req() req: AuthenticatedRequest, @Body() createUserDto: CreateUserDto): Promise { + if (createUserDto.globalAdmin) { + checkIfUserIsGlobalAdmin(req); } + try { + // Don't leak the passwordHash + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { passwordHash, ...user } = await this.userService.createUser(createUserDto, req.user.userId); + AuditLog.success(ActionType.CREATE, User.name, req.user.userId, user.id, user.name); + + return user; + } catch (err) { + AuditLog.fail(ActionType.CREATE, User.name, req.user.userId); + if (err instanceof QueryFailedError && err.message.startsWith("duplicate key value violates unique constraint")) { + throw new BadRequestException(ErrorCodes.UserAlreadyExists); + } + + this.logger.error(err); + throw new InternalServerErrorException(); + } + } + + @Put("/rejectUser") + @ApiOperation({ summary: "Rejects user and removes from awaiting users" }) + async rejectUser(@Req() req: AuthenticatedRequest, @Body() body: RejectUserDto): Promise { + checkIfUserHasAccessToOrganization(req, body.orgId, OrganizationAccessScope.UserAdministrationWrite); + + const user = await this.userService.findOne(body.userIdToReject); + const organization = await this.organizationService.findByIdWithUsers(body.orgId); + + return await this.organizationService.rejectAwaitingUser(user, organization); + } + + @Put(":id") + @ApiOperation({ summary: "Change a user" }) + async update( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number, + @Body() dto: UpdateUserDto + ): Promise { + try { + // Verify that we have admin access to the user and that the user is on an organization + const dbUser = await this.userService.findOneWithOrganizations(id); + + // Requesting user has to be admin for at least one organization containing the user + // _OR_ be global admin + if ( + !req.user.permissions.isGlobalAdmin && + !dbUser.permissions.some(perm => req.user.permissions.hasUserAdminOnOrganization(perm.organization.id)) + ) { + throw new ForbiddenException(); + } + + // Only a global admin can modify a global admin user + if (dto.globalAdmin) { + checkIfUserIsGlobalAdmin(req); + } + + // Don't leak the passwordHash + const { passwordHash: _, ...user } = await this.userService.updateUser(id, dto, req.user.userId); + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId, user.id, user.name); + + return user; + } catch (err) { + AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId, id); + if (err instanceof QueryFailedError && err.message.startsWith("duplicate key value violates unique constraint")) { + throw new BadRequestException(ErrorCodes.EmailAlreadyInUse); + } + throw err; + } + } - @Put("/rejectUser") - @ApiOperation({ summary: "Rejects user and removes from awaiting users" }) - async rejectUser(@Req() req: AuthenticatedRequest, @Body() body: RejectUserDto): Promise { - checkIfUserHasAccessToOrganization(req, body.orgId, OrganizationAccessScope.UserAdministrationWrite); - - const user = await this.userService.findOne(body.userIdToReject); - const organization = await this.organizationService.findByIdWithUsers(body.orgId); + @Put(":id/hide-welcome") + @ApiOperation({ summary: "Don't show welcome screen for a user again" }) + @Read() + async hideWelcome(@Req() req: AuthenticatedRequest): Promise { + const wasOk = await this.userService.hideWelcome(req.user.userId); - return await this.organizationService.rejectAwaitingUser(user, organization); - } + AuditLog.success(ActionType.UPDATE, User.name, req.user.userId, req.user.userId, req.user.username); - @Put(":id") - @ApiOperation({ summary: "Change a user" }) - async update( - @Req() req: AuthenticatedRequest, - @Param("id", new ParseIntPipe()) id: number, - @Body() dto: UpdateUserDto - ): Promise { - try { - // Verify that we have admin access to the user and that the user is on an organization - const dbUser = await this.userService.findOneWithOrganizations(id); - - // Requesting user has to be admin for at least one organization containing the user - // _OR_ be global admin - if ( - !req.user.permissions.isGlobalAdmin && - !dbUser.permissions.some(perm => req.user.permissions.hasUserAdminOnOrganization(perm.organization.id)) - ) { - throw new ForbiddenException(); - } - - // Only a global admin can modify a global admin user - if (dto.globalAdmin) { - checkIfUserIsGlobalAdmin(req); - } - - // Don't leak the passwordHash - const { passwordHash: _, ...user } = await this.userService.updateUser(id, dto, req.user.userId); - AuditLog.success(ActionType.UPDATE, User.name, req.user.userId, user.id, user.name); - - return user; - } catch (err) { - AuditLog.fail(ActionType.UPDATE, User.name, req.user.userId, id); - if ( - err instanceof QueryFailedError && - err.message.startsWith("duplicate key value violates unique constraint") - ) { - throw new BadRequestException(ErrorCodes.EmailAlreadyInUse); - } - throw err; - } - } + return wasOk; + } - @Put(":id/hide-welcome") - @ApiOperation({ summary: "Don't show welcome screen for a user again" }) - @Read() - async hideWelcome(@Req() req: AuthenticatedRequest): Promise { - const wasOk = await this.userService.hideWelcome(req.user.userId); + @Get("/awaitingUsers") + @ApiOperation({ summary: "Get awaiting users" }) + async findAwaitingUsers( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllEntitiesDto + ): Promise { + let organizationIds: number[] | undefined; - AuditLog.success(ActionType.UPDATE, User.name, req.user.userId, req.user.userId, req.user.username); + if (!req.user.permissions.isGlobalAdmin) { + organizationIds = req.user.permissions.getAllOrganizationsWithUserAdmin(); - return wasOk; + if (!organizationIds.length) { + throw new ForbiddenException(); + } } - @Get("/awaitingUsers") - @ApiOperation({ summary: "Get awaiting users" }) - async findAwaitingUsers( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllEntitiesDto - ): Promise { - let organizationIds: number[] | undefined; - - if (!req.user.permissions.isGlobalAdmin) { - organizationIds = req.user.permissions.getAllOrganizationsWithUserAdmin(); - - if (!organizationIds.length) { - throw new ForbiddenException(); - } - } - - try { - return await this.userService.getAwaitingUsers(query, organizationIds); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + try { + return await this.userService.getAwaitingUsers(query, organizationIds); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - - @Get("/awaitingUsers/:organizationId") - @ApiOperation({ summary: "Get awaiting users" }) - async findAwaitingUsersByOrganizationId( - @Req() req: AuthenticatedRequest, - @Param("organizationId", new ParseIntPipe()) organizationId: number, - @Query() query?: ListAllEntitiesDto - ): Promise { - // Check if user has access to organization - if (!req.user.permissions.hasUserAdminOnOrganization(organizationId)) { - throw new ForbiddenException(); - } - - try { - return await this.userService.getAwaitingUsers(query, [organizationId]); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + } + + @Get("/awaitingUsers/:organizationId") + @ApiOperation({ summary: "Get awaiting users" }) + async findAwaitingUsersByOrganizationId( + @Req() req: AuthenticatedRequest, + @Param("organizationId", new ParseIntPipe()) organizationId: number, + @Query() query?: ListAllEntitiesDto + ): Promise { + // Check if user has access to organization + if (!req.user.permissions.hasUserAdminOnOrganization(organizationId)) { + throw new ForbiddenException(); } - @Get(":id") - @ApiOperation({ summary: "Get one user" }) - async find( - @Param("id", new ParseIntPipe()) id: number, - @Query("extendedInfo") extendedInfo?: boolean - ): Promise { - const getExtendedInfo = extendedInfo != null ? extendedInfo : false; - try { - // Don't leak the passwordHash - const { passwordHash: _, ...user } = await this.userService.findOne(id, getExtendedInfo); - - return user; - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + try { + return await this.userService.getAwaitingUsers(query, [organizationId]); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - - @Get("organizationUsers/:organizationId") - @ApiOperation({ - summary: "Get all users for an organization. Requires UserAdmin priviledges for the specified organization", - }) - async findByOrganizationId( - @Req() req: AuthenticatedRequest, - @Param("organizationId", new ParseIntPipe()) organizationId: number, - @Query() query?: ListAllEntitiesDto - ): Promise { - try { - // Check if user has access to organization - if (!req.user.permissions.hasUserAdminOnOrganization(organizationId)) { - throw new ForbiddenException("User does not have org admin permissions for this organization"); - } - - // Get user objects - return await this.userService.getUsersOnOrganization(organizationId, query); - } catch (err) { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } + } + + @Get(":id") + @ApiOperation({ summary: "Get one user" }) + async find( + @Param("id", new ParseIntPipe()) id: number, + @Query("extendedInfo") extendedInfo?: boolean + ): Promise { + const getExtendedInfo = extendedInfo != null ? extendedInfo : false; + try { + // Don't leak the passwordHash + const { passwordHash: _, ...user } = await this.userService.findOne(id, getExtendedInfo); + + return user; + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); } - - @Get() - @ApiOperation({ summary: "Get all users" }) - async findAll( - @Req() req: AuthenticatedRequest, - @Query() query?: ListAllEntitiesDto - ): Promise { - if (req.user.permissions.isGlobalAdmin) { - return await this.userService.findAll(query); - } else { - const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); - return await this.userService.getUsersOnOrganizations(allowedOrganizations, query); - } + } + + @Get("organizationUsers/:organizationId") + @ApiOperation({ + summary: "Get all users for an organization. Requires UserAdmin priviledges for the specified organization", + }) + async findByOrganizationId( + @Req() req: AuthenticatedRequest, + @Param("organizationId", new ParseIntPipe()) organizationId: number, + @Query() query?: ListAllEntitiesDto + ): Promise { + try { + // Check if user has access to organization + if (!req.user.permissions.hasUserAdminOnOrganization(organizationId)) { + throw new ForbiddenException("User does not have org admin permissions for this organization"); + } + + // Get user objects + return await this.userService.getUsersOnOrganization(organizationId, query); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } + + @Get() + @ApiOperation({ summary: "Get all users" }) + async findAll( + @Req() req: AuthenticatedRequest, + @Query() query?: ListAllEntitiesDto + ): Promise { + if (req.user.permissions.isGlobalAdmin) { + return await this.userService.findAll(query); + } else { + const allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); + return await this.userService.getUsersOnOrganizations(allowedOrganizations, query); } + } } diff --git a/src/default-service/default-service.service.spec.ts b/src/default-service/default-service.service.spec.ts index 30cc0fa4..e869ad8d 100644 --- a/src/default-service/default-service.service.spec.ts +++ b/src/default-service/default-service.service.spec.ts @@ -2,17 +2,17 @@ import { Test, TestingModule } from "@nestjs/testing"; import { HealthCheckService } from "../services/health/health-check.service"; describe("DefaultServiceService", () => { - let service: HealthCheckService; + let service: HealthCheckService; - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [HealthCheckService], - }).compile(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [HealthCheckService], + }).compile(); - service = module.get(HealthCheckService); - }); + service = module.get(HealthCheckService); + }); - it("should be defined", () => { - expect(service).toBeDefined(); - }); + it("should be defined", () => { + expect(service).toBeDefined(); + }); }); diff --git a/src/entities/api-key.entity.ts b/src/entities/api-key.entity.ts index e0857227..d7ef790d 100644 --- a/src/entities/api-key.entity.ts +++ b/src/entities/api-key.entity.ts @@ -1,34 +1,26 @@ import { User } from "@entities/user.entity"; import { nameof } from "@helpers/type-helper"; -import { - Column, - Entity, - JoinColumn, - JoinTable, - ManyToMany, - OneToOne, - Unique, -} from "typeorm"; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, OneToOne, Unique } from "typeorm"; import { DbBaseEntity } from "./base.entity"; import { Permission } from "./permissions/permission.entity"; @Entity("api_key") @Unique([nameof("key")]) export class ApiKey extends DbBaseEntity { - @Column() - key: string; + @Column() + key: string; - @Column() - name: string; + @Column() + name: string; - @ManyToMany(_ => Permission, apiKeyPm => apiKeyPm.apiKeys) - @JoinTable() - permissions: Permission[]; + @ManyToMany(_ => Permission, apiKeyPm => apiKeyPm.apiKeys) + @JoinTable() + permissions: Permission[]; - @OneToOne(() => User, u => u.apiKeyRef, { - nullable: false, - cascade: true, - }) - @JoinColumn() - systemUser: User; + @OneToOne(() => User, u => u.apiKeyRef, { + nullable: false, + cascade: true, + }) + @JoinColumn() + systemUser: User; } diff --git a/src/entities/application-device-type.entity.ts b/src/entities/application-device-type.entity.ts index 9f25c888..7205ea8a 100644 --- a/src/entities/application-device-type.entity.ts +++ b/src/entities/application-device-type.entity.ts @@ -5,13 +5,13 @@ import { DbBaseEntity } from "./base.entity"; @Entity("application_device_type") export class ApplicationDeviceType extends DbBaseEntity { - @Column() - type: ApplicationDeviceTypeUnion; + @Column() + type: ApplicationDeviceTypeUnion; - @ManyToOne(() => Application, application => application.deviceTypes, { - onDelete: "CASCADE", - // Delete the row instead of null'ing application. Useful for updates - orphanedRowAction: "delete", - }) - application: Application; + @ManyToOne(() => Application, application => application.deviceTypes, { + onDelete: "CASCADE", + // Delete the row instead of null'ing application. Useful for updates + orphanedRowAction: "delete", + }) + application: Application; } diff --git a/src/entities/application.entity.ts b/src/entities/application.entity.ts index 3a3862e7..1033fdef 100644 --- a/src/entities/application.entity.ts +++ b/src/entities/application.entity.ts @@ -3,15 +3,15 @@ import { IoTDevice } from "@entities/iot-device.entity"; import { Organization } from "@entities/organization.entity"; import { ApplicationStatus } from "@enum/application-status.enum"; import { - Column, - CreateDateColumn, - Entity, - JoinTable, - ManyToMany, - ManyToOne, - OneToMany, - RelationId, - Unique, + Column, + CreateDateColumn, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + RelationId, + Unique, } from "typeorm"; import { ApplicationDeviceType } from "./application-device-type.entity"; import { ControlledProperty } from "./controlled-property.entity"; @@ -23,97 +23,97 @@ import { nameof } from "@helpers/type-helper"; @Entity("application") @Unique(["name"]) export class Application extends DbBaseEntity { - @Column() - name: string; - - @Column({ nullable: true }) - description: string; - - @OneToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => IoTDevice, - iotdevice => iotdevice.application, - { onDelete: "CASCADE" } - ) - iotDevices: IoTDevice[]; - - @OneToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => DataTarget, - datatarget => datatarget.application, - { onDelete: "CASCADE" } - ) - dataTargets: DataTarget[]; - - @OneToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Multicast, - multicasts => multicasts.application, - { onDelete: "CASCADE" } - ) - multicasts: Multicast[]; - - @ManyToOne( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Organization, - organization => organization.applications, - { onDelete: "CASCADE" } - ) - belongsTo: Organization; - - @ManyToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Permission, - permission => permission.applications - ) - @JoinTable() - permissions: Permission[]; - - @RelationId(nameof("permissions")) - permissionIds: Permission["id"][]; - - @Column({ nullable: true }) - status?: ApplicationStatus; - - @CreateDateColumn({ nullable: true }) - startDate?: Date; - - @CreateDateColumn({ nullable: true }) - endDate?: Date; - - @Column({ nullable: true }) - category?: string; - - @Column({ nullable: true }) - owner?: string; - - @Column({ nullable: true }) - contactPerson?: string; - - @Column({ nullable: true }) - contactEmail?: string; - - @Column({ nullable: true }) - contactPhone?: string; - - @Column({ nullable: true }) - personalData?: boolean; - - @Column({ nullable: true }) - hardware?: string; - - @OneToMany(() => ControlledProperty, entity => entity.application, { - nullable: true, - cascade: true, - }) - controlledProperties?: ControlledProperty[]; - - @OneToMany(() => ApplicationDeviceType, entity => entity.application, { - nullable: true, - cascade: true, - }) - deviceTypes?: ApplicationDeviceType[]; - - @Column({ nullable: true }) - chirpstackId?: string; + @Column() + name: string; + + @Column({ nullable: true }) + description: string; + + @OneToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => IoTDevice, + iotdevice => iotdevice.application, + { onDelete: "CASCADE" } + ) + iotDevices: IoTDevice[]; + + @OneToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => DataTarget, + datatarget => datatarget.application, + { onDelete: "CASCADE" } + ) + dataTargets: DataTarget[]; + + @OneToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => Multicast, + multicasts => multicasts.application, + { onDelete: "CASCADE" } + ) + multicasts: Multicast[]; + + @ManyToOne( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => Organization, + organization => organization.applications, + { onDelete: "CASCADE" } + ) + belongsTo: Organization; + + @ManyToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => Permission, + permission => permission.applications + ) + @JoinTable() + permissions: Permission[]; + + @RelationId(nameof("permissions")) + permissionIds: Permission["id"][]; + + @Column({ nullable: true }) + status?: ApplicationStatus; + + @CreateDateColumn({ nullable: true }) + startDate?: Date; + + @CreateDateColumn({ nullable: true }) + endDate?: Date; + + @Column({ nullable: true }) + category?: string; + + @Column({ nullable: true }) + owner?: string; + + @Column({ nullable: true }) + contactPerson?: string; + + @Column({ nullable: true }) + contactEmail?: string; + + @Column({ nullable: true }) + contactPhone?: string; + + @Column({ nullable: true }) + personalData?: boolean; + + @Column({ nullable: true }) + hardware?: string; + + @OneToMany(() => ControlledProperty, entity => entity.application, { + nullable: true, + cascade: true, + }) + controlledProperties?: ControlledProperty[]; + + @OneToMany(() => ApplicationDeviceType, entity => entity.application, { + nullable: true, + cascade: true, + }) + deviceTypes?: ApplicationDeviceType[]; + + @Column({ nullable: true }) + chirpstackId?: string; } diff --git a/src/entities/audit-log-entry.ts b/src/entities/audit-log-entry.ts index 9b80a08a..18d3e1ab 100644 --- a/src/entities/audit-log-entry.ts +++ b/src/entities/audit-log-entry.ts @@ -1,15 +1,15 @@ export class AuditLogEntry { - timestamp: Date; - actionType: ActionType; - type: string; - completed?: boolean; - id?: number | string; - name?: string; - userId: number; + timestamp: Date; + actionType: ActionType; + type: string; + completed?: boolean; + id?: number | string; + name?: string; + userId: number; } export enum ActionType { - DELETE = "DELETE", - CREATE = "CREATE", - UPDATE = "UPDATE", + DELETE = "DELETE", + CREATE = "CREATE", + UPDATE = "UPDATE", } diff --git a/src/entities/base.entity.ts b/src/entities/base.entity.ts index da07bc9d..01463f63 100644 --- a/src/entities/base.entity.ts +++ b/src/entities/base.entity.ts @@ -6,20 +6,20 @@ import { CreateDateColumn, JoinColumn, ManyToOne, PrimaryGeneratedColumn, Update * Name is DbBaseEntity to not clash with BaseEntity from TypeORM (which is used in the active record). */ export abstract class DbBaseEntity { - @PrimaryGeneratedColumn() - id: number; + @PrimaryGeneratedColumn() + id: number; - @CreateDateColumn() - createdAt: Date; + @CreateDateColumn() + createdAt: Date; - @UpdateDateColumn() - updatedAt: Date; + @UpdateDateColumn() + updatedAt: Date; - @ManyToOne("User", { nullable: true }) - @JoinColumn() - createdBy?: number; + @ManyToOne("User", { nullable: true }) + @JoinColumn() + createdBy?: number; - @ManyToOne("User", { nullable: true }) - @JoinColumn() - updatedBy?: number; + @ManyToOne("User", { nullable: true }) + @JoinColumn() + updatedBy?: number; } diff --git a/src/entities/controlled-property.entity.ts b/src/entities/controlled-property.entity.ts index a23f4008..35058cbb 100644 --- a/src/entities/controlled-property.entity.ts +++ b/src/entities/controlled-property.entity.ts @@ -5,13 +5,13 @@ import { DbBaseEntity } from "./base.entity"; @Entity("controlled_property") export class ControlledProperty extends DbBaseEntity { - @Column() - type: ControlledPropertyTypes; + @Column() + type: ControlledPropertyTypes; - @ManyToOne(() => Application, application => application.controlledProperties, { - onDelete: "CASCADE", - // Delete the row instead of null'ing application. Useful for updates - orphanedRowAction: "delete", - }) - application: Application; + @ManyToOne(() => Application, application => application.controlledProperties, { + onDelete: "CASCADE", + // Delete the row instead of null'ing application. Useful for updates + orphanedRowAction: "delete", + }) + application: Application; } diff --git a/src/entities/data-target.entity.ts b/src/entities/data-target.entity.ts index e5d8b886..c8bd464d 100644 --- a/src/entities/data-target.entity.ts +++ b/src/entities/data-target.entity.ts @@ -1,11 +1,4 @@ -import { - Column, - Entity, - ManyToOne, - OneToMany, - OneToOne, - TableInheritance, -} from "typeorm"; +import { Column, Entity, ManyToOne, OneToMany, OneToOne, TableInheritance } from "typeorm"; import { Application } from "@entities/application.entity"; import { DbBaseEntity } from "@entities/base.entity"; @@ -15,32 +8,32 @@ import { IoTDevicePayloadDecoderDataTargetConnection } from "./iot-device-payloa @Entity("data_target") @TableInheritance({ - column: { type: "enum", name: "type", enum: DataTargetType }, + column: { type: "enum", name: "type", enum: DataTargetType }, }) export abstract class DataTarget extends DbBaseEntity { - @Column("enum", { - enum: DataTargetType, - }) - type: DataTargetType; + @Column("enum", { + enum: DataTargetType, + }) + type: DataTargetType; - @Column() - name: string; + @Column() + name: string; - @ManyToOne( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Application, - application => application.iotDevices, - { onDelete: "CASCADE" } - ) - application: Application; + @ManyToOne( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => Application, + application => application.iotDevices, + { onDelete: "CASCADE" } + ) + application: Application; - @OneToMany(type => IoTDevicePayloadDecoderDataTargetConnection, c => c.dataTarget) - connections: IoTDevicePayloadDecoderDataTargetConnection[]; + @OneToMany(type => IoTDevicePayloadDecoderDataTargetConnection, c => c.dataTarget) + connections: IoTDevicePayloadDecoderDataTargetConnection[]; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @OneToOne(type => OpenDataDkDataset, o => o.dataTarget, { - nullable: true, - cascade: true, - }) - openDataDkDataset: OpenDataDkDataset; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @OneToOne(type => OpenDataDkDataset, o => o.dataTarget, { + nullable: true, + cascade: true, + }) + openDataDkDataset: OpenDataDkDataset; } diff --git a/src/entities/device-model.entity.ts b/src/entities/device-model.entity.ts index 7a44ac6a..0e65c4a3 100644 --- a/src/entities/device-model.entity.ts +++ b/src/entities/device-model.entity.ts @@ -5,15 +5,15 @@ import { Organization } from "./organization.entity"; @Entity("device_model") export class DeviceModel extends DbBaseEntity { - @Column({ type: "jsonb", nullable: true }) - body: JSON; + @Column({ type: "jsonb", nullable: true }) + body: JSON; - @ManyToOne(type => Organization, organization => organization.deviceModels, { - onDelete: "CASCADE", - }) - @JoinColumn() - belongsTo: Organization; + @ManyToOne(type => Organization, organization => organization.deviceModels, { + onDelete: "CASCADE", + }) + @JoinColumn() + belongsTo: Organization; - @OneToMany(type => IoTDevice, device => device.deviceModel) - devices: IoTDevice[]; + @OneToMany(type => IoTDevice, device => device.deviceModel) + devices: IoTDevice[]; } diff --git a/src/entities/dto/api-key/create-api-key.dto.ts b/src/entities/dto/api-key/create-api-key.dto.ts index 0766c086..df35483e 100644 --- a/src/entities/dto/api-key/create-api-key.dto.ts +++ b/src/entities/dto/api-key/create-api-key.dto.ts @@ -2,20 +2,20 @@ import { ApiProperty } from "@nestjs/swagger"; import { ArrayNotEmpty, ArrayUnique, IsArray, IsString, Length } from "class-validator"; export class CreateApiKeyDto { - @ApiProperty({ required: true }) - @IsString() - @Length(2, 50) - name: string; + @ApiProperty({ required: true }) + @IsString() + @Length(2, 50) + name: string; - @ApiProperty({ - required: true, - type: "array", - items: { - type: "number", - }, - }) - @IsArray() - @ArrayNotEmpty() - @ArrayUnique() - permissionIds: number[]; + @ApiProperty({ + required: true, + type: "array", + items: { + type: "number", + }, + }) + @IsArray() + @ArrayNotEmpty() + @ArrayUnique() + permissionIds: number[]; } diff --git a/src/entities/dto/api-key/list-all-api-keys.dto.ts b/src/entities/dto/api-key/list-all-api-keys.dto.ts index 51a7664e..dcca180f 100644 --- a/src/entities/dto/api-key/list-all-api-keys.dto.ts +++ b/src/entities/dto/api-key/list-all-api-keys.dto.ts @@ -2,6 +2,6 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { ApiProperty } from "@nestjs/swagger"; export class ListAllApiKeysDto extends ListAllEntitiesDto { - @ApiProperty({ required: true }) - organizationId: number; + @ApiProperty({ required: true }) + organizationId: number; } diff --git a/src/entities/dto/chirpstack-add-device-multicast.dto.ts b/src/entities/dto/chirpstack-add-device-multicast.dto.ts index 9c26f6b1..ae99eb35 100644 --- a/src/entities/dto/chirpstack-add-device-multicast.dto.ts +++ b/src/entities/dto/chirpstack-add-device-multicast.dto.ts @@ -1,4 +1,4 @@ export class AddDeviceToMulticastDto { - devEUI: string; - multicastGroupID: string; + devEUI: string; + multicastGroupID: string; } diff --git a/src/entities/dto/chirpstack/adr-algorithm.dto.ts b/src/entities/dto/chirpstack/adr-algorithm.dto.ts index b0cbbd0d..aa4fe3b2 100644 --- a/src/entities/dto/chirpstack/adr-algorithm.dto.ts +++ b/src/entities/dto/chirpstack/adr-algorithm.dto.ts @@ -1,17 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; -import { - IsString, - Length, -} from "class-validator"; +import { IsString, Length } from "class-validator"; export class AdrAlgorithmDto { - @ApiProperty({ required: true }) - @IsString() - @Length(1, 1024) - id: string; + @ApiProperty({ required: true }) + @IsString() + @Length(1, 1024) + id: string; - @ApiProperty({ required: true }) - @IsString() - @Length(1, 1024) - name: string; + @ApiProperty({ required: true }) + @IsString() + @Length(1, 1024) + name: string; } diff --git a/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts b/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts index 48d0a3d2..2d588db4 100644 --- a/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts +++ b/src/entities/dto/chirpstack/backend/gateway-all-status.dto.ts @@ -9,14 +9,14 @@ import { GatewayStatus } from "./gateway-status.dto"; export class GatewayGetAllStatusResponseDto extends ListAllEntitiesResponseDto {} export class ListAllGatewayStatusDto extends ListAllEntitiesDto { - @IsSwaggerOptional() - @StringToNumber() - organizationId?: number; + @IsSwaggerOptional() + @StringToNumber() + organizationId?: number; - @IsSwaggerOptional({ - default: GatewayStatusInterval.DAY, - enum: GatewayStatusInterval, - }) - @IsEnum(GatewayStatusInterval) - timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; + @IsSwaggerOptional({ + default: GatewayStatusInterval.DAY, + enum: GatewayStatusInterval, + }) + @IsEnum(GatewayStatusInterval) + timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; } diff --git a/src/entities/dto/chirpstack/backend/gateway-status.dto.ts b/src/entities/dto/chirpstack/backend/gateway-status.dto.ts index 4acff60d..c0e035cc 100644 --- a/src/entities/dto/chirpstack/backend/gateway-status.dto.ts +++ b/src/entities/dto/chirpstack/backend/gateway-status.dto.ts @@ -3,21 +3,21 @@ import { IsSwaggerOptional } from "@helpers/optional-validator"; import { IsEnum } from "class-validator"; export interface StatusTimestamp { - timestamp: Date; - wasOnline: boolean; + timestamp: Date; + wasOnline: boolean; } export interface GatewayStatus { - id: string; - name: string; - statusTimestamps: StatusTimestamp[]; + id: string; + name: string; + statusTimestamps: StatusTimestamp[]; } export class GetGatewayStatusQuery { - @IsSwaggerOptional({ - default: GatewayStatusInterval.DAY, - enum: GatewayStatusInterval, - }) - @IsEnum(GatewayStatusInterval) - timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; + @IsSwaggerOptional({ + default: GatewayStatusInterval.DAY, + enum: GatewayStatusInterval, + }) + @IsEnum(GatewayStatusInterval) + timeInterval: GatewayStatusInterval = GatewayStatusInterval.DAY; } diff --git a/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts index e80434c5..1ecfc314 100644 --- a/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-application-response.dto.ts @@ -1,6 +1,6 @@ export class ChirpstackApplicationResponseDto { - id: string; - name: string; - description: string; - tenantId?: string; + id: string; + name: string; + description: string; + tenantId?: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-application.dto.ts b/src/entities/dto/chirpstack/chirpstack-application.dto.ts index 06365798..7023f130 100644 --- a/src/entities/dto/chirpstack/chirpstack-application.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-application.dto.ts @@ -1,4 +1,4 @@ export class ChirpstackApplicationDto { - name: string; - description: string; + name: string; + description: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-boards.dto.ts b/src/entities/dto/chirpstack/chirpstack-boards.dto.ts index 06136288..f5ec03b2 100644 --- a/src/entities/dto/chirpstack/chirpstack-boards.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-boards.dto.ts @@ -2,13 +2,13 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsString, Matches } from "class-validator"; export class ChirpstackBoardsDto { - @ApiProperty({ required: false }) - @IsString() - @Matches(/[0-9A-Fa-f]{32}/) - fineTimestampKey: string; + @ApiProperty({ required: false }) + @IsString() + @Matches(/[0-9A-Fa-f]{32}/) + fineTimestampKey: string; - @ApiProperty({ required: false }) - @IsString() - @Matches(/[0-9A-Fa-f]{16}/) - fpgaID: string; + @ApiProperty({ required: false }) + @IsString() + @Matches(/[0-9A-Fa-f]{16}/) + fpgaID: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-activation-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-activation-response.dto.ts index 7feb84eb..2cf14608 100644 --- a/src/entities/dto/chirpstack/chirpstack-device-activation-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-device-activation-response.dto.ts @@ -1,15 +1,15 @@ export class ChirpstackDeviceActivationDto { - deviceActivation: ChirpstackDeviceActivationContentsDto; + deviceActivation: ChirpstackDeviceActivationContentsDto; } export class ChirpstackDeviceActivationContentsDto { - devEUI: string; - devAddr: string; - appSKey: string; - nwkSEncKey: string; - sNwkSIntKey: string; - fNwkSIntKey: string; - fCntUp: number; - nFCntDown: number; - aFCntDown: number; + devEUI: string; + devAddr: string; + appSKey: string; + nwkSEncKey: string; + sNwkSIntKey: string; + fNwkSIntKey: string; + fCntUp: number; + nFCntDown: number; + aFCntDown: number; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts index 8ef466b8..23f68617 100644 --- a/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-device-contents.dto.ts @@ -2,42 +2,42 @@ import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { IsHexadecimal, IsOptional, IsString, IsUUID, Length } from "class-validator"; export class ChirpstackDeviceContentsDto { - @ApiHideProperty() - name?: string; + @ApiHideProperty() + name?: string; - @ApiHideProperty() - description?: string; + @ApiHideProperty() + description?: string; - @ApiHideProperty() - applicationID?: string; + @ApiHideProperty() + applicationID?: string; - @ApiProperty({ required: true }) - @IsString() - @IsHexadecimal() - @Length(16, 16) - devEUI: string; + @ApiProperty({ required: true }) + @IsString() + @IsHexadecimal() + @Length(16, 16) + devEUI: string; - @ApiProperty({ required: true }) - @IsUUID() - deviceProfileID: string; + @ApiProperty({ required: true }) + @IsUUID() + deviceProfileID: string; - @ApiProperty({ required: false, default: false }) - @IsOptional() - isDisabled?: boolean; + @ApiProperty({ required: false, default: false }) + @IsOptional() + isDisabled?: boolean; - @ApiProperty({ required: false, default: false }) - @IsOptional() - skipFCntCheck?: boolean; + @ApiProperty({ required: false, default: false }) + @IsOptional() + skipFCntCheck?: boolean; - @ApiProperty({ required: false, default: {} }) - variables?: Array<[string, string]>; + @ApiProperty({ required: false, default: {} }) + variables?: Array<[string, string]>; - @ApiProperty({ required: false, default: {} }) - tags?: Array<[string, string]>; + @ApiProperty({ required: false, default: {} }) + tags?: Array<[string, string]>; - @ApiHideProperty() - deviceStatusBattery?: number; + @ApiHideProperty() + deviceStatusBattery?: number; - @ApiHideProperty() - deviceStatusMargin?: number; + @ApiHideProperty() + deviceStatusMargin?: number; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-downlink-queue-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-downlink-queue-response.dto.ts index 054764fc..ff1e3343 100644 --- a/src/entities/dto/chirpstack/chirpstack-device-downlink-queue-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-device-downlink-queue-response.dto.ts @@ -1,13 +1,13 @@ export interface DeviceQueueItem { - devEUI?: string; - confirmed?: boolean; - fCnt?: number; - fPort?: number; - data: string; - jsonObject?: string; + devEUI?: string; + confirmed?: boolean; + fCnt?: number; + fPort?: number; + data: string; + jsonObject?: string; } export interface DeviceDownlinkQueueResponseDto { - deviceQueueItems: DeviceQueueItem[]; - totalCount: number; + deviceQueueItems: DeviceQueueItem[]; + totalCount: number; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts index 7b9a7ada..2279440d 100644 --- a/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-device-keys-response.dto.ts @@ -1,9 +1,9 @@ export class ChirpstackDeviceKeysResponseDto { - deviceKeys: ChirpstackDeviceKeysContentDto; + deviceKeys: ChirpstackDeviceKeysContentDto; } export class ChirpstackDeviceKeysContentDto { - devEUI: string; - nwkKey: string; - appKey: string; + devEUI: string; + nwkKey: string; + appKey: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-device-metrics.dto.ts b/src/entities/dto/chirpstack/chirpstack-device-metrics.dto.ts index 294642df..95a9e9b1 100644 --- a/src/entities/dto/chirpstack/chirpstack-device-metrics.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-device-metrics.dto.ts @@ -1,13 +1,13 @@ export class DeviceMetricsDto { - [timestamp: string]: { - rssi: number; - snr: number; - rxPacketsPerDr: Record; - }; + [timestamp: string]: { + rssi: number; + snr: number; + rxPacketsPerDr: Record; + }; } export enum MetricProperties { - rssi = "rssi", - snr = "snr", - dr = "rxPacketsPerDr", + rssi = "rssi", + snr = "snr", + dr = "rxPacketsPerDr", } diff --git a/src/entities/dto/chirpstack/chirpstack-error-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-error-response.dto.ts index 2e8d967e..e958b880 100644 --- a/src/entities/dto/chirpstack/chirpstack-error-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-error-response.dto.ts @@ -1,6 +1,6 @@ export class ChirpstackErrorResponseDto { - error: string; - code: number; - message: string; - details: any[]; + error: string; + code: number; + message: string; + details: any[]; } diff --git a/src/entities/dto/chirpstack/chirpstack-many-device-response.ts b/src/entities/dto/chirpstack/chirpstack-many-device-response.ts index cc7633c7..fa43b2f5 100644 --- a/src/entities/dto/chirpstack/chirpstack-many-device-response.ts +++ b/src/entities/dto/chirpstack/chirpstack-many-device-response.ts @@ -1,16 +1,16 @@ export interface ChirpstackDeviceResponseContents { - devEUI: string; - name: string; - description: string; - deviceProfileID: string; - deviceProfileName: string; - deviceStatusBattery: number; - deviceStatusMargin: number; - deviceStatusExternalPowerSource: boolean; - lastSeenAt?: Date; + devEUI: string; + name: string; + description: string; + deviceProfileID: string; + deviceProfileName: string; + deviceStatusBattery: number; + deviceStatusMargin: number; + deviceStatusExternalPowerSource: boolean; + lastSeenAt?: Date; } export interface ChirpstackManyDeviceResponseDto { - totalCount: string; - result: ChirpstackDeviceResponseContents[]; + totalCount: string; + result: ChirpstackDeviceResponseContents[]; } diff --git a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts index fd7b7eae..83d9d70b 100644 --- a/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-mqtt-message.dto.ts @@ -1,28 +1,28 @@ export class ChirpstackMQTTMessageDto { - adr: boolean; - data: string; - fCnt: number; - fPort: number; - deviceInfo: ChirpstackMQTTMessageDeviceInfo; - txInfo: ChirpstackMQTTMessageTxInfoDto; - dr: number; - frequency: number; - confirmed: boolean + adr: boolean; + data: string; + fCnt: number; + fPort: number; + deviceInfo: ChirpstackMQTTMessageDeviceInfo; + txInfo: ChirpstackMQTTMessageTxInfoDto; + dr: number; + frequency: number; + confirmed: boolean; } export class ChirpstackMQTTMessageTxInfoDto { - frequency: number; - dr: number; + frequency: number; + dr: number; } export class ChirpstackMQTTConnectionStateMessageDto { - gatewayId: string; - isOnline: boolean; + gatewayId: string; + isOnline: boolean; } export class ChirpstackMQTTMessageDeviceInfo { - applicationID: string; - applicationName: string; - devEui: string; - deviceName: string; + applicationID: string; + applicationName: string; + devEui: string; + deviceName: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts b/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts index 9228164e..1b85e781 100644 --- a/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-multicast-contents.dto.ts @@ -2,33 +2,33 @@ import { multicastGroup } from "@enum/multicast-type.enum"; import { ApiProperty } from "@nestjs/swagger"; export class ChirpstackMulticastContentsDto { - @ApiProperty({ required: true }) - applicationID: string; + @ApiProperty({ required: true }) + applicationID: string; - @ApiProperty({ required: true }) - dr: number; + @ApiProperty({ required: true }) + dr: number; - @ApiProperty({ required: true }) - fCnt: number; + @ApiProperty({ required: true }) + fCnt: number; - @ApiProperty({ required: true }) - frequency: number; + @ApiProperty({ required: true }) + frequency: number; - @ApiProperty({ required: true }) - groupType: multicastGroup; + @ApiProperty({ required: true }) + groupType: multicastGroup; - @ApiProperty({ required: true }) - mcAddr: string; + @ApiProperty({ required: true }) + mcAddr: string; - @ApiProperty({ required: true }) - mcAppSKey: string; + @ApiProperty({ required: true }) + mcAppSKey: string; - @ApiProperty({ required: true }) - mcNwkSKey: string; + @ApiProperty({ required: true }) + mcNwkSKey: string; - @ApiProperty({ required: true }) - name: string; + @ApiProperty({ required: true }) + name: string; - @ApiProperty({ required: true }) - id: string; + @ApiProperty({ required: true }) + id: string; } diff --git a/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts index 13b8f9f6..fa4c0e25 100644 --- a/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto.ts @@ -1,10 +1,10 @@ export interface MulticastQueueItem { - multicastGroupId?: string; - fCnt?: number; - fPort?: number; - data: string; + multicastGroupId?: string; + fCnt?: number; + fPort?: number; + data: string; } export interface MulticastDownlinkQueueResponseDto { - deviceQueueItems: MulticastQueueItem[]; + deviceQueueItems: MulticastQueueItem[]; } diff --git a/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts b/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts index 8752d23a..f2d600a7 100644 --- a/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-paginated-list.dto.ts @@ -2,13 +2,13 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsOptional } from "class-validator"; export class ChirpstackPaginatedListDto { - @ApiProperty({ type: Number, required: false }) - @IsOptional() - limit? = 100; - @ApiProperty({ type: Number, required: false }) - @IsOptional() - offset? = 0; - @ApiProperty({ type: Number, required: false }) - @IsOptional() - organizationId?: number; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + limit? = 100; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + offset? = 0; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + organizationId?: number; } diff --git a/src/entities/dto/chirpstack/chirpstack-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-response.dto.ts index 81ba9671..374448c3 100644 --- a/src/entities/dto/chirpstack/chirpstack-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-response.dto.ts @@ -1,6 +1,6 @@ import { ChirpstackErrorResponseDto } from "./chirpstack-error-response.dto"; export class ChirpstackResponseStatus { - success: boolean; - chirpstackError?: ChirpstackErrorResponseDto; + success: boolean; + chirpstackError?: ChirpstackErrorResponseDto; } diff --git a/src/entities/dto/chirpstack/chirpstack-single-application-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-single-application-response.dto.ts index 49091d7f..a431599a 100644 --- a/src/entities/dto/chirpstack/chirpstack-single-application-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-single-application-response.dto.ts @@ -1,5 +1,5 @@ import { ChirpstackApplicationResponseDto } from "./chirpstack-application-response.dto"; export class ChirpstackSingleApplicationResponseDto { - application: ChirpstackApplicationResponseDto; + application: ChirpstackApplicationResponseDto; } diff --git a/src/entities/dto/chirpstack/chirpstack-single-device-response.dto.ts b/src/entities/dto/chirpstack/chirpstack-single-device-response.dto.ts index 945a9fa2..9f4e9402 100644 --- a/src/entities/dto/chirpstack/chirpstack-single-device-response.dto.ts +++ b/src/entities/dto/chirpstack/chirpstack-single-device-response.dto.ts @@ -1,11 +1,11 @@ import { ChirpstackDeviceContentsDto } from "./chirpstack-device-contents.dto"; export class ChirpstackSingleDeviceResponseDto { - device: ChirpstackDeviceContentsDto; + device: ChirpstackDeviceContentsDto; - lastSeenAt: string; + lastSeenAt: string; - deviceStatusBattery: number; + deviceStatusBattery: number; - deviceStatusMargin: number; + deviceStatusMargin: number; } diff --git a/src/entities/dto/chirpstack/common-location.dto.ts b/src/entities/dto/chirpstack/common-location.dto.ts index 250d5948..b3f6c16a 100644 --- a/src/entities/dto/chirpstack/common-location.dto.ts +++ b/src/entities/dto/chirpstack/common-location.dto.ts @@ -3,24 +3,24 @@ import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { IsOptional, IsString, Max, Min } from "class-validator"; export class CommonLocationDto { - @ApiProperty({ required: true }) - @Max(180.0) - @Min(-180.0) - longitude: number; + @ApiProperty({ required: true }) + @Max(180.0) + @Min(-180.0) + longitude: number; - @ApiProperty({ required: true }) - @Max(90.0) - @Min(-90.0) - latitude: number; + @ApiProperty({ required: true }) + @Max(90.0) + @Min(-90.0) + latitude: number; - @ApiProperty({ required: false }) - @IsOptional() - altitude?: number; + @ApiProperty({ required: false }) + @IsOptional() + altitude?: number; - @ApiHideProperty() - source?: LocationSourceMap[keyof LocationSourceMap] + @ApiHideProperty() + source?: LocationSourceMap[keyof LocationSourceMap]; - @ApiProperty({ required: false }) - @IsOptional() - accuracy?: number; + @ApiProperty({ required: false }) + @IsOptional() + accuracy?: number; } diff --git a/src/entities/dto/chirpstack/create-chirpstack-application.dto.ts b/src/entities/dto/chirpstack/create-chirpstack-application.dto.ts index 0f6f0c16..acd21602 100644 --- a/src/entities/dto/chirpstack/create-chirpstack-application.dto.ts +++ b/src/entities/dto/chirpstack/create-chirpstack-application.dto.ts @@ -6,8 +6,8 @@ import { ChirpstackApplicationDto } from "./chirpstack-application.dto"; import { ChirpstackDeviceContentsDto } from "./chirpstack-device-contents.dto"; export class CreateChirpstackApplicationDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => ChirpstackDeviceContentsDto) - application: ChirpstackApplicationDto; + @ApiProperty({ required: true }) + @ValidateNested({ each: true }) + @Type(() => ChirpstackDeviceContentsDto) + application: ChirpstackApplicationDto; } diff --git a/src/entities/dto/chirpstack/create-chirpstack-device-queue-item.dto.ts b/src/entities/dto/chirpstack/create-chirpstack-device-queue-item.dto.ts index 78c1edc0..c74c1247 100644 --- a/src/entities/dto/chirpstack/create-chirpstack-device-queue-item.dto.ts +++ b/src/entities/dto/chirpstack/create-chirpstack-device-queue-item.dto.ts @@ -1,14 +1,14 @@ export interface CreateChirpstackDeviceQueueItemDto { - deviceQueueItem: DeviceQueueItem; + deviceQueueItem: DeviceQueueItem; } export interface DeviceQueueItem { - fPort: number; - data: string; - confirmed: boolean; - devEUI: string; + fPort: number; + data: string; + confirmed: boolean; + devEUI: string; } export interface CreateChirpstackDeviceQueueItemResponse { - fCnt: number; + fCnt: number; } diff --git a/src/entities/dto/chirpstack/create-chirpstack-device.dto.ts b/src/entities/dto/chirpstack/create-chirpstack-device.dto.ts index 817992aa..fe7aeff5 100644 --- a/src/entities/dto/chirpstack/create-chirpstack-device.dto.ts +++ b/src/entities/dto/chirpstack/create-chirpstack-device.dto.ts @@ -5,8 +5,8 @@ import { ValidateNested } from "class-validator"; import { ChirpstackDeviceContentsDto } from "@dto/chirpstack/chirpstack-device-contents.dto"; export class CreateChirpstackDeviceDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => ChirpstackDeviceContentsDto) - device: ChirpstackDeviceContentsDto; + @ApiProperty({ required: true }) + @ValidateNested({ each: true }) + @Type(() => ChirpstackDeviceContentsDto) + device: ChirpstackDeviceContentsDto; } diff --git a/src/entities/dto/chirpstack/create-chirpstack-multicast-queue-item.dto.ts b/src/entities/dto/chirpstack/create-chirpstack-multicast-queue-item.dto.ts index bfc2a932..77185db4 100644 --- a/src/entities/dto/chirpstack/create-chirpstack-multicast-queue-item.dto.ts +++ b/src/entities/dto/chirpstack/create-chirpstack-multicast-queue-item.dto.ts @@ -1,13 +1,13 @@ export interface CreateChirpstackMulticastQueueItemDto { - multicastQueueItem: MulticastQueueItem; + multicastQueueItem: MulticastQueueItem; } export interface MulticastQueueItem { - fPort: number; - data: string; - multicastGroupID: string; + fPort: number; + data: string; + multicastGroupID: string; } export interface CreateChirpstackMulticastQueueItemResponse { - fCnt: number; + fCnt: number; } diff --git a/src/entities/dto/chirpstack/create-chirpstack-profile-response.dto.ts b/src/entities/dto/chirpstack/create-chirpstack-profile-response.dto.ts index b2130c7b..5bf78e51 100644 --- a/src/entities/dto/chirpstack/create-chirpstack-profile-response.dto.ts +++ b/src/entities/dto/chirpstack/create-chirpstack-profile-response.dto.ts @@ -1,3 +1,3 @@ export class CreateChirpstackProfileResponseDto { - id: string; + id: string; } diff --git a/src/entities/dto/chirpstack/create-device-profile.dto.ts b/src/entities/dto/chirpstack/create-device-profile.dto.ts index f4efcc37..e53a9dbb 100644 --- a/src/entities/dto/chirpstack/create-device-profile.dto.ts +++ b/src/entities/dto/chirpstack/create-device-profile.dto.ts @@ -5,17 +5,17 @@ import { ValidateNested } from "class-validator"; import { DeviceProfileDto } from "./device-profile.dto"; export class CreateDeviceProfileDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => DeviceProfileDto) - deviceProfile: DeviceProfileDto; + @ApiProperty({ required: true }) + @ValidateNested({ each: true }) + @Type(() => DeviceProfileDto) + deviceProfile: DeviceProfileDto; - @ApiProperty({ required: true }) - internalOrganizationId: number; + @ApiProperty({ required: true }) + internalOrganizationId: number; - @ApiProperty({ required: false }) - createdAt: Date; + @ApiProperty({ required: false }) + createdAt: Date; - @ApiProperty({ required: false }) - updatedAt: Date; + @ApiProperty({ required: false }) + updatedAt: Date; } diff --git a/src/entities/dto/chirpstack/create-gateway.dto.ts b/src/entities/dto/chirpstack/create-gateway.dto.ts index 2a36491c..1ed46d5e 100644 --- a/src/entities/dto/chirpstack/create-gateway.dto.ts +++ b/src/entities/dto/chirpstack/create-gateway.dto.ts @@ -5,11 +5,11 @@ import { ValidateNested } from "class-validator"; import { GatewayContentsDto } from "@dto/chirpstack/gateway-contents.dto"; export class CreateGatewayDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => GatewayContentsDto) - gateway: GatewayContentsDto; + @ApiProperty({ required: true }) + @ValidateNested({ each: true }) + @Type(() => GatewayContentsDto) + gateway: GatewayContentsDto; - @ApiProperty({ required: true }) - organizationId: number; + @ApiProperty({ required: true }) + organizationId: number; } diff --git a/src/entities/dto/chirpstack/create-multicast-chirpstack.dto.ts b/src/entities/dto/chirpstack/create-multicast-chirpstack.dto.ts index 44a21e52..38909ac2 100644 --- a/src/entities/dto/chirpstack/create-multicast-chirpstack.dto.ts +++ b/src/entities/dto/chirpstack/create-multicast-chirpstack.dto.ts @@ -4,8 +4,8 @@ import { ValidateNested } from "class-validator"; import { ChirpstackMulticastContentsDto } from "./chirpstack-multicast-contents.dto"; export class CreateMulticastChirpStackDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => ChirpstackMulticastContentsDto) - multicastGroup: ChirpstackMulticastContentsDto; + @ApiProperty({ required: true }) + @ValidateNested({ each: true }) + @Type(() => ChirpstackMulticastContentsDto) + multicastGroup: ChirpstackMulticastContentsDto; } diff --git a/src/entities/dto/chirpstack/device-profile.dto.ts b/src/entities/dto/chirpstack/device-profile.dto.ts index f849dcc5..160ec832 100644 --- a/src/entities/dto/chirpstack/device-profile.dto.ts +++ b/src/entities/dto/chirpstack/device-profile.dto.ts @@ -3,107 +3,107 @@ import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { IsInt, IsNotEmpty, IsOptional, IsString, Length, Min, ValidateIf } from "class-validator"; export class DeviceProfileDto { - @ApiProperty({ required: true }) - @IsString() - @Length(1, 1024) - name: string; - - @ApiProperty({ required: true }) - @IsNotEmpty() - macVersion: MacVersionMap[keyof MacVersionMap]; - - @ApiProperty({ required: true }) - @IsNotEmpty() - regParamsRevision: RegParamsRevisionMap[keyof RegParamsRevisionMap]; - - @ApiProperty({ required: false }) - @IsString() - adrAlgorithmID?: string; - - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsClassB) - @Min(0) - @IsInt() - classBTimeout?: number; - - @ApiProperty({ required: false }) - @Min(0) - @IsInt() - classCTimeout?: number; - - @ApiProperty({ required: false }) - id?: string; - - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsClassB) - @Min(0) - @IsInt() - pingSlotDR?: number; - - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsClassB) - @Min(0) - @IsInt() - pingSlotFreq?: number; - - @ApiProperty({ required: false }) - @IsOptional() - pingSlotPeriod?: number; - - @ApiProperty({ required: false }) - rfRegion?: string; - - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) - @Min(0) - @IsInt() - rxDROffset1?: number; - - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) - @Min(0) - @IsInt() - rxDataRate2?: number; - - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) - @Min(0) - @IsInt() - rxDelay1?: number; - - @ApiProperty({ required: false }) - @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) - @Min(0) - @IsInt() - rxFreq2?: number; - - @ApiProperty({ required: false }) - supportsClassB?: boolean; - - @ApiProperty({ required: false }) - supportsClassC?: boolean; - - @ApiProperty({ required: false }) - supportsJoin?: boolean; - - @ApiProperty({ required: false }) - devStatusReqFreq?: number; - - @ApiHideProperty() - tags?: { [id: string]: string }; - - @ApiHideProperty() - tagsMap?: Array<[string, string]>; - - @ApiHideProperty() - internalOrganizationId?: number; - - @ApiHideProperty() - updatedBy?: number; - - @ApiHideProperty() - createdBy?: number; - - @ApiHideProperty() - organizationID?: string; + @ApiProperty({ required: true }) + @IsString() + @Length(1, 1024) + name: string; + + @ApiProperty({ required: true }) + @IsNotEmpty() + macVersion: MacVersionMap[keyof MacVersionMap]; + + @ApiProperty({ required: true }) + @IsNotEmpty() + regParamsRevision: RegParamsRevisionMap[keyof RegParamsRevisionMap]; + + @ApiProperty({ required: false }) + @IsString() + adrAlgorithmID?: string; + + @ApiProperty({ required: false }) + @ValidateIf((o: DeviceProfileDto) => o.supportsClassB) + @Min(0) + @IsInt() + classBTimeout?: number; + + @ApiProperty({ required: false }) + @Min(0) + @IsInt() + classCTimeout?: number; + + @ApiProperty({ required: false }) + id?: string; + + @ApiProperty({ required: false }) + @ValidateIf((o: DeviceProfileDto) => o.supportsClassB) + @Min(0) + @IsInt() + pingSlotDR?: number; + + @ApiProperty({ required: false }) + @ValidateIf((o: DeviceProfileDto) => o.supportsClassB) + @Min(0) + @IsInt() + pingSlotFreq?: number; + + @ApiProperty({ required: false }) + @IsOptional() + pingSlotPeriod?: number; + + @ApiProperty({ required: false }) + rfRegion?: string; + + @ApiProperty({ required: false }) + @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) + @Min(0) + @IsInt() + rxDROffset1?: number; + + @ApiProperty({ required: false }) + @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) + @Min(0) + @IsInt() + rxDataRate2?: number; + + @ApiProperty({ required: false }) + @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) + @Min(0) + @IsInt() + rxDelay1?: number; + + @ApiProperty({ required: false }) + @ValidateIf((o: DeviceProfileDto) => o.supportsJoin == false) + @Min(0) + @IsInt() + rxFreq2?: number; + + @ApiProperty({ required: false }) + supportsClassB?: boolean; + + @ApiProperty({ required: false }) + supportsClassC?: boolean; + + @ApiProperty({ required: false }) + supportsJoin?: boolean; + + @ApiProperty({ required: false }) + devStatusReqFreq?: number; + + @ApiHideProperty() + tags?: { [id: string]: string }; + + @ApiHideProperty() + tagsMap?: Array<[string, string]>; + + @ApiHideProperty() + internalOrganizationId?: number; + + @ApiHideProperty() + updatedBy?: number; + + @ApiHideProperty() + createdBy?: number; + + @ApiHideProperty() + organizationID?: string; } diff --git a/src/entities/dto/chirpstack/device/device-stats.response.dto.ts b/src/entities/dto/chirpstack/device/device-stats.response.dto.ts index a27e99f9..0e34cc66 100644 --- a/src/entities/dto/chirpstack/device/device-stats.response.dto.ts +++ b/src/entities/dto/chirpstack/device/device-stats.response.dto.ts @@ -1,6 +1,6 @@ export class DeviceStatsResponseDto { - timestamp: string; - rssi: number; - snr: number; - rxPacketsPerDr?: Record; + timestamp: string; + rssi: number; + snr: number; + rxPacketsPerDr?: Record; } diff --git a/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts b/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts index 874daa01..78d0360c 100644 --- a/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts +++ b/src/entities/dto/chirpstack/device/lorawan-stats.response.dto.ts @@ -1,10 +1,10 @@ export class LoRaWANStatsResponseDto { - result: LoRaWANStatsElementDto[]; + result: LoRaWANStatsElementDto[]; } export class LoRaWANStatsElementDto { - gwRssi: number; - gwSnr: number; - rxPacketsPerDr: Record; - timestamp: string; + gwRssi: number; + gwSnr: number; + rxPacketsPerDr: Record; + timestamp: string; } diff --git a/src/entities/dto/chirpstack/gateway-contents.dto.ts b/src/entities/dto/chirpstack/gateway-contents.dto.ts index f71b01a0..a449492b 100644 --- a/src/entities/dto/chirpstack/gateway-contents.dto.ts +++ b/src/entities/dto/chirpstack/gateway-contents.dto.ts @@ -1,19 +1,19 @@ import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { - IsEmail, - IsEnum, - IsHexadecimal, - IsJSON, - IsOptional, - IsPhoneNumber, - IsString, - Length, - Matches, - MaxLength, - MinLength, - ValidateIf, - ValidateNested, + IsEmail, + IsEnum, + IsHexadecimal, + IsJSON, + IsOptional, + IsPhoneNumber, + IsString, + Length, + Matches, + MaxLength, + MinLength, + ValidateIf, + ValidateNested, } from "class-validator"; import { CommonLocationDto } from "@dto/chirpstack/common-location.dto"; import { GatewayPlacement, GatewayStatus } from "@enum/gateway.enum"; @@ -22,100 +22,100 @@ import { IsJSONOrNull } from "@helpers/is-json-or-null.validator"; import { nameof } from "@helpers/type-helper"; export class GatewayContentsDto { - @ApiProperty({ required: true }) - @IsOptional() - @IsString() - @MaxLength(1024) - description?: string; - - @ApiProperty({ required: false, default: false }) - @IsOptional() - discoveryEnabled: boolean; - - @ApiProperty({ required: true }) - @IsString() - @IsHexadecimal() - @Length(16, 16) - gatewayId: string; - - @ApiProperty({ required: false }) - @ValidateNested({ each: true }) - @Type(() => CommonLocationDto) - location: CommonLocationDto; - - @ApiProperty({ required: false }) - metadata?: JSON; - - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - @MaxLength(50) - @Matches(/^[a-zA-Z0-9\-_]+$/, { - message: "The name may only contain words, numbers and dashes.", - }) - name: string; - - @ApiProperty({ required: false }) - @IsEnum(GatewayPlacement) - @IsOptional() - placement?: GatewayPlacement; - - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - modelName?: string; - - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - antennaType?: string; - - @ApiProperty({ required: false }) - @IsEnum(GatewayStatus) - @IsOptional() - status?: GatewayStatus; - - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - gatewayResponsibleName?: string; - - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - @ValidateIf(value => !isNullOrWhitespace(value.gatewayResponsibleEmail)) - @IsEmail() - gatewayResponsibleEmail?: string; - - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - @ValidateIf(value => !isNullOrWhitespace(value.gatewayResponsiblePhoneNumber)) - @IsPhoneNumber("DK") - gatewayResponsiblePhoneNumber?: string; - - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - operationalResponsibleName?: string; - - @ApiProperty({ required: false }) - @IsString() - @IsOptional() - @ValidateIf(value => !isNullOrWhitespace(value.operationalResponsibleEmail)) - @IsEmail() - operationalResponsibleEmail?: string; - - @ApiHideProperty() - tenantId: string; - - @ApiProperty({ required: false }) - @IsJSONOrNull(nameof("tagsString")) - tagsString?: string; - - @ApiHideProperty() - tags?: { [id: string]: string }; - - @ApiHideProperty() - id: string; + @ApiProperty({ required: true }) + @IsOptional() + @IsString() + @MaxLength(1024) + description?: string; + + @ApiProperty({ required: false, default: false }) + @IsOptional() + discoveryEnabled: boolean; + + @ApiProperty({ required: true }) + @IsString() + @IsHexadecimal() + @Length(16, 16) + gatewayId: string; + + @ApiProperty({ required: false }) + @ValidateNested({ each: true }) + @Type(() => CommonLocationDto) + location: CommonLocationDto; + + @ApiProperty({ required: false }) + metadata?: JSON; + + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + @MaxLength(50) + @Matches(/^[a-zA-Z0-9\-_]+$/, { + message: "The name may only contain words, numbers and dashes.", + }) + name: string; + + @ApiProperty({ required: false }) + @IsEnum(GatewayPlacement) + @IsOptional() + placement?: GatewayPlacement; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + modelName?: string; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + antennaType?: string; + + @ApiProperty({ required: false }) + @IsEnum(GatewayStatus) + @IsOptional() + status?: GatewayStatus; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + gatewayResponsibleName?: string; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + @ValidateIf(value => !isNullOrWhitespace(value.gatewayResponsibleEmail)) + @IsEmail() + gatewayResponsibleEmail?: string; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + @ValidateIf(value => !isNullOrWhitespace(value.gatewayResponsiblePhoneNumber)) + @IsPhoneNumber("DK") + gatewayResponsiblePhoneNumber?: string; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + operationalResponsibleName?: string; + + @ApiProperty({ required: false }) + @IsString() + @IsOptional() + @ValidateIf(value => !isNullOrWhitespace(value.operationalResponsibleEmail)) + @IsEmail() + operationalResponsibleEmail?: string; + + @ApiHideProperty() + tenantId: string; + + @ApiProperty({ required: false }) + @IsJSONOrNull(nameof("tagsString")) + tagsString?: string; + + @ApiHideProperty() + tags?: { [id: string]: string }; + + @ApiHideProperty() + id: string; } diff --git a/src/entities/dto/chirpstack/gateway-response.dto.ts b/src/entities/dto/chirpstack/gateway-response.dto.ts index 71f4a27d..199dddf0 100644 --- a/src/entities/dto/chirpstack/gateway-response.dto.ts +++ b/src/entities/dto/chirpstack/gateway-response.dto.ts @@ -2,37 +2,37 @@ import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; import { CommonLocationDto } from "./common-location.dto"; export class ChirpstackGatewayResponseDto { - id?: number; - gatewayId: string; - name: string; - description?: string; - rxPacketsReceived?: number; - txPacketsEmitted?: number; - internalOrganizationId?: number; - internalOrganizationName?: string; - location: CommonLocationDto; - createdAt?: Timestamp.AsObject; - updatedAt?: Timestamp.AsObject; - lastSeenAt?: Timestamp.AsObject; - updatedBy?: number; - createdBy?: number; - tags?: { [id: string]: string }; + id?: number; + gatewayId: string; + name: string; + description?: string; + rxPacketsReceived?: number; + txPacketsEmitted?: number; + internalOrganizationId?: number; + internalOrganizationName?: string; + location: CommonLocationDto; + createdAt?: Timestamp.AsObject; + updatedAt?: Timestamp.AsObject; + lastSeenAt?: Timestamp.AsObject; + updatedBy?: number; + createdBy?: number; + tags?: { [id: string]: string }; } export class GatewayResponseDto { - id: number; - gatewayId: string; - name: string; - description?: string; - rxPacketsReceived: number; - txPacketsEmitted: number; - organizationId: number; - organizationName: string; - location: CommonLocationDto; - tags: { [id: string]: string | number }; - createdAt?: Date; - updatedAt?: Date; - lastSeenAt?: Date; - updatedBy?: number; - createdBy?: number; + id: number; + gatewayId: string; + name: string; + description?: string; + rxPacketsReceived: number; + txPacketsEmitted: number; + organizationId: number; + organizationName: string; + location: CommonLocationDto; + tags: { [id: string]: string | number }; + createdAt?: Date; + updatedAt?: Date; + lastSeenAt?: Date; + updatedBy?: number; + createdBy?: number; } diff --git a/src/entities/dto/chirpstack/gateway-stats.response.dto.ts b/src/entities/dto/chirpstack/gateway-stats.response.dto.ts index 5de3cced..70ac60e2 100644 --- a/src/entities/dto/chirpstack/gateway-stats.response.dto.ts +++ b/src/entities/dto/chirpstack/gateway-stats.response.dto.ts @@ -1,11 +1,11 @@ import { Metric } from "@chirpstack/chirpstack-api/common/common_pb"; export class GatewayStatsResponseDto { - result: GatewayStatsElementDto[]; + result: GatewayStatsElementDto[]; } export class GatewayStatsElementDto { - timestamp: string; - rxPacketsReceived: number; - txPacketsEmitted: number; + timestamp: string; + rxPacketsReceived: number; + txPacketsEmitted: number; } diff --git a/src/entities/dto/chirpstack/list-all-adr-algorithms-response.dto.ts b/src/entities/dto/chirpstack/list-all-adr-algorithms-response.dto.ts index 6ba12710..89644d29 100644 --- a/src/entities/dto/chirpstack/list-all-adr-algorithms-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-adr-algorithms-response.dto.ts @@ -1,5 +1,5 @@ import { AdrAlgorithmDto } from "@dto/chirpstack/adr-algorithm.dto"; export class ListAllAdrAlgorithmsResponseDto { - adrAlgorithms: AdrAlgorithmDto[]; + adrAlgorithms: AdrAlgorithmDto[]; } diff --git a/src/entities/dto/chirpstack/list-all-applications-response.dto.ts b/src/entities/dto/chirpstack/list-all-applications-response.dto.ts index fa1fad16..47b69cdc 100644 --- a/src/entities/dto/chirpstack/list-all-applications-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-applications-response.dto.ts @@ -1,6 +1,6 @@ import { ChirpstackApplicationResponseDto } from "./chirpstack-application-response.dto"; export class ListAllChirpstackApplicationsResponseDto { - resultList: ChirpstackApplicationResponseDto[]; - totalCount: number; + resultList: ChirpstackApplicationResponseDto[]; + totalCount: number; } diff --git a/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts b/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts index 9e906979..f1a76774 100644 --- a/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-device-profiles-response.dto.ts @@ -1,16 +1,16 @@ import { DeviceProfileDto } from "./device-profile.dto"; export class ListAllDeviceProfilesResponseDto { - result: DeviceProfileListDto[]; - totalCount: string; + result: DeviceProfileListDto[]; + totalCount: string; } export class DeviceProfileListDto { - id: string; - name: string; - createdAt: Date; - createdBy?: number; - internalOrganizationId?: number; - updatedAt: Date; - updatedBy?: number; + id: string; + name: string; + createdAt: Date; + createdBy?: number; + internalOrganizationId?: number; + updatedAt: Date; + updatedBy?: number; } diff --git a/src/entities/dto/chirpstack/list-all-devices-response.dto.ts b/src/entities/dto/chirpstack/list-all-devices-response.dto.ts index 72f99cb7..54cdb13d 100644 --- a/src/entities/dto/chirpstack/list-all-devices-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-devices-response.dto.ts @@ -1,6 +1,6 @@ import { ChirpstackDeviceContentsDto } from "@dto/chirpstack/chirpstack-device-contents.dto"; export class ListAllDevicesResponseDto { - result: ChirpstackDeviceContentsDto[]; - totalCount: string; + result: ChirpstackDeviceContentsDto[]; + totalCount: string; } diff --git a/src/entities/dto/chirpstack/list-all-gateways-response.dto.ts b/src/entities/dto/chirpstack/list-all-gateways-response.dto.ts index 3b0e4ab9..31be73ab 100644 --- a/src/entities/dto/chirpstack/list-all-gateways-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-gateways-response.dto.ts @@ -1,11 +1,11 @@ import { ChirpstackGatewayResponseDto, GatewayResponseDto } from "./gateway-response.dto"; export class ListAllGatewaysResponseDto { - totalCount: number; - resultList: GatewayResponseDto[]; + totalCount: number; + resultList: GatewayResponseDto[]; } export class ListAllChirpstackGatewaysResponseDto { - totalCount: number; - resultList: ChirpstackGatewayResponseDto[]; + totalCount: number; + resultList: ChirpstackGatewayResponseDto[]; } diff --git a/src/entities/dto/chirpstack/list-all-gateways.dto.ts b/src/entities/dto/chirpstack/list-all-gateways.dto.ts index 82e2d30a..675300f0 100644 --- a/src/entities/dto/chirpstack/list-all-gateways.dto.ts +++ b/src/entities/dto/chirpstack/list-all-gateways.dto.ts @@ -8,19 +8,19 @@ import { Transform } from "class-transformer"; import { DefaultLimit, DefaultOffset } from "@config/constants/pagination-constants"; export class ListAllGatewaysDto extends OmitType(ListAllEntitiesDto, ["limit", "offset"]) { - @IsSwaggerOptional({ description: "Filter to one organization" }) - @StringToNumber() - organizationId?: number; + @IsSwaggerOptional({ description: "Filter to one organization" }) + @StringToNumber() + organizationId?: number; - @ApiProperty({ type: Number, required: false }) - @IsOptional() - @IsNumber() - @Transform(({ value }) => NullableStringToNumber(value)) - limit? = DefaultLimit; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @IsNumber() + @Transform(({ value }) => NullableStringToNumber(value)) + limit? = DefaultLimit; - @ApiProperty({ type: Number, required: false }) - @IsOptional() - @IsNumber() - @Transform(({ value }) => NullableStringToNumber(value)) - offset? = DefaultOffset; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @IsNumber() + @Transform(({ value }) => NullableStringToNumber(value)) + offset? = DefaultOffset; } diff --git a/src/entities/dto/chirpstack/list-all-organizations-response.dto.ts b/src/entities/dto/chirpstack/list-all-organizations-response.dto.ts index c9e78538..979bf29e 100644 --- a/src/entities/dto/chirpstack/list-all-organizations-response.dto.ts +++ b/src/entities/dto/chirpstack/list-all-organizations-response.dto.ts @@ -1,7 +1,7 @@ import { Organization } from "@entities/organization.entity"; export class ListAllOrganizationsResponseDto { - totalCount: number; + totalCount: number; - result: Organization[]; + result: Organization[]; } diff --git a/src/entities/dto/chirpstack/organization.dto.ts b/src/entities/dto/chirpstack/organization.dto.ts index eae85343..952b4ba9 100644 --- a/src/entities/dto/chirpstack/organization.dto.ts +++ b/src/entities/dto/chirpstack/organization.dto.ts @@ -1,8 +1,8 @@ export class OrganizationDto { - id: string; - name: string; - displayName: string; - canHaveGateways: boolean; - createdAt: string; - updatedAt: string; + id: string; + name: string; + displayName: string; + canHaveGateways: boolean; + createdAt: string; + updatedAt: string; } diff --git a/src/entities/dto/chirpstack/single-gateway-response.dto.ts b/src/entities/dto/chirpstack/single-gateway-response.dto.ts index 2580402c..b89c75fe 100644 --- a/src/entities/dto/chirpstack/single-gateway-response.dto.ts +++ b/src/entities/dto/chirpstack/single-gateway-response.dto.ts @@ -2,6 +2,6 @@ import { GatewayStatsElementDto } from "@dto/chirpstack/gateway-stats.response.d import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; export class SingleGatewayResponseDto { - gateway: GatewayResponseDto; - stats?: GatewayStatsElementDto[]; + gateway: GatewayResponseDto; + stats?: GatewayStatsElementDto[]; } diff --git a/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts b/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts index e0123eb1..cbcc97e0 100644 --- a/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts +++ b/src/entities/dto/chirpstack/state/chirpstack-mqtt-state-message.dto.ts @@ -1,5 +1,5 @@ export class ChirpstackMQTTConnectionStateMessage { - gatewayId: string; - gatewayIdLegacy?: string; - state: "ONLINE"; + gatewayId: string; + gatewayIdLegacy?: string; + state: "ONLINE"; } diff --git a/src/entities/dto/chirpstack/update-device-profile.dto.ts b/src/entities/dto/chirpstack/update-device-profile.dto.ts index f6e55da8..e804a1cf 100644 --- a/src/entities/dto/chirpstack/update-device-profile.dto.ts +++ b/src/entities/dto/chirpstack/update-device-profile.dto.ts @@ -1,6 +1,4 @@ import { OmitType } from "@nestjs/swagger"; import { CreateDeviceProfileDto } from "./create-device-profile.dto"; -export class UpdateDeviceProfileDto extends OmitType(CreateDeviceProfileDto, [ - "internalOrganizationId", -]) {} +export class UpdateDeviceProfileDto extends OmitType(CreateDeviceProfileDto, ["internalOrganizationId"]) {} diff --git a/src/entities/dto/chirpstack/update-gateway.dto.ts b/src/entities/dto/chirpstack/update-gateway.dto.ts index 98c907cb..d7ebb636 100644 --- a/src/entities/dto/chirpstack/update-gateway.dto.ts +++ b/src/entities/dto/chirpstack/update-gateway.dto.ts @@ -4,19 +4,19 @@ import { GatewayContentsDto } from "./gateway-contents.dto"; import { Type } from "class-transformer"; export class UpdateGatewayContentsDto extends OmitType(GatewayContentsDto, ["gatewayId"]) { - @ApiHideProperty() - gatewayId: string; + @ApiHideProperty() + gatewayId: string; - @ApiHideProperty() - createdBy?: number; + @ApiHideProperty() + createdBy?: number; - @ApiHideProperty() - updatedBy?: number; + @ApiHideProperty() + updatedBy?: number; } export class UpdateGatewayDto { - @ApiProperty({ required: true }) - @ValidateNested({ each: true }) - @Type(() => UpdateGatewayContentsDto) - gateway: UpdateGatewayContentsDto; + @ApiProperty({ required: true }) + @ValidateNested({ each: true }) + @Type(() => UpdateGatewayContentsDto) + gateway: UpdateGatewayContentsDto; } diff --git a/src/entities/dto/create-application.dto.ts b/src/entities/dto/create-application.dto.ts index 632674be..9026a3c4 100644 --- a/src/entities/dto/create-application.dto.ts +++ b/src/entities/dto/create-application.dto.ts @@ -9,91 +9,91 @@ import { ApiProperty } from "@nestjs/swagger"; import { ArrayUnique, IsArray, IsBoolean, IsEnum, IsOptional, IsString, MaxLength, MinLength } from "class-validator"; export class CreateApplicationDto { - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - @MaxLength(50) - name: string; - - @ApiProperty({ required: true }) - organizationId: number; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @MaxLength(1024) - description?: string; - - @IsSwaggerOptional() - @IsEnum(ApplicationStatus) - status?: ApplicationStatus; - - @IsSwaggerOptional() - @ValidateDate() - startDate?: Date; - - @IsSwaggerOptional() - @ValidateDate() - endDate?: Date; - - @IsSwaggerOptional() - @IsString() - @MaxLength(100) - category?: string; - - @IsSwaggerOptional() - @IsString() - @MaxLength(100) - owner?: string; - - @IsSwaggerOptional() - @IsString() - @MaxLength(100) - contactPerson?: string; - - @IsSwaggerOptional() - @IsString() - @MaxLength(100) - contactEmail?: string; - - @IsSwaggerOptional() - @IsPhoneNumberString(nameof("contactPhone")) - @MaxLength(12) - contactPhone?: string; - - @IsSwaggerOptional() - @IsBoolean() - personalData?: boolean; - - @IsSwaggerOptional() - @IsString() - @MaxLength(1024) - hardware?: string; - - @IsSwaggerOptional() - @IsArray() - @ArrayUnique() - @IsEnum(ControlledPropertyTypes, { each: true }) - controlledProperties?: ControlledPropertyTypes[]; - - @IsSwaggerOptional() - @IsArray() - @ArrayUnique() - @IsEnum(ApplicationDeviceTypes, { each: true }) - deviceTypes?: ApplicationDeviceTypeUnion[]; - - @ApiProperty({ - required: false, - type: "array", - items: { - type: "number", - }, - }) - permissionIds?: number[]; - - @ApiProperty({ required: false }) - @IsOptional() - @IsString() - @MaxLength(1024) - chirpstackId?: string; + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + @MaxLength(50) + name: string; + + @ApiProperty({ required: true }) + organizationId: number; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + @MaxLength(1024) + description?: string; + + @IsSwaggerOptional() + @IsEnum(ApplicationStatus) + status?: ApplicationStatus; + + @IsSwaggerOptional() + @ValidateDate() + startDate?: Date; + + @IsSwaggerOptional() + @ValidateDate() + endDate?: Date; + + @IsSwaggerOptional() + @IsString() + @MaxLength(100) + category?: string; + + @IsSwaggerOptional() + @IsString() + @MaxLength(100) + owner?: string; + + @IsSwaggerOptional() + @IsString() + @MaxLength(100) + contactPerson?: string; + + @IsSwaggerOptional() + @IsString() + @MaxLength(100) + contactEmail?: string; + + @IsSwaggerOptional() + @IsPhoneNumberString(nameof("contactPhone")) + @MaxLength(12) + contactPhone?: string; + + @IsSwaggerOptional() + @IsBoolean() + personalData?: boolean; + + @IsSwaggerOptional() + @IsString() + @MaxLength(1024) + hardware?: string; + + @IsSwaggerOptional() + @IsArray() + @ArrayUnique() + @IsEnum(ControlledPropertyTypes, { each: true }) + controlledProperties?: ControlledPropertyTypes[]; + + @IsSwaggerOptional() + @IsArray() + @ArrayUnique() + @IsEnum(ApplicationDeviceTypes, { each: true }) + deviceTypes?: ApplicationDeviceTypeUnion[]; + + @ApiProperty({ + required: false, + type: "array", + items: { + type: "number", + }, + }) + permissionIds?: number[]; + + @ApiProperty({ required: false }) + @IsOptional() + @IsString() + @MaxLength(1024) + chirpstackId?: string; } diff --git a/src/entities/dto/create-data-target.dto.ts b/src/entities/dto/create-data-target.dto.ts index 1470a2de..74cfc12f 100644 --- a/src/entities/dto/create-data-target.dto.ts +++ b/src/entities/dto/create-data-target.dto.ts @@ -5,106 +5,106 @@ import { IsNotBlank } from "@helpers/is-not-blank.validator"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { - IsEnum, - IsInt, - IsNotEmpty, - IsNumber, - IsOptional, - IsString, - IsUrl, - MaxLength, - Min, - MinLength, - ValidateIf, - ValidateNested, + IsEnum, + IsInt, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUrl, + MaxLength, + Min, + MinLength, + ValidateIf, + ValidateNested, } from "class-validator"; export class CreateDataTargetDto { - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - @MaxLength(50) - name: string; - - @ApiProperty({ required: false, default: "" }) - tenant: string; - - @ApiProperty({ required: false, default: "", example: null }) - context: string; - - @ApiProperty({ required: true, example: 1 }) - @IsNumber() - @Min(1) - applicationId: number; - - @ApiProperty({ required: true }) - type: DataTargetType; - - @ValidateIf((obj: CreateDataTargetDto) => obj.type !== DataTargetType.OpenDataDK) - @ApiProperty({ required: true, example: "https://example.com/endpoint" }) - @IsString() - @MaxLength(1024) - @IsNotBlank("url") - @IsUrl({ - require_tld: false, - require_protocol: true, - protocols: ["http", "https", "mqtt", "mqtts"], - }) - url: string; - - @ValidateIf((obj: CreateDataTargetDto) => obj.type !== DataTargetType.OpenDataDK) - @ApiProperty({ required: true, example: 30000 }) - @IsInt() - timeout: number; - - @ApiProperty({ required: false, default: "", example: null }) - authorizationHeader: string; - - @ApiProperty({ required: false }) - tokenEndpoint?: string; - - @ApiProperty({ required: false }) - clientId?: string; - - @ApiProperty({ required: false }) - clientSecret?: string; - - @ApiPropertyOptional({ required: false }) - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => CreateOpenDataDkDatasetDto) - openDataDkDataset?: CreateOpenDataDkDatasetDto; - - @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) - @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) - @Type(() => Number) - @IsInt() - @Min(1025) // Don't allow priviliged ports - mqttPort?: number; - - @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) - @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) - @IsString() - @IsNotEmpty() - @MaxLength(100) - mqttTopic?: string; - - @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) - @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) - @IsEnum(QoS) - mqttQos?: QoS; - - @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) - @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) - @IsString() - @IsNotEmpty() - @MaxLength(100) - mqttUsername?: string; - - @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) - @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) - @IsString() - @IsNotEmpty() - @MaxLength(100) - mqttPassword?: string; + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + @MaxLength(50) + name: string; + + @ApiProperty({ required: false, default: "" }) + tenant: string; + + @ApiProperty({ required: false, default: "", example: null }) + context: string; + + @ApiProperty({ required: true, example: 1 }) + @IsNumber() + @Min(1) + applicationId: number; + + @ApiProperty({ required: true }) + type: DataTargetType; + + @ValidateIf((obj: CreateDataTargetDto) => obj.type !== DataTargetType.OpenDataDK) + @ApiProperty({ required: true, example: "https://example.com/endpoint" }) + @IsString() + @MaxLength(1024) + @IsNotBlank("url") + @IsUrl({ + require_tld: false, + require_protocol: true, + protocols: ["http", "https", "mqtt", "mqtts"], + }) + url: string; + + @ValidateIf((obj: CreateDataTargetDto) => obj.type !== DataTargetType.OpenDataDK) + @ApiProperty({ required: true, example: 30000 }) + @IsInt() + timeout: number; + + @ApiProperty({ required: false, default: "", example: null }) + authorizationHeader: string; + + @ApiProperty({ required: false }) + tokenEndpoint?: string; + + @ApiProperty({ required: false }) + clientId?: string; + + @ApiProperty({ required: false }) + clientSecret?: string; + + @ApiPropertyOptional({ required: false }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => CreateOpenDataDkDatasetDto) + openDataDkDataset?: CreateOpenDataDkDatasetDto; + + @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) + @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) + @Type(() => Number) + @IsInt() + @Min(1025) // Don't allow priviliged ports + mqttPort?: number; + + @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) + @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) + @IsString() + @IsNotEmpty() + @MaxLength(100) + mqttTopic?: string; + + @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) + @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) + @IsEnum(QoS) + mqttQos?: QoS; + + @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) + @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) + @IsString() + @IsNotEmpty() + @MaxLength(100) + mqttUsername?: string; + + @ApiPropertyOptional({ required: false, description: "Required for MQTT datatarget" }) + @ValidateIf((obj: CreateDataTargetDto) => obj.type === DataTargetType.MQTT) + @IsString() + @IsNotEmpty() + @MaxLength(100) + mqttPassword?: string; } diff --git a/src/entities/dto/create-device-model.dto.ts b/src/entities/dto/create-device-model.dto.ts index 64ed0da1..5a09ecdf 100644 --- a/src/entities/dto/create-device-model.dto.ts +++ b/src/entities/dto/create-device-model.dto.ts @@ -2,12 +2,12 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsDefined, IsNumber } from "class-validator"; export class CreateDeviceModelDto { - @ApiProperty({ required: true }) - @IsNumber() - belongsToId: number; + @ApiProperty({ required: true }) + @IsNumber() + belongsToId: number; - @ApiProperty({ required: true }) - // @IsJSON or @IsString does not work. Will be validated during the flow - @IsDefined() - body: JSON; + @ApiProperty({ required: true }) + // @IsJSON or @IsString does not work. Will be validated during the flow + @IsDefined() + body: JSON; } diff --git a/src/entities/dto/create-iot-device-downlink.dto.ts b/src/entities/dto/create-iot-device-downlink.dto.ts index f7c6354d..be76000d 100644 --- a/src/entities/dto/create-iot-device-downlink.dto.ts +++ b/src/entities/dto/create-iot-device-downlink.dto.ts @@ -2,13 +2,13 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Matches } from "class-validator"; export class CreateIoTDeviceDownlinkDto { - @ApiProperty({ required: true }) - @Matches(/^[0-9A-Fa-f]+$/, { message: "Must be hexadecimal" }) - data: string; + @ApiProperty({ required: true }) + @Matches(/^[0-9A-Fa-f]+$/, { message: "Must be hexadecimal" }) + data: string; - @ApiPropertyOptional() - port?: number; + @ApiPropertyOptional() + port?: number; - @ApiPropertyOptional({ default: true }) - confirmed?: boolean; + @ApiPropertyOptional({ default: true }) + confirmed?: boolean; } diff --git a/src/entities/dto/create-iot-device-payload-decoder-data-target-connection.dto.ts b/src/entities/dto/create-iot-device-payload-decoder-data-target-connection.dto.ts index ab0adafa..6a9bf683 100644 --- a/src/entities/dto/create-iot-device-payload-decoder-data-target-connection.dto.ts +++ b/src/entities/dto/create-iot-device-payload-decoder-data-target-connection.dto.ts @@ -2,20 +2,20 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNumber, IsOptional } from "class-validator"; export class CreateIoTDevicePayloadDecoderDataTargetConnectionDto { - @ApiProperty({ - required: true, - type: "array", - items: { - type: "number", - }, - }) - iotDeviceIds: number[]; + @ApiProperty({ + required: true, + type: "array", + items: { + type: "number", + }, + }) + iotDeviceIds: number[]; - @ApiProperty({ required: false }) - @IsOptional() - payloadDecoderId?: number; + @ApiProperty({ required: false }) + @IsOptional() + payloadDecoderId?: number; - @ApiProperty({ required: true }) - @IsNumber() - dataTargetId: number; + @ApiProperty({ required: true }) + @IsNumber() + dataTargetId: number; } diff --git a/src/entities/dto/create-iot-device.dto.ts b/src/entities/dto/create-iot-device.dto.ts index c559e528..4a6bb5f7 100644 --- a/src/entities/dto/create-iot-device.dto.ts +++ b/src/entities/dto/create-iot-device.dto.ts @@ -1,16 +1,16 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { - IsEnum, - IsInt, - IsOptional, - IsString, - Max, - MaxLength, - Min, - MinLength, - ValidateIf, - ValidateNested, + IsEnum, + IsInt, + IsOptional, + IsString, + Max, + MaxLength, + Min, + MinLength, + ValidateIf, + ValidateNested, } from "class-validator"; import { IoTDeviceType } from "@enum/device-type.enum"; @@ -23,78 +23,78 @@ import { CreateMqttInternalBrokerSettingsDto } from "@dto/create-mqtt-internal-b import { CreateMqttExternalBrokerSettingsDto } from "@dto/create-mqtt-external-broker-settings.dto"; export class CreateIoTDeviceDto { - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - @MaxLength(50) - name: string; + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + @MaxLength(50) + name: string; - @ApiProperty({ required: true }) - @IsEnum(IoTDeviceType) - type: IoTDeviceType; + @ApiProperty({ required: true }) + @IsEnum(IoTDeviceType) + type: IoTDeviceType; - @Min(1) - @ApiProperty({ - title: "The id of the application that this IoTDevice should belong to", - required: true, - }) - applicationId: number; + @Min(1) + @ApiProperty({ + title: "The id of the application that this IoTDevice should belong to", + required: true, + }) + applicationId: number; - @ApiProperty({ required: false }) - @Max(180.0) - @Min(-180.0) - @IsOptional() - longitude: number; + @ApiProperty({ required: false }) + @Max(180.0) + @Min(-180.0) + @IsOptional() + longitude: number; - @ApiProperty({ required: false }) - @Max(90.0) - @Min(-90.0) - @IsOptional() - latitude: number; + @ApiProperty({ required: false }) + @Max(90.0) + @Min(-90.0) + @IsOptional() + latitude: number; - @ApiProperty({ required: false }) - @MaxLength(1024) - @IsOptional() - @IsString() - commentOnLocation?: string; + @ApiProperty({ required: false }) + @MaxLength(1024) + @IsOptional() + @IsString() + commentOnLocation?: string; - @ApiProperty({ required: false }) - @MaxLength(1024) - @IsOptional() - @IsString() - comment?: string; + @ApiProperty({ required: false }) + @MaxLength(1024) + @IsOptional() + @IsString() + comment?: string; - @ApiProperty({ required: false }) - @IsOptional() - @IsMetadataJson(nameof("metadata")) - metadata?: JSON; + @ApiProperty({ required: false }) + @IsOptional() + @IsMetadataJson(nameof("metadata")) + metadata?: JSON; - @ApiProperty({ required: false }) - @IsOptional() - @IsInt() - deviceModelId?: number; + @ApiProperty({ required: false }) + @IsOptional() + @IsInt() + deviceModelId?: number; - @ApiProperty({ required: false }) - @ValidateIf(o => o.type === IoTDeviceType.LoRaWAN) - @ValidateNested({ each: true }) - @Type(() => CreateLoRaWANSettingsDto) - lorawanSettings?: CreateLoRaWANSettingsDto; + @ApiProperty({ required: false }) + @ValidateIf(o => o.type === IoTDeviceType.LoRaWAN) + @ValidateNested({ each: true }) + @Type(() => CreateLoRaWANSettingsDto) + lorawanSettings?: CreateLoRaWANSettingsDto; - @ApiProperty({ required: false }) - @ValidateIf(o => o.type === IoTDeviceType.SigFox) - @ValidateNested({ each: true }) - @Type(() => CreateSigFoxSettingsDto) - sigfoxSettings?: CreateSigFoxSettingsDto; + @ApiProperty({ required: false }) + @ValidateIf(o => o.type === IoTDeviceType.SigFox) + @ValidateNested({ each: true }) + @Type(() => CreateSigFoxSettingsDto) + sigfoxSettings?: CreateSigFoxSettingsDto; - @ApiProperty({ required: false }) - @ValidateIf(o => o.type === IoTDeviceType.MQTTInternalBroker) - @ValidateNested({ each: true }) - @Type(() => CreateMqttInternalBrokerSettingsDto) - mqttInternalBrokerSettings?: CreateMqttInternalBrokerSettingsDto; + @ApiProperty({ required: false }) + @ValidateIf(o => o.type === IoTDeviceType.MQTTInternalBroker) + @ValidateNested({ each: true }) + @Type(() => CreateMqttInternalBrokerSettingsDto) + mqttInternalBrokerSettings?: CreateMqttInternalBrokerSettingsDto; - @ApiProperty({ required: false }) - @ValidateIf(o => o.type === IoTDeviceType.MQTTExternalBroker) - @ValidateNested({ each: true }) - @Type(() => CreateMqttExternalBrokerSettingsDto) - mqttExternalBrokerSettings?: CreateMqttExternalBrokerSettingsDto; + @ApiProperty({ required: false }) + @ValidateIf(o => o.type === IoTDeviceType.MQTTExternalBroker) + @ValidateNested({ each: true }) + @Type(() => CreateMqttExternalBrokerSettingsDto) + mqttExternalBrokerSettings?: CreateMqttExternalBrokerSettingsDto; } diff --git a/src/entities/dto/create-lorawan-settings.dto.ts b/src/entities/dto/create-lorawan-settings.dto.ts index 86de0f66..67f2960a 100644 --- a/src/entities/dto/create-lorawan-settings.dto.ts +++ b/src/entities/dto/create-lorawan-settings.dto.ts @@ -1,70 +1,62 @@ import { ApiProperty, PickType } from "@nestjs/swagger"; -import { - IsHexadecimal, - IsInt, - IsNumber, - IsString, - Length, - Min, - ValidateIf, -} from "class-validator"; +import { IsHexadecimal, IsInt, IsNumber, IsString, Length, Min, ValidateIf } from "class-validator"; import { ActivationType } from "@enum/lorawan-activation-type.enum"; import { ChirpstackDeviceContentsDto } from "./chirpstack/chirpstack-device-contents.dto"; export class CreateLoRaWANSettingsDto extends PickType(ChirpstackDeviceContentsDto, [ - "devEUI", - "deviceProfileID", - "skipFCntCheck", - "isDisabled", - "deviceStatusBattery", - "deviceStatusMargin", + "devEUI", + "deviceProfileID", + "skipFCntCheck", + "isDisabled", + "deviceStatusBattery", + "deviceStatusMargin", ]) { - @ApiProperty({ required: true }) - activationType: ActivationType; - - /* OTAA */ - @ApiProperty({ required: false }) - @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.OTAA) - @IsString() - @Length(32, 32) - @IsHexadecimal() - OTAAapplicationKey?: string; - - /* ABP */ - @ApiProperty({ required: false }) - @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) - @IsString() - @Length(8, 8) - @IsHexadecimal() - devAddr?: string; - - @ApiProperty({ required: false }) - @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) - @IsNumber() - @IsInt() - @Min(0) - fCntUp?: number; - - @ApiProperty({ required: false }) - @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) - @IsNumber() - @IsInt() - @Min(0) - nFCntDown?: number; - - @ApiProperty({ required: false }) - @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) - @IsString() - @Length(32, 32) - @IsHexadecimal() - networkSessionKey?: string; - - @ApiProperty({ required: false }) - @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) - @IsString() - @Length(32, 32) - @IsHexadecimal() - applicationSessionKey?: string; + @ApiProperty({ required: true }) + activationType: ActivationType; + + /* OTAA */ + @ApiProperty({ required: false }) + @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.OTAA) + @IsString() + @Length(32, 32) + @IsHexadecimal() + OTAAapplicationKey?: string; + + /* ABP */ + @ApiProperty({ required: false }) + @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) + @IsString() + @Length(8, 8) + @IsHexadecimal() + devAddr?: string; + + @ApiProperty({ required: false }) + @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) + @IsNumber() + @IsInt() + @Min(0) + fCntUp?: number; + + @ApiProperty({ required: false }) + @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) + @IsNumber() + @IsInt() + @Min(0) + nFCntDown?: number; + + @ApiProperty({ required: false }) + @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) + @IsString() + @Length(32, 32) + @IsHexadecimal() + networkSessionKey?: string; + + @ApiProperty({ required: false }) + @ValidateIf((o: CreateLoRaWANSettingsDto) => o.activationType == ActivationType.ABP) + @IsString() + @Length(32, 32) + @IsHexadecimal() + applicationSessionKey?: string; } diff --git a/src/entities/dto/create-mqtt-external-broker-settings.dto.ts b/src/entities/dto/create-mqtt-external-broker-settings.dto.ts index 82815eea..210abf7b 100644 --- a/src/entities/dto/create-mqtt-external-broker-settings.dto.ts +++ b/src/entities/dto/create-mqtt-external-broker-settings.dto.ts @@ -1,71 +1,62 @@ import { ApiProperty } from "@nestjs/swagger"; -import { - IsEnum, - IsNumber, - IsString, - Matches, - Max, - Min, - MinLength, - ValidateIf, -} from "class-validator"; +import { IsEnum, IsNumber, IsString, Matches, Max, Min, MinLength, ValidateIf } from "class-validator"; import { AuthenticationType } from "@enum/authentication-type.enum"; export class CreateMqttExternalBrokerSettingsDto { - @ApiProperty({ required: true }) - @IsString() - @Matches(/^mqtts?:\/\/\S+/, { - message: "Der skal tilføjes protokol (mqtt/mqtts) på url", - }) - mqttURL: string; - - @ApiProperty({ required: true }) - @IsNumber(undefined, { message: "Port skal udfyldes" }) - @Min(0, { message: "Port skal være over 0" }) - @Max(65535, { message: "Port skal være under 65535" }) - mqttPort: number; - - @ApiProperty({ required: true }) - @IsString() - mqtttopicname: string; - - @ApiProperty({ required: true }) - @IsEnum(AuthenticationType, { message: "Der skal vælges en autentifikations type" }) - authenticationType: AuthenticationType; - - @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - mqttusername: string; - - @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - mqttpassword: string; - - @ValidateIf(d => d.authenticationType === AuthenticationType.CERTIFICATE) - @ApiProperty({ required: true }) - @IsString() - @Matches(/^-----BEGIN CERTIFICATE-----([\s\S]*)-----END CERTIFICATE-----\s?$/, { - message: "CA certifikatet mangler eller er forkert format", - }) - caCertificate: string; - - @ValidateIf(d => d.authenticationType === AuthenticationType.CERTIFICATE) - @ApiProperty({ required: true }) - @IsString() - @Matches(/^-----BEGIN CERTIFICATE-----([\s\S]*)-----END CERTIFICATE-----\s?$/, { - message: "Enheds certifikat mangler eller er forkert format", - }) - deviceCertificate: string; - - @ValidateIf(d => d.authenticationType === AuthenticationType.CERTIFICATE) - @ApiProperty({ required: true }) - @IsString() - @Matches(/^-----([\s\S]*)-----\s?$/, { - message: "Enheds certifikatnøgle mangler eller er forkert format", - }) - deviceCertificateKey: string; + @ApiProperty({ required: true }) + @IsString() + @Matches(/^mqtts?:\/\/\S+/, { + message: "Der skal tilføjes protokol (mqtt/mqtts) på url", + }) + mqttURL: string; + + @ApiProperty({ required: true }) + @IsNumber(undefined, { message: "Port skal udfyldes" }) + @Min(0, { message: "Port skal være over 0" }) + @Max(65535, { message: "Port skal være under 65535" }) + mqttPort: number; + + @ApiProperty({ required: true }) + @IsString() + mqtttopicname: string; + + @ApiProperty({ required: true }) + @IsEnum(AuthenticationType, { message: "Der skal vælges en autentifikations type" }) + authenticationType: AuthenticationType; + + @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + mqttusername: string; + + @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + mqttpassword: string; + + @ValidateIf(d => d.authenticationType === AuthenticationType.CERTIFICATE) + @ApiProperty({ required: true }) + @IsString() + @Matches(/^-----BEGIN CERTIFICATE-----([\s\S]*)-----END CERTIFICATE-----\s?$/, { + message: "CA certifikatet mangler eller er forkert format", + }) + caCertificate: string; + + @ValidateIf(d => d.authenticationType === AuthenticationType.CERTIFICATE) + @ApiProperty({ required: true }) + @IsString() + @Matches(/^-----BEGIN CERTIFICATE-----([\s\S]*)-----END CERTIFICATE-----\s?$/, { + message: "Enheds certifikat mangler eller er forkert format", + }) + deviceCertificate: string; + + @ValidateIf(d => d.authenticationType === AuthenticationType.CERTIFICATE) + @ApiProperty({ required: true }) + @IsString() + @Matches(/^-----([\s\S]*)-----\s?$/, { + message: "Enheds certifikatnøgle mangler eller er forkert format", + }) + deviceCertificateKey: string; } diff --git a/src/entities/dto/create-mqtt-internal-broker-settings.dto.ts b/src/entities/dto/create-mqtt-internal-broker-settings.dto.ts index f36ac94c..1cba113f 100644 --- a/src/entities/dto/create-mqtt-internal-broker-settings.dto.ts +++ b/src/entities/dto/create-mqtt-internal-broker-settings.dto.ts @@ -3,19 +3,19 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsEnum, IsString, MinLength, ValidateIf } from "class-validator"; export class CreateMqttInternalBrokerSettingsDto { - @ApiProperty({ required: true }) - @IsEnum(AuthenticationType) - authenticationType: AuthenticationType; + @ApiProperty({ required: true }) + @IsEnum(AuthenticationType) + authenticationType: AuthenticationType; - @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - mqttusername: string; + @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + mqttusername: string; - @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - mqttpassword: string; + @ValidateIf(d => d.authenticationType === AuthenticationType.PASSWORD) + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + mqttpassword: string; } diff --git a/src/entities/dto/create-multicast-downlink.dto.ts b/src/entities/dto/create-multicast-downlink.dto.ts index cdea6d1a..f554be56 100644 --- a/src/entities/dto/create-multicast-downlink.dto.ts +++ b/src/entities/dto/create-multicast-downlink.dto.ts @@ -2,12 +2,12 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsHexadecimal, IsInt, Min } from "class-validator"; export class CreateMulticastDownlinkDto { - @ApiProperty({ required: true }) - @IsHexadecimal() - data: string; + @ApiProperty({ required: true }) + @IsHexadecimal() + data: string; - @ApiProperty({ required: true, example: 1 }) - @IsInt() - @Min(1) - port: number; + @ApiProperty({ required: true, example: 1 }) + @IsInt() + @Min(1) + port: number; } diff --git a/src/entities/dto/create-multicast.dto.ts b/src/entities/dto/create-multicast.dto.ts index 799d034b..70655f31 100644 --- a/src/entities/dto/create-multicast.dto.ts +++ b/src/entities/dto/create-multicast.dto.ts @@ -1,70 +1,62 @@ import { IoTDevice } from "@entities/iot-device.entity"; import { multicastGroup } from "@enum/multicast-type.enum"; import { ApiProperty } from "@nestjs/swagger"; -import { - IsHexadecimal, - IsInt, - IsNumber, - IsString, - MaxLength, - Min, - MinLength, -} from "class-validator"; +import { IsHexadecimal, IsInt, IsNumber, IsString, MaxLength, Min, MinLength } from "class-validator"; export class CreateMulticastDto { - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - @MaxLength(50) - name: string; - - @ApiProperty({ required: true, example: 1 }) - @IsNumber() - @Min(1) - applicationID: number; - - @ApiProperty({ required: true }) - @IsHexadecimal() - @MinLength(8) - @MaxLength(8) - mcAddr: string; - - @ApiProperty({ required: true }) - @IsHexadecimal() - @MaxLength(32) - @MinLength(32) - mcNwkSKey: string; - - @ApiProperty({ required: true }) - @IsHexadecimal() - @MaxLength(32) - @MinLength(32) - mcAppSKey: string; - - @ApiProperty({ required: true, example: 300 }) - @IsInt() - @Min(0) - fCnt: number; - - @ApiProperty({ required: true, example: 300 }) - @IsInt() - @Min(0) - dr: number; - - @ApiProperty({ required: true, example: 300 }) - @IsInt() - @Min(0) - frequency: number; - - @ApiProperty({ required: true }) - @IsString() - @MaxLength(32) - @MinLength(1) - groupType: multicastGroup; - - @ApiProperty({ required: true }) - multicastId: string; - - @ApiProperty({ required: false }) - iotDevices: IoTDevice[]; + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + @MaxLength(50) + name: string; + + @ApiProperty({ required: true, example: 1 }) + @IsNumber() + @Min(1) + applicationID: number; + + @ApiProperty({ required: true }) + @IsHexadecimal() + @MinLength(8) + @MaxLength(8) + mcAddr: string; + + @ApiProperty({ required: true }) + @IsHexadecimal() + @MaxLength(32) + @MinLength(32) + mcNwkSKey: string; + + @ApiProperty({ required: true }) + @IsHexadecimal() + @MaxLength(32) + @MinLength(32) + mcAppSKey: string; + + @ApiProperty({ required: true, example: 300 }) + @IsInt() + @Min(0) + fCnt: number; + + @ApiProperty({ required: true, example: 300 }) + @IsInt() + @Min(0) + dr: number; + + @ApiProperty({ required: true, example: 300 }) + @IsInt() + @Min(0) + frequency: number; + + @ApiProperty({ required: true }) + @IsString() + @MaxLength(32) + @MinLength(1) + groupType: multicastGroup; + + @ApiProperty({ required: true }) + multicastId: string; + + @ApiProperty({ required: false }) + iotDevices: IoTDevice[]; } diff --git a/src/entities/dto/create-open-data-dk-dataset.dto.ts b/src/entities/dto/create-open-data-dk-dataset.dto.ts index f3400e53..4efa10c8 100644 --- a/src/entities/dto/create-open-data-dk-dataset.dto.ts +++ b/src/entities/dto/create-open-data-dk-dataset.dto.ts @@ -2,38 +2,38 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsEmail, IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator"; export class CreateOpenDataDkDatasetDto { - @ApiProperty({ required: true }) - @IsString() - @IsNotEmpty() - name: string; + @ApiProperty({ required: true }) + @IsString() + @IsNotEmpty() + name: string; - @ApiProperty({ required: true }) - @IsString() - @IsNotEmpty() - description: string; + @ApiProperty({ required: true }) + @IsString() + @IsNotEmpty() + description: string; - @ApiPropertyOptional({ required: false }) - @IsOptional() - @IsString({ each: true, always: true }) - keywords?: string[]; + @ApiPropertyOptional({ required: false }) + @IsOptional() + @IsString({ each: true, always: true }) + keywords?: string[]; - @ApiProperty({ required: true }) - @IsString() - @IsUrl({ protocols: ["http", "https"] }) - license: string; + @ApiProperty({ required: true }) + @IsString() + @IsUrl({ protocols: ["http", "https"] }) + license: string; - @ApiProperty({ required: true }) - @IsString() - @IsNotEmpty() - authorName: string; + @ApiProperty({ required: true }) + @IsString() + @IsNotEmpty() + authorName: string; - @ApiProperty({ required: true }) - @IsString() - @IsEmail() - authorEmail: string; + @ApiProperty({ required: true }) + @IsString() + @IsEmail() + authorEmail: string; - @ApiProperty({ required: true }) - @IsString() - @IsNotEmpty() - resourceTitle: string; + @ApiProperty({ required: true }) + @IsString() + @IsNotEmpty() + resourceTitle: string; } diff --git a/src/entities/dto/create-payload-decoder.dto.ts b/src/entities/dto/create-payload-decoder.dto.ts index b88909d3..b4cdd656 100644 --- a/src/entities/dto/create-payload-decoder.dto.ts +++ b/src/entities/dto/create-payload-decoder.dto.ts @@ -2,20 +2,20 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNumber, IsString, MaxLength, MinLength } from "class-validator"; export class CreatePayloadDecoderDto { - @ApiProperty({ required: true }) - @IsString() - @MinLength(1) - @MaxLength(50) - name: string; + @ApiProperty({ required: true }) + @IsString() + @MinLength(1) + @MaxLength(50) + name: string; - @ApiProperty({ - required: true, - description: "the decoding function must be encoded using JSON.stringify", - }) - @IsString() - decodingFunction: string; + @ApiProperty({ + required: true, + description: "the decoding function must be encoded using JSON.stringify", + }) + @IsString() + decodingFunction: string; - @ApiProperty({ required: true }) - @IsNumber() - organizationId: number; + @ApiProperty({ required: true }) + @IsNumber() + organizationId: number; } diff --git a/src/entities/dto/create-sigfox-settings.dto.ts b/src/entities/dto/create-sigfox-settings.dto.ts index a333accd..44e6a38b 100644 --- a/src/entities/dto/create-sigfox-settings.dto.ts +++ b/src/entities/dto/create-sigfox-settings.dto.ts @@ -1,51 +1,44 @@ import { ApiHideProperty, ApiProperty } from "@nestjs/swagger"; -import { - IsNumber, - IsOptional, - IsString, - Matches, - MaxLength, - ValidateIf, -} from "class-validator"; +import { IsNumber, IsOptional, IsString, Matches, MaxLength, ValidateIf } from "class-validator"; export class CreateSigFoxSettingsDto { - @ApiProperty({ required: true }) - @IsString() - @MaxLength(8) - @Matches(/[0-9A-Fa-f]{1,8}/) - deviceId: string; - - @ApiProperty({ required: false }) - @ValidateIf(o => !o.connectToExistingDeviceInBackend) - @IsString() - @MaxLength(24) - @Matches(/[0-9A-Fa-f]{1,24}/) - deviceTypeId?: string; - - @ApiHideProperty() - deviceTypeName?: string; - - @ApiProperty({ required: true }) - @IsNumber() - groupId: number; - - @ApiHideProperty() - groupName?: string; - - @ApiProperty({ required: false }) - connectToExistingDeviceInBackend?: boolean; - - @ApiProperty({ required: false }) - @ValidateIf(o => !o.connectToExistingDeviceInBackend) - @IsString() - @Matches(/[0-9A-Fa-f]+/) - pac?: string; - - @ApiProperty({ required: false }) - @ValidateIf(o => !o.connectToExistingDeviceInBackend && !o?.prototype) - @IsString() - endProductCertificate?: string; - - @ApiProperty({ required: false }) - prototype?: boolean; + @ApiProperty({ required: true }) + @IsString() + @MaxLength(8) + @Matches(/[0-9A-Fa-f]{1,8}/) + deviceId: string; + + @ApiProperty({ required: false }) + @ValidateIf(o => !o.connectToExistingDeviceInBackend) + @IsString() + @MaxLength(24) + @Matches(/[0-9A-Fa-f]{1,24}/) + deviceTypeId?: string; + + @ApiHideProperty() + deviceTypeName?: string; + + @ApiProperty({ required: true }) + @IsNumber() + groupId: number; + + @ApiHideProperty() + groupName?: string; + + @ApiProperty({ required: false }) + connectToExistingDeviceInBackend?: boolean; + + @ApiProperty({ required: false }) + @ValidateIf(o => !o.connectToExistingDeviceInBackend) + @IsString() + @Matches(/[0-9A-Fa-f]+/) + pac?: string; + + @ApiProperty({ required: false }) + @ValidateIf(o => !o.connectToExistingDeviceInBackend && !o?.prototype) + @IsString() + endProductCertificate?: string; + + @ApiProperty({ required: false }) + prototype?: boolean; } diff --git a/src/entities/dto/current-user-info.dto.ts b/src/entities/dto/current-user-info.dto.ts index 7b12778c..07a5bf07 100644 --- a/src/entities/dto/current-user-info.dto.ts +++ b/src/entities/dto/current-user-info.dto.ts @@ -2,6 +2,6 @@ import { Organization } from "@entities/organization.entity"; import { User } from "@entities/user.entity"; export class CurrentUserInfoDto { - user: User; - organizations: Organization[]; + user: User; + organizations: Organization[]; } diff --git a/src/entities/dto/delete-application-response.dto.ts b/src/entities/dto/delete-application-response.dto.ts index fd865ec7..3bd94fc8 100644 --- a/src/entities/dto/delete-application-response.dto.ts +++ b/src/entities/dto/delete-application-response.dto.ts @@ -1,7 +1,7 @@ export class DeleteResponseDto { - constructor(affected: number) { - this.affected = affected; - } + constructor(affected: number) { + this.affected = affected; + } - affected: number; + affected: number; } diff --git a/src/entities/dto/internal/authenticated-request.ts b/src/entities/dto/internal/authenticated-request.ts index a6f4cc33..43666a8f 100644 --- a/src/entities/dto/internal/authenticated-request.ts +++ b/src/entities/dto/internal/authenticated-request.ts @@ -3,14 +3,14 @@ import { ErrorCodes } from "@enum/error-codes.enum"; import { AuthenticatedUser } from "./authenticated-user"; export type AuthenticatedRequest = { - user: AuthenticatedUser; + user: AuthenticatedUser; }; export type AuthenticatedRequestLocalStrategy = { - user: User; + user: User; }; export class AuthenticatedRequestKombitStrategy { - user: User | ErrorCodes; - cookies: any; + user: User | ErrorCodes; + cookies: any; } diff --git a/src/entities/dto/internal/authenticated-user.ts b/src/entities/dto/internal/authenticated-user.ts index 923d5cb6..2809aa5e 100644 --- a/src/entities/dto/internal/authenticated-user.ts +++ b/src/entities/dto/internal/authenticated-user.ts @@ -1,9 +1,9 @@ import { UserPermissions } from "@dto/permission-organization-application.dto"; export class AuthenticatedUser { - userId: number; - username: string; - permissions?: UserPermissions; - nameID?: string; - nameIDFormat?: string; + userId: number; + username: string; + permissions?: UserPermissions; + nameID?: string; + nameIDFormat?: string; } diff --git a/src/entities/dto/internal/jwt-payload.dto.ts b/src/entities/dto/internal/jwt-payload.dto.ts index 95a4692d..5753b4f3 100644 --- a/src/entities/dto/internal/jwt-payload.dto.ts +++ b/src/entities/dto/internal/jwt-payload.dto.ts @@ -1,5 +1,5 @@ export class JwtPayloadDto { - sub: number; - username: string; - isKombit?: boolean; + sub: number; + username: string; + isKombit?: boolean; } diff --git a/src/entities/dto/iot-device/create-iot-device-batch.dto.ts b/src/entities/dto/iot-device/create-iot-device-batch.dto.ts index 730fec80..da2f9fc3 100644 --- a/src/entities/dto/iot-device/create-iot-device-batch.dto.ts +++ b/src/entities/dto/iot-device/create-iot-device-batch.dto.ts @@ -3,9 +3,9 @@ import { ArrayMaxSize, ArrayNotEmpty, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; export class CreateIoTDeviceBatchDto { - @ArrayNotEmpty() - @ArrayMaxSize(50) - @ValidateNested({ each: true }) - @Type(() => CreateIoTDeviceDto) - data: CreateIoTDeviceDto[]; + @ArrayNotEmpty() + @ArrayMaxSize(50) + @ValidateNested({ each: true }) + @Type(() => CreateIoTDeviceDto) + data: CreateIoTDeviceDto[]; } diff --git a/src/entities/dto/iot-device/create-iot-device-map.dto.ts b/src/entities/dto/iot-device/create-iot-device-map.dto.ts index eb803945..875c090e 100644 --- a/src/entities/dto/iot-device/create-iot-device-map.dto.ts +++ b/src/entities/dto/iot-device/create-iot-device-map.dto.ts @@ -5,16 +5,16 @@ import { IoTDevice } from "@entities/iot-device.entity"; * Represents an IoT device payload from a client */ export type CreateIoTDeviceMapDto = { - /** - * Client payload - */ - iotDeviceDto: CreateIoTDeviceDto; - /** - * If an operation on the dto succeeds, this should be set - */ - iotDevice?: IoTDevice; - /** - * If an operation on the dto fails, this should be set - */ - error?: Omit; + /** + * Client payload + */ + iotDeviceDto: CreateIoTDeviceDto; + /** + * If an operation on the dto succeeds, this should be set + */ + iotDevice?: IoTDevice; + /** + * If an operation on the dto fails, this should be set + */ + error?: Omit; }; diff --git a/src/entities/dto/iot-device/iot-device-batch-response.dto.ts b/src/entities/dto/iot-device/iot-device-batch-response.dto.ts index d35dbe4e..d390c5cd 100644 --- a/src/entities/dto/iot-device/iot-device-batch-response.dto.ts +++ b/src/entities/dto/iot-device/iot-device-batch-response.dto.ts @@ -1,13 +1,13 @@ import { IoTDevice } from "@entities/iot-device.entity"; export class IotDeviceBatchResponseDto { - data?: IoTDevice; - /** - * Identification metadata about the payload in case it was not valid - */ - idMetadata: { - name: string; - applicationId: number; - }; - error?: Omit; + data?: IoTDevice; + /** + * Identification metadata about the payload in case it was not valid + */ + idMetadata: { + name: string; + applicationId: number; + }; + error?: Omit; } diff --git a/src/entities/dto/iot-device/update-iot-device-batch.dto.ts b/src/entities/dto/iot-device/update-iot-device-batch.dto.ts index e0a39b78..87979c65 100644 --- a/src/entities/dto/iot-device/update-iot-device-batch.dto.ts +++ b/src/entities/dto/iot-device/update-iot-device-batch.dto.ts @@ -3,9 +3,9 @@ import { ArrayMaxSize, ArrayNotEmpty, ValidateNested } from "class-validator"; import { Type } from "class-transformer"; export class UpdateIoTDeviceBatchDto { - @ArrayNotEmpty() - @ArrayMaxSize(50) - @ValidateNested({ each: true }) - @Type(() => UpdateIoTDeviceDto) - data: UpdateIoTDeviceDto[]; + @ArrayNotEmpty() + @ArrayMaxSize(50) + @ValidateNested({ each: true }) + @Type(() => UpdateIoTDeviceDto) + data: UpdateIoTDeviceDto[]; } diff --git a/src/entities/dto/jwt-response.dto.ts b/src/entities/dto/jwt-response.dto.ts index 855aec7b..23e9b777 100644 --- a/src/entities/dto/jwt-response.dto.ts +++ b/src/entities/dto/jwt-response.dto.ts @@ -1,3 +1,3 @@ export class JwtResponseDto { - accessToken: string; + accessToken: string; } diff --git a/src/entities/dto/kafka/raw-gateway-state.dto.ts b/src/entities/dto/kafka/raw-gateway-state.dto.ts index 6b9d57fb..ecee00b0 100644 --- a/src/entities/dto/kafka/raw-gateway-state.dto.ts +++ b/src/entities/dto/kafka/raw-gateway-state.dto.ts @@ -1,5 +1,5 @@ import { RawRequestDto } from "./raw-request.dto"; export class RawGatewayStateDto extends RawRequestDto { - gatewayId: string; + gatewayId: string; } diff --git a/src/entities/dto/kafka/raw-iot-device-request.dto.ts b/src/entities/dto/kafka/raw-iot-device-request.dto.ts index 072efb61..dfb6a027 100644 --- a/src/entities/dto/kafka/raw-iot-device-request.dto.ts +++ b/src/entities/dto/kafka/raw-iot-device-request.dto.ts @@ -1,5 +1,5 @@ import { RawRequestDto } from "./raw-request.dto"; export class RawIoTDeviceRequestDto extends RawRequestDto { - iotDeviceId: number; + iotDeviceId: number; } diff --git a/src/entities/dto/kafka/raw-request.dto.ts b/src/entities/dto/kafka/raw-request.dto.ts index b15cf370..cac8cc31 100644 --- a/src/entities/dto/kafka/raw-request.dto.ts +++ b/src/entities/dto/kafka/raw-request.dto.ts @@ -1,7 +1,7 @@ import { IoTDeviceType } from "@enum/device-type.enum"; export class RawRequestDto { - type: IoTDeviceType[number]; - rawPayload: JSON; - unixTimestamp?: number; + type: IoTDeviceType[number]; + rawPayload: JSON; + unixTimestamp?: number; } diff --git a/src/entities/dto/kafka/transformed-payload.dto.ts b/src/entities/dto/kafka/transformed-payload.dto.ts index 95e948e5..8f5a0d89 100644 --- a/src/entities/dto/kafka/transformed-payload.dto.ts +++ b/src/entities/dto/kafka/transformed-payload.dto.ts @@ -1,5 +1,5 @@ export class TransformedPayloadDto { - payload: JSON; - iotDeviceId: number; - payloadDecoderId?: number; + payload: JSON; + iotDeviceId: number; + payloadDecoderId?: number; } diff --git a/src/entities/dto/list-all-applications-response.dto.ts b/src/entities/dto/list-all-applications-response.dto.ts index b211fcb3..b747f64f 100644 --- a/src/entities/dto/list-all-applications-response.dto.ts +++ b/src/entities/dto/list-all-applications-response.dto.ts @@ -1,6 +1,4 @@ import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; import { Application } from "@entities/application.entity"; -export class ListAllApplicationsResponseDto extends ListAllEntitiesResponseDto< - Application -> {} +export class ListAllApplicationsResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/list-all-applications.dto.ts b/src/entities/dto/list-all-applications.dto.ts index a6b5d3de..ffeeb13e 100644 --- a/src/entities/dto/list-all-applications.dto.ts +++ b/src/entities/dto/list-all-applications.dto.ts @@ -7,19 +7,19 @@ import { Transform } from "class-transformer"; import { IsOptional, IsNumber } from "class-validator"; export class ListAllApplicationsDto extends OmitType(ListAllEntitiesDto, ["limit", "offset"]) { - @IsSwaggerOptional({ description: "Filter to one organization" }) - @StringToNumber() - organizationId?: number; + @IsSwaggerOptional({ description: "Filter to one organization" }) + @StringToNumber() + organizationId?: number; - @ApiProperty({ type: Number, required: false }) - @IsOptional() - @IsNumber() - @Transform(({ value }) => NullableStringToNumber(value)) - limit? = DefaultLimit; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @IsNumber() + @Transform(({ value }) => NullableStringToNumber(value)) + limit? = DefaultLimit; - @ApiProperty({ type: Number, required: false }) - @IsOptional() - @IsNumber() - @Transform(({ value }) => NullableStringToNumber(value)) - offset? = DefaultOffset; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @IsNumber() + @Transform(({ value }) => NullableStringToNumber(value)) + offset? = DefaultOffset; } diff --git a/src/entities/dto/list-all-connections-response.dto.ts b/src/entities/dto/list-all-connections-response.dto.ts index 71340ab1..0ecf3f4f 100644 --- a/src/entities/dto/list-all-connections-response.dto.ts +++ b/src/entities/dto/list-all-connections-response.dto.ts @@ -1,6 +1,4 @@ import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; import { IoTDevicePayloadDecoderDataTargetConnection } from "@entities/iot-device-payload-decoder-data-target-connection.entity"; -export class ListAllConnectionsResponseDto extends ListAllEntitiesResponseDto< - IoTDevicePayloadDecoderDataTargetConnection -> {} +export class ListAllConnectionsResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/list-all-data-targets-response.dto.ts b/src/entities/dto/list-all-data-targets-response.dto.ts index a5243ccb..85859694 100644 --- a/src/entities/dto/list-all-data-targets-response.dto.ts +++ b/src/entities/dto/list-all-data-targets-response.dto.ts @@ -1,6 +1,4 @@ import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; import { DataTarget } from "@entities/data-target.entity"; -export class ListAllDataTargetsResponseDto extends ListAllEntitiesResponseDto< - DataTarget -> {} +export class ListAllDataTargetsResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/list-all-data-targets.dto.ts b/src/entities/dto/list-all-data-targets.dto.ts index f8a2b08e..9c8f4d7a 100644 --- a/src/entities/dto/list-all-data-targets.dto.ts +++ b/src/entities/dto/list-all-data-targets.dto.ts @@ -3,11 +3,10 @@ import { ApiProperty } from "@nestjs/swagger"; import { ListAllEntitiesDto } from "./list-all-entities.dto"; export class ListAllDataTargetsDto extends ListAllEntitiesDto { - @ApiProperty({ - type: Number, - required: false, - description: - "Limit the results to the data-targets associated with a single application", - }) - applicationId?: number; + @ApiProperty({ + type: Number, + required: false, + description: "Limit the results to the data-targets associated with a single application", + }) + applicationId?: number; } diff --git a/src/entities/dto/list-all-device-model-response.dto.ts b/src/entities/dto/list-all-device-model-response.dto.ts index 117fefdc..f8b6e50c 100644 --- a/src/entities/dto/list-all-device-model-response.dto.ts +++ b/src/entities/dto/list-all-device-model-response.dto.ts @@ -1,6 +1,4 @@ import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; import { DeviceModel } from "@entities/device-model.entity"; -export class ListAllDeviceModelResponseDto extends ListAllEntitiesResponseDto< - DeviceModel -> {} +export class ListAllDeviceModelResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/list-all-device-models.dto.ts b/src/entities/dto/list-all-device-models.dto.ts index 606376aa..3a533dda 100644 --- a/src/entities/dto/list-all-device-models.dto.ts +++ b/src/entities/dto/list-all-device-models.dto.ts @@ -2,6 +2,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { ListAllEntitiesDto } from "./list-all-entities.dto"; export class ListAllDeviceModelsDto extends ListAllEntitiesDto { - @ApiProperty({ required: false, type: Number }) - organizationId?: number; + @ApiProperty({ required: false, type: Number }) + organizationId?: number; } diff --git a/src/entities/dto/list-all-entities-response.dto.ts b/src/entities/dto/list-all-entities-response.dto.ts index 662ad286..316d6b5b 100644 --- a/src/entities/dto/list-all-entities-response.dto.ts +++ b/src/entities/dto/list-all-entities-response.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from "@nestjs/swagger"; export class ListAllEntitiesResponseDto { - @ApiProperty() - data: T[]; - @ApiProperty() - count: number; + @ApiProperty() + data: T[]; + @ApiProperty() + count: number; } diff --git a/src/entities/dto/list-all-entities.dto.ts b/src/entities/dto/list-all-entities.dto.ts index 10847c33..83eff7d1 100644 --- a/src/entities/dto/list-all-entities.dto.ts +++ b/src/entities/dto/list-all-entities.dto.ts @@ -4,43 +4,43 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsOptional, IsString } from "class-validator"; export class ListAllEntitiesDto { - @ApiProperty({ type: Number, required: false }) - @IsOptional() - @StringToNumber() - limit? = DefaultLimit; - @ApiProperty({ type: Number, required: false }) - @IsOptional() - @StringToNumber() - offset? = DefaultOffset; - @ApiProperty({ type: String, required: false }) - @IsOptional() - @IsString() - sort?: "ASC" | "DESC"; - @ApiProperty({ type: String, required: false }) - @IsOptional() - @IsString() - orderOn?: - | "id" - | "name" - | "createdAt" - | "updatedAt" - | "lastLogin" - | "type" - | "organisations" - | "active" - | "groupName" - | "rssi" - | "snr" - | "status" - | "startDate" - | "endDate" - | "owner" - | "contactPerson" - | "personalData" - | "openDataDkEnabled" - | "deviceModel" - | "devices" - | "dataTargets" - | "organizationName" - | "commentOnLocation" + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @StringToNumber() + limit? = DefaultLimit; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @StringToNumber() + offset? = DefaultOffset; + @ApiProperty({ type: String, required: false }) + @IsOptional() + @IsString() + sort?: "ASC" | "DESC"; + @ApiProperty({ type: String, required: false }) + @IsOptional() + @IsString() + orderOn?: + | "id" + | "name" + | "createdAt" + | "updatedAt" + | "lastLogin" + | "type" + | "organisations" + | "active" + | "groupName" + | "rssi" + | "snr" + | "status" + | "startDate" + | "endDate" + | "owner" + | "contactPerson" + | "personalData" + | "openDataDkEnabled" + | "deviceModel" + | "devices" + | "dataTargets" + | "organizationName" + | "commentOnLocation"; } diff --git a/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts b/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts index ce01c041..60829121 100644 --- a/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts +++ b/src/entities/dto/list-all-iot-devices-minimal-response.dto.ts @@ -2,42 +2,42 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsSwaggerOptional } from "@helpers/optional-validator"; export class ListAllIoTDevicesMinimalResponseDto { - @ApiProperty() - data: IoTDeviceMinimal[]; - @ApiProperty() - count: number; + @ApiProperty() + data: IoTDeviceMinimal[]; + @ApiProperty() + count: number; } export class IoTDeviceMinimal { - id: number; + id: number; - name: string; + name: string; - canRead: boolean; + canRead: boolean; - applicationId: number; + applicationId: number; - organizationId: number; + organizationId: number; - lastActiveTime: Date; + lastActiveTime: Date; } export class IoTDeviceMinimalRaw { - id: number; + id: number; - name: string; + name: string; - applicationId: number; + applicationId: number; - organizationId: number; + organizationId: number; - sentTime: Date; + sentTime: Date; } export class PayloadDecoderIoDeviceMinimalQuery { - @IsSwaggerOptional() - limit? = 20; + @IsSwaggerOptional() + limit? = 20; - @IsSwaggerOptional() - offset? = 0; + @IsSwaggerOptional() + offset? = 0; } diff --git a/src/entities/dto/list-all-iot-devices-to-map-response.dto.ts b/src/entities/dto/list-all-iot-devices-to-map-response.dto.ts index 051cde7f..41afd641 100644 --- a/src/entities/dto/list-all-iot-devices-to-map-response.dto.ts +++ b/src/entities/dto/list-all-iot-devices-to-map-response.dto.ts @@ -2,9 +2,9 @@ import { IoTDeviceType } from "@enum/device-type.enum"; import { Point } from "geojson"; export class IoTDevicesListToMapResponseDto { - id: number; - name: string; - type: IoTDeviceType; - latestSentMessage: Date; - location: Point; + id: number; + name: string; + type: IoTDeviceType; + latestSentMessage: Date; + location: Point; } diff --git a/src/entities/dto/list-all-multicasts.dto.ts b/src/entities/dto/list-all-multicasts.dto.ts index a27155c2..6f7c4268 100644 --- a/src/entities/dto/list-all-multicasts.dto.ts +++ b/src/entities/dto/list-all-multicasts.dto.ts @@ -3,11 +3,10 @@ import { ApiProperty } from "@nestjs/swagger"; import { ListAllEntitiesDto } from "./list-all-entities.dto"; export class ListAllMulticastsDto extends ListAllEntitiesDto { - @ApiProperty({ - type: Number, - required: false, - description: - "Limit the results to the multicasts associated with a single application", - }) - applicationId?: number; + @ApiProperty({ + type: Number, + required: false, + description: "Limit the results to the multicasts associated with a single application", + }) + applicationId?: number; } diff --git a/src/entities/dto/list-all-organizations-response.dto.ts b/src/entities/dto/list-all-organizations-response.dto.ts index b7f5a1d7..03718944 100644 --- a/src/entities/dto/list-all-organizations-response.dto.ts +++ b/src/entities/dto/list-all-organizations-response.dto.ts @@ -3,12 +3,8 @@ import { PickType } from "@nestjs/swagger"; import { Organization } from "@entities/organization.entity"; import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; -export class ListAllOrganizationsResponseDto extends ListAllEntitiesResponseDto< - Organization -> {} +export class ListAllOrganizationsResponseDto extends ListAllEntitiesResponseDto {} -export class ListAllMinimalOrganizationsResponseDto extends ListAllEntitiesResponseDto< - MinimalOrganization -> {} +export class ListAllMinimalOrganizationsResponseDto extends ListAllEntitiesResponseDto {} export class MinimalOrganization extends PickType(Organization, ["id", "name"]) {} diff --git a/src/entities/dto/list-all-paginated.dto.ts b/src/entities/dto/list-all-paginated.dto.ts index cd22d6b3..8e920121 100644 --- a/src/entities/dto/list-all-paginated.dto.ts +++ b/src/entities/dto/list-all-paginated.dto.ts @@ -1,8 +1,8 @@ import { IsSwaggerOptional } from "@helpers/optional-validator"; export class ListAllPaginated { - @IsSwaggerOptional({ type: Number }) - limit? = 100; - @IsSwaggerOptional({ type: Number }) - offset? = 0; + @IsSwaggerOptional({ type: Number }) + limit? = 100; + @IsSwaggerOptional({ type: Number }) + offset? = 0; } diff --git a/src/entities/dto/list-all-payload-decoder.dto.ts b/src/entities/dto/list-all-payload-decoder.dto.ts index e7af4220..80948cb6 100644 --- a/src/entities/dto/list-all-payload-decoder.dto.ts +++ b/src/entities/dto/list-all-payload-decoder.dto.ts @@ -3,6 +3,6 @@ import { ApiPropertyOptional } from "@nestjs/swagger"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; export class ListAllPayloadDecoderDto extends ListAllEntitiesDto { - @ApiPropertyOptional({ description: "Filter to one organization" }) - organizationId?: number; + @ApiPropertyOptional({ description: "Filter to one organization" }) + organizationId?: number; } diff --git a/src/entities/dto/list-all-payload-decoders-response.dto.ts b/src/entities/dto/list-all-payload-decoders-response.dto.ts index b505d4b4..84fd2594 100644 --- a/src/entities/dto/list-all-payload-decoders-response.dto.ts +++ b/src/entities/dto/list-all-payload-decoders-response.dto.ts @@ -1,6 +1,4 @@ import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; import { PayloadDecoder } from "@entities/payload-decoder.entity"; -export class ListAllPayloadDecoderResponseDto extends ListAllEntitiesResponseDto< - PayloadDecoder -> {} +export class ListAllPayloadDecoderResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/list-all-permissions-response.dto.ts b/src/entities/dto/list-all-permissions-response.dto.ts index d5411a0a..6b1268e8 100644 --- a/src/entities/dto/list-all-permissions-response.dto.ts +++ b/src/entities/dto/list-all-permissions-response.dto.ts @@ -2,6 +2,4 @@ import { Permission } from "@entities/permissions/permission.entity"; import { ListAllEntitiesResponseDto } from "./list-all-entities-response.dto"; -export class ListAllPermissionsResponseDto extends ListAllEntitiesResponseDto< - Permission -> {} +export class ListAllPermissionsResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/list-all-permissions.dto.ts b/src/entities/dto/list-all-permissions.dto.ts index 8728eff9..b92304d1 100644 --- a/src/entities/dto/list-all-permissions.dto.ts +++ b/src/entities/dto/list-all-permissions.dto.ts @@ -2,9 +2,9 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ListAllEntitiesDto } from "./list-all-entities.dto"; export class ListAllPermissionsDto extends ListAllEntitiesDto { - @ApiProperty({ type: String, required: false }) - organisationId?: string; + @ApiProperty({ type: String, required: false }) + organisationId?: string; - @ApiProperty({ type: String, required: false }) - userId?: string; + @ApiProperty({ type: String, required: false }) + userId?: string; } diff --git a/src/entities/dto/list-all-search-results-response.dto.ts b/src/entities/dto/list-all-search-results-response.dto.ts index 569301e1..32e0803a 100644 --- a/src/entities/dto/list-all-search-results-response.dto.ts +++ b/src/entities/dto/list-all-search-results-response.dto.ts @@ -2,8 +2,8 @@ import { ApiProperty } from "@nestjs/swagger"; import { SearchResultDto } from "./search-result.dto"; export class ListAllSearchResultsResponseDto { - @ApiProperty() - data: SearchResultDto[]; - @ApiProperty() - count: number; + @ApiProperty() + data: SearchResultDto[]; + @ApiProperty() + count: number; } diff --git a/src/entities/dto/list-all-users-minimal-response.dto.ts b/src/entities/dto/list-all-users-minimal-response.dto.ts index 8a6cdec4..79004f2a 100644 --- a/src/entities/dto/list-all-users-minimal-response.dto.ts +++ b/src/entities/dto/list-all-users-minimal-response.dto.ts @@ -1,8 +1,8 @@ export class ListAllUsersMinimalResponseDto { - users: UsersMinimal[]; + users: UsersMinimal[]; } export class UsersMinimal { - id: number; - name: string; + id: number; + name: string; } diff --git a/src/entities/dto/list-all-users-response.dto.ts b/src/entities/dto/list-all-users-response.dto.ts index 5b46cc33..c99843ef 100644 --- a/src/entities/dto/list-all-users-response.dto.ts +++ b/src/entities/dto/list-all-users-response.dto.ts @@ -1,6 +1,4 @@ import { ListAllEntitiesResponseDto } from "./list-all-entities-response.dto"; import { UserResponseDto } from "./user-response.dto"; -export class ListAllUsersResponseDto extends ListAllEntitiesResponseDto< - UserResponseDto -> {} +export class ListAllUsersResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/login.dto.ts b/src/entities/dto/login.dto.ts index eaae5a64..cf026fcd 100644 --- a/src/entities/dto/login.dto.ts +++ b/src/entities/dto/login.dto.ts @@ -2,10 +2,10 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsString } from "class-validator"; export class LoginDto { - @ApiProperty({ default: "john@localhost.dk" }) - @IsString() - username: string; - @ApiProperty({ default: "hunter2" }) - @IsString() - password: string; + @ApiProperty({ default: "john@localhost.dk" }) + @IsString() + username: string; + @ApiProperty({ default: "hunter2" }) + @IsString() + password: string; } diff --git a/src/entities/dto/lorawan-device-with-chirpstack-data.dto.ts b/src/entities/dto/lorawan-device-with-chirpstack-data.dto.ts index 4135a495..4257a09d 100644 --- a/src/entities/dto/lorawan-device-with-chirpstack-data.dto.ts +++ b/src/entities/dto/lorawan-device-with-chirpstack-data.dto.ts @@ -2,5 +2,5 @@ import { LoRaWANDevice } from "@entities/lorawan-device.entity"; import { CreateLoRaWANSettingsDto } from "./create-lorawan-settings.dto"; export class LoRaWANDeviceWithChirpstackDataDto extends LoRaWANDevice { - lorawanSettings: CreateLoRaWANSettingsDto; + lorawanSettings: CreateLoRaWANSettingsDto; } diff --git a/src/entities/dto/mqtt-external-broker-device.dto.ts b/src/entities/dto/mqtt-external-broker-device.dto.ts index 01148771..e9387e14 100644 --- a/src/entities/dto/mqtt-external-broker-device.dto.ts +++ b/src/entities/dto/mqtt-external-broker-device.dto.ts @@ -2,9 +2,9 @@ import { MQTTDetails } from "@dto/mqtt-internal-broker-device.dto"; export class MQTTExternalBrokerDeviceDTO extends MQTTExternalBrokerDevice { - mqttExternalBrokerSettings: MQTTExternalBrokerSettingsDTO; + mqttExternalBrokerSettings: MQTTExternalBrokerSettingsDTO; } export class MQTTExternalBrokerSettingsDTO extends MQTTDetails { - invalidMqttConfig: boolean; + invalidMqttConfig: boolean; } diff --git a/src/entities/dto/mqtt-internal-broker-device.dto.ts b/src/entities/dto/mqtt-internal-broker-device.dto.ts index d64f470f..25a2af00 100644 --- a/src/entities/dto/mqtt-internal-broker-device.dto.ts +++ b/src/entities/dto/mqtt-internal-broker-device.dto.ts @@ -3,18 +3,18 @@ import { AuthenticationType } from "@enum/authentication-type.enum"; import { MQTTPermissionLevel } from "@enum/mqtt-permission-level.enum"; export class MQTTInternalBrokerDeviceDTO extends MQTTInternalBrokerDevice { - mqttInternalBrokerSettings: MQTTDetails; + mqttInternalBrokerSettings: MQTTDetails; } export class MQTTDetails { - authenticationType: AuthenticationType; - permissions: MQTTPermissionLevel; - caCertificate: string; - deviceCertificate: string; - deviceCertificateKey: string; - mqttusername: string; - mqttpassword: string; - mqttURL: string; - mqttPort: number; - mqtttopicname: string; + authenticationType: AuthenticationType; + permissions: MQTTPermissionLevel; + caCertificate: string; + deviceCertificate: string; + deviceCertificateKey: string; + mqttusername: string; + mqttpassword: string; + mqttURL: string; + mqttPort: number; + mqtttopicname: string; } diff --git a/src/entities/dto/oddk-mail-info.dto.ts b/src/entities/dto/oddk-mail-info.dto.ts index b58f8ef9..5623935d 100644 --- a/src/entities/dto/oddk-mail-info.dto.ts +++ b/src/entities/dto/oddk-mail-info.dto.ts @@ -2,22 +2,22 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNumber, IsString, IsUrl, MaxLength } from "class-validator"; export class OddkMailInfo { - @ApiProperty({ required: true }) - @IsNumber() - organizationId: number; + @ApiProperty({ required: true }) + @IsNumber() + organizationId: number; - @ApiProperty({ required: true }) - @IsString() - organizationOddkAlias: string; + @ApiProperty({ required: true }) + @IsString() + organizationOddkAlias: string; - @ApiProperty({ required: false }) - comment?: string; + @ApiProperty({ required: false }) + comment?: string; - @ApiProperty({ required: true }) - @IsString() - @MaxLength(1024) - @IsUrl({ - require_tld: false, - }) - sharingUrl: string; + @ApiProperty({ required: true }) + @IsString() + @MaxLength(1024) + @IsUrl({ + require_tld: false, + }) + sharingUrl: string; } diff --git a/src/entities/dto/open-data-dk-dcat.dto.ts b/src/entities/dto/open-data-dk-dcat.dto.ts index 4f9beab1..c5aad646 100644 --- a/src/entities/dto/open-data-dk-dcat.dto.ts +++ b/src/entities/dto/open-data-dk-dcat.dto.ts @@ -1,50 +1,50 @@ import { Expose } from "class-transformer"; export class Publisher { - name: string; + name: string; } export class ContactPoint { - @Expose({ name: "@type" }) - "@type": string; - fn: string; - hasEmail: string; + @Expose({ name: "@type" }) + "@type": string; + fn: string; + hasEmail: string; } export class Distribution { - @Expose({ name: "@type" }) - "@type": string; - title?: string; - format: string; - license: string; - mediaType: string; - accessURL: string; + @Expose({ name: "@type" }) + "@type": string; + title?: string; + format: string; + license: string; + mediaType: string; + accessURL: string; } export class Dataset { - @Expose({ name: "@type" }) - "@type": string; - identifier: string; - landingPage: string; - title: string; - description: string; - keyword: string[]; - issued: Date; - modified: Date; - publisher: Publisher; - contactPoint: ContactPoint; - accessLevel: string; - distribution: Distribution[]; - spatial: string; - theme: string[]; + @Expose({ name: "@type" }) + "@type": string; + identifier: string; + landingPage: string; + title: string; + description: string; + keyword: string[]; + issued: Date; + modified: Date; + publisher: Publisher; + contactPoint: ContactPoint; + accessLevel: string; + distribution: Distribution[]; + spatial: string; + theme: string[]; } export class DCATRootObject { - @Expose({ name: "@context" }) - "@context": string; - @Expose({ name: "@type" }) - "@type": string; - conformsTo: string; - describedBy: string; - dataset: Dataset[]; + @Expose({ name: "@context" }) + "@context": string; + @Expose({ name: "@type" }) + "@type": string; + conformsTo: string; + describedBy: string; + dataset: Dataset[]; } diff --git a/src/entities/dto/permission-minimal.dto.ts b/src/entities/dto/permission-minimal.dto.ts index f7aeb829..7c1a6505 100644 --- a/src/entities/dto/permission-minimal.dto.ts +++ b/src/entities/dto/permission-minimal.dto.ts @@ -1,7 +1,7 @@ import { PermissionType } from "@enum/permission-type.enum"; export class PermissionMinimalDto { - permission_type_type: PermissionType; - organization_id: number; - application_id: number; + permission_type_type: PermissionType; + organization_id: number; + application_id: number; } diff --git a/src/entities/dto/permission-organization-application.dto.ts b/src/entities/dto/permission-organization-application.dto.ts index d5c969ab..018172b8 100644 --- a/src/entities/dto/permission-organization-application.dto.ts +++ b/src/entities/dto/permission-organization-application.dto.ts @@ -1,84 +1,81 @@ import * as _ from "lodash"; export class UserPermissions { - constructor() { - this.orgToReadPermissions = new Map(); - this.orgToUserAdminPermissions = new Map(); - this.orgToGatewayAdminPermissions = new Set(); - this.orgToApplicationAdminPermissions = new Map(); + constructor() { + this.orgToReadPermissions = new Map(); + this.orgToUserAdminPermissions = new Map(); + this.orgToGatewayAdminPermissions = new Set(); + this.orgToApplicationAdminPermissions = new Map(); + } + + orgToReadPermissions: Map; + orgToUserAdminPermissions: Map; + orgToGatewayAdminPermissions: Set; + orgToApplicationAdminPermissions: Map; + isGlobalAdmin = false; + + getAllApplicationsWithAtLeastRead(): number[] { + return _.union( + this.extractValues(this.orgToReadPermissions), + this.extractValues(this.orgToUserAdminPermissions), + this.getAllApplicationsWithAdmin() + ); + } + + getAllApplicationsWithAdmin(): number[] { + return this.extractValues(this.orgToApplicationAdminPermissions); + } + + getAllOrganizationsWithAtLeastUserAdminRead(): number[] { + return _.union(this.extractKeys(this.orgToReadPermissions), this.getAllOrganizationsWithUserAdmin()); + } + + getAllOrganizationsWithAtLeastApplicationRead(): number[] { + return _.union( + this.extractKeys(this.orgToReadPermissions), + this.getAllOrganizationsWithApplicationAdmin(), + this.getAllOrganizationsWithUserAdmin() + ); + } + + getAllOrganizationsWithUserAdmin(): number[] { + return this.extractKeys(this.orgToUserAdminPermissions); + } + + getAllOrganizationsWithGatewayAdmin(): number[] { + return Array.from(this.orgToGatewayAdminPermissions); + } + + getAllOrganizationsWithApplicationAdmin(): number[] { + return this.extractKeys(this.orgToApplicationAdminPermissions); + } + + hasUserAdminOnOrganization(organizationId: number): boolean { + if (this.isGlobalAdmin) { + return true; + } else { + let organizationsWithAdmin = this.getAllOrganizationsWithUserAdmin(); + return organizationsWithAdmin.indexOf(organizationId) > -1; } + } - orgToReadPermissions: Map; - orgToUserAdminPermissions: Map; - orgToGatewayAdminPermissions: Set; - orgToApplicationAdminPermissions: Map; - isGlobalAdmin = false; - - getAllApplicationsWithAtLeastRead(): number[] { - return _.union( - this.extractValues(this.orgToReadPermissions), - this.extractValues(this.orgToUserAdminPermissions), - this.getAllApplicationsWithAdmin() - ); - } + private extractValues(map: Map): number[] { + let res: number[] = []; - getAllApplicationsWithAdmin(): number[] { - return this.extractValues(this.orgToApplicationAdminPermissions); - } + map.forEach(val => { + res = _.union(res, val); + }); - getAllOrganizationsWithAtLeastUserAdminRead(): number[] { - return _.union( - this.extractKeys(this.orgToReadPermissions), - this.getAllOrganizationsWithUserAdmin() - ); - } + return res; + } - getAllOrganizationsWithAtLeastApplicationRead(): number[] { - return _.union( - this.extractKeys(this.orgToReadPermissions), - this.getAllOrganizationsWithApplicationAdmin(), - this.getAllOrganizationsWithUserAdmin(), - ); - } + private extractKeys(map: Map): number[] { + const res: number[] = []; - getAllOrganizationsWithUserAdmin(): number[] { - return this.extractKeys(this.orgToUserAdminPermissions); + for (const key of map.keys()) { + res.push(+key); } - getAllOrganizationsWithGatewayAdmin(): number[] { - return Array.from(this.orgToGatewayAdminPermissions); - } - - getAllOrganizationsWithApplicationAdmin(): number[] { - return this.extractKeys(this.orgToApplicationAdminPermissions); - } - - hasUserAdminOnOrganization(organizationId: number): boolean { - if (this.isGlobalAdmin) { - return true; - } else { - let organizationsWithAdmin = this.getAllOrganizationsWithUserAdmin(); - return organizationsWithAdmin.indexOf(organizationId) > -1; - } - } - - private extractValues(map: Map): number[] { - let res: number[] = []; - - map.forEach(val => { - res = _.union(res, val); - }); - - return res; - } - - private extractKeys(map: Map): number[] { - const res: number[] = []; - - for (const key of map.keys()) { - res.push(+key); - } - - return res; - } + return res; + } } diff --git a/src/entities/dto/receive-data.dto.ts b/src/entities/dto/receive-data.dto.ts index 55ec935a..91ab25b5 100644 --- a/src/entities/dto/receive-data.dto.ts +++ b/src/entities/dto/receive-data.dto.ts @@ -9,7 +9,7 @@ import { IsOptional } from "class-validator"; * @see https://github.com/typestack/class-validator/issues/1503 */ export class ReceiveDataDto { - @Exclude() - @IsOptional() - ignoreMe: unknown; + @Exclude() + @IsOptional() + ignoreMe: unknown; } diff --git a/src/entities/dto/search-result.dto.ts b/src/entities/dto/search-result.dto.ts index c5aefc7e..de3159a2 100644 --- a/src/entities/dto/search-result.dto.ts +++ b/src/entities/dto/search-result.dto.ts @@ -1,45 +1,45 @@ import { DbBaseEntity } from "@entities/base.entity"; export class SearchResultDto { - id: number | string; - - createdAt: Date; - - updatedAt: Date; - - name: string; - - type: SearchResultType; - - applicationId?: number; - organizationId?: number; - organizationName?: string; - - deviceId?: string; - deviceEUI?: string; - apiKey?: string; - gatewayId?: string; - deviceType?: string; - - constructor( - name: string, - id: number | string, - createdAt: Date, - updatedAt: Date, - gatewayId: string, - type?: SearchResultType - ) { - this.name = name; - this.type = type; - this.id = id; - this.gatewayId = gatewayId; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } + id: number | string; + + createdAt: Date; + + updatedAt: Date; + + name: string; + + type: SearchResultType; + + applicationId?: number; + organizationId?: number; + organizationName?: string; + + deviceId?: string; + deviceEUI?: string; + apiKey?: string; + gatewayId?: string; + deviceType?: string; + + constructor( + name: string, + id: number | string, + createdAt: Date, + updatedAt: Date, + gatewayId: string, + type?: SearchResultType + ) { + this.name = name; + this.type = type; + this.id = id; + this.gatewayId = gatewayId; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } } export enum SearchResultType { - Gateway = "Gateway", - IoTDevice = "IoTDevice", - Application = "Application", + Gateway = "Gateway", + IoTDevice = "IoTDevice", + Application = "Application", } diff --git a/src/entities/dto/sigfox-device-with-backend-data.dto.ts b/src/entities/dto/sigfox-device-with-backend-data.dto.ts index e618a11e..cc22e916 100644 --- a/src/entities/dto/sigfox-device-with-backend-data.dto.ts +++ b/src/entities/dto/sigfox-device-with-backend-data.dto.ts @@ -2,5 +2,5 @@ import { SigFoxDevice } from "@entities/sigfox-device.entity"; import { CreateSigFoxSettingsDto } from "./create-sigfox-settings.dto"; export class SigFoxDeviceWithBackendDataDto extends SigFoxDevice { - sigfoxSettings: CreateSigFoxSettingsDto; + sigfoxSettings: CreateSigFoxSettingsDto; } diff --git a/src/entities/dto/sigfox/external/create-sigfox-api-callback-request.dto.ts b/src/entities/dto/sigfox/external/create-sigfox-api-callback-request.dto.ts index 4508bc08..4f9b8bdc 100644 --- a/src/entities/dto/sigfox/external/create-sigfox-api-callback-request.dto.ts +++ b/src/entities/dto/sigfox/external/create-sigfox-api-callback-request.dto.ts @@ -1,13 +1,13 @@ export interface CreateSigFoxApiCallbackRequestDto { - channel: string; - callbackType: number; - callbackSubtype: number; - payloadConfig: string; - enabled: boolean; - url: string; - httpMethod: string; - headers?: any; - sendSni: boolean; - bodyTemplate: string; - contentType: string; + channel: string; + callbackType: number; + callbackSubtype: number; + payloadConfig: string; + enabled: boolean; + url: string; + httpMethod: string; + headers?: any; + sendSni: boolean; + bodyTemplate: string; + contentType: string; } diff --git a/src/entities/dto/sigfox/external/create-sigfox-api-device-request.dto.ts b/src/entities/dto/sigfox/external/create-sigfox-api-device-request.dto.ts index cd8bf58c..826a5ac3 100644 --- a/src/entities/dto/sigfox/external/create-sigfox-api-device-request.dto.ts +++ b/src/entities/dto/sigfox/external/create-sigfox-api-device-request.dto.ts @@ -1,17 +1,17 @@ export class CreateSigFoxApiDeviceRequestDto { - id: string; - name: string; - pac: string; - deviceTypeId: string; + id: string; + name: string; + pac: string; + deviceTypeId: string; - activable?: boolean; - automaticRenewal?: boolean; - lat?: number; - lng?: number; - productCertificate?: ProductCertificate; - prototype?: boolean; + activable?: boolean; + automaticRenewal?: boolean; + lat?: number; + lng?: number; + productCertificate?: ProductCertificate; + prototype?: boolean; } export class ProductCertificate { - key: string; + key: string; } diff --git a/src/entities/dto/sigfox/external/create-sigfox-api-device-type-request.dto.ts b/src/entities/dto/sigfox/external/create-sigfox-api-device-type-request.dto.ts index cc8e6320..60b9013c 100644 --- a/src/entities/dto/sigfox/external/create-sigfox-api-device-type-request.dto.ts +++ b/src/entities/dto/sigfox/external/create-sigfox-api-device-type-request.dto.ts @@ -3,45 +3,45 @@ import { ApiHideProperty, ApiProperty, ApiPropertyOptional } from "@nestjs/swagg import { IsEmail, IsOptional, MaxLength, MinLength } from "class-validator"; export class CreateSigFoxApiDeviceTypeRequestDto { - @ApiProperty({ required: true }) - @MinLength(1) - @MaxLength(100) - name: string; + @ApiProperty({ required: true }) + @MinLength(1) + @MaxLength(100) + name: string; - @ApiProperty({ required: true }) - contractId: string; + @ApiProperty({ required: true }) + contractId: string; - @ApiProperty({ required: true }) - @MaxLength(300) - description: string; + @ApiProperty({ required: true }) + @MaxLength(300) + description: string; - @ApiPropertyOptional() - keepAlive?: number; + @ApiPropertyOptional() + keepAlive?: number; - @ApiPropertyOptional() - @IsOptional() - @IsEmail() - alertEmail?: string; + @ApiPropertyOptional() + @IsOptional() + @IsEmail() + alertEmail?: string; - /* This is required, but is set by the backend. */ - @ApiHideProperty() - groupId?: string; + /* This is required, but is set by the backend. */ + @ApiHideProperty() + groupId?: string; - @ApiHideProperty() - automaticRenewal?: boolean; + @ApiHideProperty() + automaticRenewal?: boolean; - @ApiHideProperty() - geolocPayloadConfigId?: string; + @ApiHideProperty() + geolocPayloadConfigId?: string; - @ApiHideProperty() - downlinkMode?: SigFoxDownlinkMode; + @ApiHideProperty() + downlinkMode?: SigFoxDownlinkMode; - @ApiHideProperty() - downlinkDataString?: string; + @ApiHideProperty() + downlinkDataString?: string; - @ApiHideProperty() - payloadType?: SigFoxPayloadType; + @ApiHideProperty() + payloadType?: SigFoxPayloadType; - @ApiHideProperty() - payloadConfig?: string; + @ApiHideProperty() + payloadConfig?: string; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-bulk-transfer-request.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-bulk-transfer-request.dto.ts index a0b5bb16..5fe30145 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-bulk-transfer-request.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-bulk-transfer-request.dto.ts @@ -1,10 +1,10 @@ export interface SigFoxApiBulkTransferRequestDto { - deviceTypeId: string; - data: SigFoxApiBulkTransferDeviceSettings[]; + deviceTypeId: string; + data: SigFoxApiBulkTransferDeviceSettings[]; } export interface SigFoxApiBulkTransferDeviceSettings { - id: string; - keepHistory: boolean; - activable: boolean; + id: string; + keepHistory: boolean; + activable: boolean; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-callbacks-response.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-callbacks-response.dto.ts index 2b523e2c..2c3d08d1 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-callbacks-response.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-callbacks-response.dto.ts @@ -1,24 +1,24 @@ export interface SigFoxApiCallbacksResponseDto { - data: SigFoxApiCallbackContent[]; + data: SigFoxApiCallbackContent[]; } export interface SigFoxApiCallbackContent { - id: string; - channel: string; - callbackType: number; - payloadConfig: string; - callbackSubtype: number; - enabled: boolean; - dead: boolean; - downlinkHook: boolean; - url: string; - contentType: string; - httpMethod: string; - sendSni: boolean; - headers: any; - linePattern: string; - recipient: string; - subject: string; - message: string; - bodyTemplate: string; + id: string; + channel: string; + callbackType: number; + payloadConfig: string; + callbackSubtype: number; + enabled: boolean; + dead: boolean; + downlinkHook: boolean; + url: string; + contentType: string; + httpMethod: string; + sendSni: boolean; + headers: any; + linePattern: string; + recipient: string; + subject: string; + message: string; + bodyTemplate: string; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-contract-infos-response.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-contract-infos-response.dto.ts index 2df9fbd1..ab9cca64 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-contract-infos-response.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-contract-infos-response.dto.ts @@ -1,33 +1,33 @@ import { SigFoxApiIdReferenceDto } from "./sigfox-api-id-reference.dto"; export interface SigFoxApiContractInfosResponseDto { - data: SigFoxApiContractInfosContent[]; + data: SigFoxApiContractInfosContent[]; } export interface SigFoxApiContractInfosContent { - id: string; - name: string; - order: SigFoxApiIdReferenceDto; - contractId: string; - group: SigFoxApiIdReferenceDto; - userId: string; - startTime: number; - communicationEndTime: number; - timezone: string; - maxUplinkFrames: number; - maxDownlinkFrames: number; - tokenDuration: number; - maxTokens: number; - activationEndTime: number; - automaticRenewal: boolean; - renewalDuration: number; - blacklistedTerritories: any[]; - createdBy: string; - lastEditionTime: number; - lastEditedBy: string; - bidir: boolean; - highPriorityDownlink: boolean; - tokensInUse: number; - tokensUsed: number; - creationTime: number; + id: string; + name: string; + order: SigFoxApiIdReferenceDto; + contractId: string; + group: SigFoxApiIdReferenceDto; + userId: string; + startTime: number; + communicationEndTime: number; + timezone: string; + maxUplinkFrames: number; + maxDownlinkFrames: number; + tokenDuration: number; + maxTokens: number; + activationEndTime: number; + automaticRenewal: boolean; + renewalDuration: number; + blacklistedTerritories: any[]; + createdBy: string; + lastEditionTime: number; + lastEditedBy: string; + bidir: boolean; + highPriorityDownlink: boolean; + tokensInUse: number; + tokensUsed: number; + creationTime: number; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-device-response.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-device-response.dto.ts index 4b214db6..54eee523 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-device-response.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-device-response.dto.ts @@ -1,51 +1,51 @@ import { SigFoxApiIdReferenceDto } from "./sigfox-api-id-reference.dto"; export interface SigFoxApiDeviceResponse { - data: SigFoxApiDeviceContent[]; + data: SigFoxApiDeviceContent[]; } export interface SigFoxApiDeviceContent { - id: string; - name: string; - satelliteCapable: boolean; - sequenceNumber: number; - lastCom: any; - state: number; - comState: number; - pac: string; - location: Location; - deviceType: SigFoxApiIdReferenceDto; - group: SigFoxApiIdReferenceDto; - lqi: number; - activationTime: any; - token: Token; - contract: SigFoxApiIdReferenceDto; - creationTime: any; - modemCertificate: SigFoxApiIdReferenceDto; - prototype: boolean; - productCertificate: SigFoxProductCertificateIdKey; - automaticRenewal: boolean; - automaticRenewalStatus: number; - createdBy: string; - lastEditionTime: any; - lastEditedBy: string; - activable: boolean; + id: string; + name: string; + satelliteCapable: boolean; + sequenceNumber: number; + lastCom: any; + state: number; + comState: number; + pac: string; + location: Location; + deviceType: SigFoxApiIdReferenceDto; + group: SigFoxApiIdReferenceDto; + lqi: number; + activationTime: any; + token: Token; + contract: SigFoxApiIdReferenceDto; + creationTime: any; + modemCertificate: SigFoxApiIdReferenceDto; + prototype: boolean; + productCertificate: SigFoxProductCertificateIdKey; + automaticRenewal: boolean; + automaticRenewalStatus: number; + createdBy: string; + lastEditionTime: any; + lastEditedBy: string; + activable: boolean; } export interface Location { - lat: number; - lng: number; + lat: number; + lng: number; } export interface Token { - state: number; - detailMessage: string; - end: any; - freeMessages?: number; - freeMessagesSent?: number; + state: number; + detailMessage: string; + end: any; + freeMessages?: number; + freeMessagesSent?: number; } export interface SigFoxProductCertificateIdKey { - id: number; - key: string; + id: number; + key: string; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-device-type-response.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-device-type-response.dto.ts index c5fab78a..c877100a 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-device-type-response.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-device-type-response.dto.ts @@ -1,24 +1,24 @@ import { SigFoxApiIdReferenceDto } from "./sigfox-api-id-reference.dto"; export interface SigFoxApiDeviceTypeResponse { - data: SigFoxApiDeviceTypeContent[]; + data: SigFoxApiDeviceTypeContent[]; } export interface SigFoxApiDeviceTypeContent { - id: string; - automaticRenewal: boolean; - name: string; - description: string; - keepAlive: number; - payloadType: number; - downlinkMode: number; - downlinkDataString: string; - group: SigFoxApiIdReferenceDto; - contract: SigFoxApiIdReferenceDto; - contracts: SigFoxApiIdReferenceDto[]; - detachedContracts: SigFoxApiIdReferenceDto[]; - creationTime: any; - createdBy: string; - lastEditionTime: any; - lastEditedBy: string; + id: string; + automaticRenewal: boolean; + name: string; + description: string; + keepAlive: number; + payloadType: number; + downlinkMode: number; + downlinkDataString: string; + group: SigFoxApiIdReferenceDto; + contract: SigFoxApiIdReferenceDto; + contracts: SigFoxApiIdReferenceDto[]; + detachedContracts: SigFoxApiIdReferenceDto[]; + creationTime: any; + createdBy: string; + lastEditionTime: any; + lastEditedBy: string; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-groups-response.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-groups-response.dto.ts index 84a326f0..d4a26df3 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-groups-response.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-groups-response.dto.ts @@ -1,28 +1,28 @@ export interface Path { - id: string; - name: string; + id: string; + name: string; } export interface SigFoxApiGroupsContent { - id: string; - name: string; - description: string; - type: number; - timezone: string; - nameCI: string; - path: Path[]; - currentPrototypeCount: number; - createdBy: string; - creationTime: number; - leaf: boolean; - actions: string[]; + id: string; + name: string; + description: string; + type: number; + timezone: string; + nameCI: string; + path: Path[]; + currentPrototypeCount: number; + createdBy: string; + creationTime: number; + leaf: boolean; + actions: string[]; } export interface Paging { - next: string; + next: string; } export interface SigFoxApiGroupsResponse { - data: SigFoxApiGroupsContent[]; - paging: Paging; + data: SigFoxApiGroupsContent[]; + paging: Paging; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-id-reference.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-id-reference.dto.ts index d4ff6876..c2a8d62c 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-id-reference.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-id-reference.dto.ts @@ -1,4 +1,4 @@ export class SigFoxApiIdReferenceDto { - id: string; - name?: string; + id: string; + name?: string; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-single-device-response.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-single-device-response.dto.ts index 4437ca4c..7781dd12 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-single-device-response.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-single-device-response.dto.ts @@ -1,86 +1,86 @@ export interface SigFoxApiSingleDeviceResponseDto { - id: string; - name: string; - satelliteCapable: boolean; - deviceType: DeviceType; - contract: Contract; - group: Group; - modemCertificate: ModemCertificate; - prototype: boolean; - productCertificate: ProductCertificate; - location: Location; - lastComputedLocation: LastComputedLocation; - pac: string; - sequenceNumber: number; - trashSequenceNumber: number; - lastCom: number; - lqi: number; - activationTime: number; - creationTime: number; - state: number; - comState: number; - token: Token; - unsubscriptionTime: number; - createdBy: string; - lastEditionTime: number; - lastEditedBy: string; - automaticRenewal: boolean; - automaticRenewalStatus: number; - activable: boolean; - actions: string[]; - resources: string[]; + id: string; + name: string; + satelliteCapable: boolean; + deviceType: DeviceType; + contract: Contract; + group: Group; + modemCertificate: ModemCertificate; + prototype: boolean; + productCertificate: ProductCertificate; + location: Location; + lastComputedLocation: LastComputedLocation; + pac: string; + sequenceNumber: number; + trashSequenceNumber: number; + lastCom: number; + lqi: number; + activationTime: number; + creationTime: number; + state: number; + comState: number; + token: Token; + unsubscriptionTime: number; + createdBy: string; + lastEditionTime: number; + lastEditedBy: string; + automaticRenewal: boolean; + automaticRenewalStatus: number; + activable: boolean; + actions: string[]; + resources: string[]; } export interface DeviceType { - id: string; - name: string; - actions: string[]; - resources: string[]; + id: string; + name: string; + actions: string[]; + resources: string[]; } export interface Contract { - id: string; - name: string; - actions: string[]; - resources: string[]; + id: string; + name: string; + actions: string[]; + resources: string[]; } export interface Group { - id: string; - name: string; - type: number; - level: number; - actions: string[]; + id: string; + name: string; + type: number; + level: number; + actions: string[]; } export interface ModemCertificate { - id: string; - key: string; + id: string; + key: string; } export interface ProductCertificate { - id: string; - key: string; + id: string; + key: string; } export interface Location { - lat: number; - lng: number; + lat: number; + lng: number; } export interface LastComputedLocation { - lat: number; - lng: number; - radius: number; - sourceCode: number; - placeIds: string[]; + lat: number; + lng: number; + radius: number; + sourceCode: number; + placeIds: string[]; } export interface Token { - state: number; - detailMessage: string; - end: number; - unsubscriptionTime: number; - freeMessages: number; - freeMessagesSent: number; + state: number; + detailMessage: string; + end: number; + unsubscriptionTime: number; + freeMessages: number; + freeMessagesSent: number; } diff --git a/src/entities/dto/sigfox/external/sigfox-api-users-response.dto.ts b/src/entities/dto/sigfox/external/sigfox-api-users-response.dto.ts index c51aa653..0081d64b 100644 --- a/src/entities/dto/sigfox/external/sigfox-api-users-response.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-api-users-response.dto.ts @@ -1,15 +1,15 @@ import { SigFoxApiIdReferenceDto } from "./sigfox-api-id-reference.dto"; export interface SigFoxApiUsersResponseDto { - data: SigFoxApiUsersContent[]; + data: SigFoxApiUsersContent[]; } export interface SigFoxApiUsersContent { - id: string; - group: SigFoxApiIdReferenceDto; - name: string; - timezone: string; - creationTime: any; - profiles: SigFoxApiIdReferenceDto[]; - accessToken: string; + id: string; + group: SigFoxApiIdReferenceDto; + name: string; + timezone: string; + creationTime: any; + profiles: SigFoxApiIdReferenceDto[]; + accessToken: string; } diff --git a/src/entities/dto/sigfox/external/sigfox-callback.dto.ts b/src/entities/dto/sigfox/external/sigfox-callback.dto.ts index a17a7226..ed91ace9 100644 --- a/src/entities/dto/sigfox/external/sigfox-callback.dto.ts +++ b/src/entities/dto/sigfox/external/sigfox-callback.dto.ts @@ -1,7 +1,7 @@ export class SigFoxDownlinkCallbackDto { - [key: string]: SigFoxDownlinkDataDto; + [key: string]: SigFoxDownlinkDataDto; } export class SigFoxDownlinkDataDto { - downlinkData: string; + downlinkData: string; } diff --git a/src/entities/dto/sigfox/external/update-sigfox-api-device-request.dto.ts b/src/entities/dto/sigfox/external/update-sigfox-api-device-request.dto.ts index f67cf6e2..0b111473 100644 --- a/src/entities/dto/sigfox/external/update-sigfox-api-device-request.dto.ts +++ b/src/entities/dto/sigfox/external/update-sigfox-api-device-request.dto.ts @@ -1,7 +1,8 @@ import { OmitType } from "@nestjs/swagger"; import { CreateSigFoxApiDeviceRequestDto } from "./create-sigfox-api-device-request.dto"; -export class UpdateSigFoxApiDeviceRequestDto extends OmitType( - CreateSigFoxApiDeviceRequestDto, - ["id", "pac", "deviceTypeId"] -) {} +export class UpdateSigFoxApiDeviceRequestDto extends OmitType(CreateSigFoxApiDeviceRequestDto, [ + "id", + "pac", + "deviceTypeId", +]) {} diff --git a/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts b/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts index 9060f24c..b10a0d40 100644 --- a/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts +++ b/src/entities/dto/sigfox/internal/create-sigfox-group-request.dto.ts @@ -2,15 +2,15 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNumber, IsString } from "class-validator"; export class CreateSigFoxGroupRequestDto { - @ApiProperty({ required: true }) - @IsNumber() - organizationId: number; + @ApiProperty({ required: true }) + @IsNumber() + organizationId: number; - @ApiProperty({ required: true }) - @IsString() - username: string; + @ApiProperty({ required: true }) + @IsString() + username: string; - @ApiProperty({ required: true }) - @IsString() - password: string; + @ApiProperty({ required: true }) + @IsString() + password: string; } diff --git a/src/entities/dto/sigfox/internal/list-all-sigfox-groups-response.dto.ts b/src/entities/dto/sigfox/internal/list-all-sigfox-groups-response.dto.ts index fbcc097c..50fcf32e 100644 --- a/src/entities/dto/sigfox/internal/list-all-sigfox-groups-response.dto.ts +++ b/src/entities/dto/sigfox/internal/list-all-sigfox-groups-response.dto.ts @@ -1,6 +1,4 @@ import { ListAllEntitiesResponseDto } from "@dto/list-all-entities-response.dto"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; -export class ListAllSigFoxGroupResponseDto extends ListAllEntitiesResponseDto< - SigFoxGroup -> {} +export class ListAllSigFoxGroupResponseDto extends ListAllEntitiesResponseDto {} diff --git a/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts b/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts index 7bd0dbe8..f760a789 100644 --- a/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts +++ b/src/entities/dto/sigfox/internal/sigfox-get-all-request.dto.ts @@ -1,6 +1,6 @@ import { StringToNumber } from "@helpers/string-to-number-validator"; export class SigFoxGetAllRequestDto { - @StringToNumber() - organizationId: number; + @StringToNumber() + organizationId: number; } diff --git a/src/entities/dto/sigfox/internal/sigfox-test-response.dto.ts b/src/entities/dto/sigfox/internal/sigfox-test-response.dto.ts index dc371f3b..1f0fcb17 100644 --- a/src/entities/dto/sigfox/internal/sigfox-test-response.dto.ts +++ b/src/entities/dto/sigfox/internal/sigfox-test-response.dto.ts @@ -1,3 +1,3 @@ export class SigFoxTestResponse { - status: boolean; + status: boolean; } diff --git a/src/entities/dto/sigfox/internal/update-sigfox-group-request.dto.ts b/src/entities/dto/sigfox/internal/update-sigfox-group-request.dto.ts index e5940d15..3ed5a142 100644 --- a/src/entities/dto/sigfox/internal/update-sigfox-group-request.dto.ts +++ b/src/entities/dto/sigfox/internal/update-sigfox-group-request.dto.ts @@ -2,6 +2,4 @@ import { OmitType } from "@nestjs/swagger"; import { CreateSigFoxGroupRequestDto } from "./create-sigfox-group-request.dto"; -export class UpdateSigFoxGroupRequestDto extends OmitType(CreateSigFoxGroupRequestDto, [ - "organizationId", -]) {} +export class UpdateSigFoxGroupRequestDto extends OmitType(CreateSigFoxGroupRequestDto, ["organizationId"]) {} diff --git a/src/entities/dto/sigfox/sigfox-callback.dto.ts b/src/entities/dto/sigfox/sigfox-callback.dto.ts index 23188ff0..fa0a5d71 100644 --- a/src/entities/dto/sigfox/sigfox-callback.dto.ts +++ b/src/entities/dto/sigfox/sigfox-callback.dto.ts @@ -5,33 +5,33 @@ import { IsNumber, IsOptional, IsString } from "class-validator"; * Docs: https://support.sigfox.com/docs/uplink */ export class SigFoxCallbackDto { - @IsNumber() - time: number; - @IsString() - deviceTypeId: string; - @IsString() - deviceId: string; - @IsString() - data: string; - @IsNumber() - seqNumber: number; - // If true, then the device expects a downlink - ack: boolean; + @IsNumber() + time: number; + @IsString() + deviceTypeId: string; + @IsString() + deviceId: string; + @IsString() + data: string; + @IsNumber() + seqNumber: number; + // If true, then the device expects a downlink + ack: boolean; - // Only included in BIDIR - @IsOptional() - longPolling?: boolean; + // Only included in BIDIR + @IsOptional() + longPolling?: boolean; - // these are not available for all contracts "Condition: for devices with contract option NETWORK METADATA" - // https://support.sigfox.com/docs/bidir - // We cannot assume they'll exists - @IsOptional() - @IsNumber() - snr?: number; - @IsOptional() - @IsNumber() - rssi?: number; - @IsOptional() - @IsString() - station?: string; + // these are not available for all contracts "Condition: for devices with contract option NETWORK METADATA" + // https://support.sigfox.com/docs/bidir + // We cannot assume they'll exists + @IsOptional() + @IsNumber() + snr?: number; + @IsOptional() + @IsNumber() + rssi?: number; + @IsOptional() + @IsString() + station?: string; } diff --git a/src/entities/dto/test-payload-decoder.dto.ts b/src/entities/dto/test-payload-decoder.dto.ts index 61e28ce3..331a13da 100644 --- a/src/entities/dto/test-payload-decoder.dto.ts +++ b/src/entities/dto/test-payload-decoder.dto.ts @@ -1,10 +1,10 @@ import { IsString } from "class-validator"; export class TestPayloadDecoderDto { - @IsString() - code: string; - @IsString() - iotDeviceJsonString: string; - @IsString() - rawPayloadJsonString: string; + @IsString() + code: string; + @IsString() + iotDeviceJsonString: string; + @IsString() + rawPayloadJsonString: string; } diff --git a/src/entities/dto/update-iot-device.dto.ts b/src/entities/dto/update-iot-device.dto.ts index 3b183e8a..41b1d371 100644 --- a/src/entities/dto/update-iot-device.dto.ts +++ b/src/entities/dto/update-iot-device.dto.ts @@ -3,7 +3,7 @@ import { StringToNumber } from "@helpers/string-to-number-validator"; import { Min } from "class-validator"; export class UpdateIoTDeviceDto extends CreateIoTDeviceDto { - @StringToNumber() - @Min(1) - id: number; + @StringToNumber() + @Min(1) + id: number; } diff --git a/src/entities/dto/update-multicast.dto.ts b/src/entities/dto/update-multicast.dto.ts index 2359b823..6e87ba74 100644 --- a/src/entities/dto/update-multicast.dto.ts +++ b/src/entities/dto/update-multicast.dto.ts @@ -3,6 +3,6 @@ import { CreateMulticastDto } from "./create-multicast.dto"; import { ApiProperty } from "@nestjs/swagger"; export class UpdateMulticastDto extends PartialType(CreateMulticastDto) { - @ApiProperty({ required: false }) - id: string; + @ApiProperty({ required: false }) + id: string; } diff --git a/src/entities/dto/user-management/add-user-to-permission.dto.ts b/src/entities/dto/user-management/add-user-to-permission.dto.ts index 0b084aa9..abe7d16b 100644 --- a/src/entities/dto/user-management/add-user-to-permission.dto.ts +++ b/src/entities/dto/user-management/add-user-to-permission.dto.ts @@ -2,17 +2,17 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNumber, IsArray, ArrayUnique, ArrayNotEmpty } from "class-validator"; export class PermissionRequestAcceptUser { - @ApiProperty({ required: true }) - @IsNumber() - organizationId: number; + @ApiProperty({ required: true }) + @IsNumber() + organizationId: number; - @ApiProperty({ required: true }) - @IsNumber() - userId: number; + @ApiProperty({ required: true }) + @IsNumber() + userId: number; - @ApiProperty({ required: true }) - @IsArray() - @ArrayNotEmpty() - @ArrayUnique() - permissionIds: number[]; + @ApiProperty({ required: true }) + @IsArray() + @ArrayNotEmpty() + @ArrayUnique() + permissionIds: number[]; } diff --git a/src/entities/dto/user-management/create-new-kombit-user.dto.ts b/src/entities/dto/user-management/create-new-kombit-user.dto.ts index d252077d..17dfff70 100644 --- a/src/entities/dto/user-management/create-new-kombit-user.dto.ts +++ b/src/entities/dto/user-management/create-new-kombit-user.dto.ts @@ -2,12 +2,12 @@ import { ApiProperty } from "@nestjs/swagger"; import { ArrayMinSize, IsEmail, IsNotEmpty } from "class-validator"; export class CreateNewKombitUserDto { - @ApiProperty({ required: true }) - @IsEmail() - @IsNotEmpty() - email: string; - - @ApiProperty({ required: true }) - @ArrayMinSize(1) - requestedOrganizationIds: number[]; + @ApiProperty({ required: true }) + @IsEmail() + @IsNotEmpty() + email: string; + + @ApiProperty({ required: true }) + @ArrayMinSize(1) + requestedOrganizationIds: number[]; } diff --git a/src/entities/dto/user-management/create-organization.dto.ts b/src/entities/dto/user-management/create-organization.dto.ts index c1dd3c47..51129918 100644 --- a/src/entities/dto/user-management/create-organization.dto.ts +++ b/src/entities/dto/user-management/create-organization.dto.ts @@ -1,6 +1,6 @@ import { IsString } from "class-validator"; export class CreateOrganizationDto { - @IsString() - name: string; + @IsString() + name: string; } diff --git a/src/entities/dto/user-management/create-permission.dto.ts b/src/entities/dto/user-management/create-permission.dto.ts index d0eaaa1c..544d4514 100644 --- a/src/entities/dto/user-management/create-permission.dto.ts +++ b/src/entities/dto/user-management/create-permission.dto.ts @@ -7,43 +7,43 @@ import { ArrayDistinct } from "@helpers/array-distinct.validator"; import { nameof } from "@helpers/type-helper"; export class CreatePermissionDto { - @ApiProperty({ - required: true, - enum: PermissionType, - }) - @IsArray() - @ArrayDistinct(nameof('type')) - @Type(() => PermissionTypeDto) - @ValidateNested({ each: true }) - levels: PermissionTypeDto[] + @ApiProperty({ + required: true, + enum: PermissionType, + }) + @IsArray() + @ArrayDistinct(nameof("type")) + @Type(() => PermissionTypeDto) + @ValidateNested({ each: true }) + levels: PermissionTypeDto[]; - @ApiProperty({ required: true }) - @IsString() - @Length(1, 1024) - name: string; + @ApiProperty({ required: true }) + @IsString() + @Length(1, 1024) + name: string; - @ApiProperty({ required: true }) - @IsNumber() - organizationId: number; + @ApiProperty({ required: true }) + @IsNumber() + organizationId: number; - @ApiProperty({ - required: true, - type: "array", - items: { - type: "number", - }, - }) - userIds: number[]; + @ApiProperty({ + required: true, + type: "array", + items: { + type: "number", + }, + }) + userIds: number[]; - @ApiProperty({ - required: true, - type: "array", - items: { - type: "number", - }, - }) - applicationIds?: number[]; + @ApiProperty({ + required: true, + type: "array", + items: { + type: "number", + }, + }) + applicationIds?: number[]; - @ApiProperty({ required: false }) - automaticallyAddNewApplications?: boolean; + @ApiProperty({ required: false }) + automaticallyAddNewApplications?: boolean; } diff --git a/src/entities/dto/user-management/create-user.dto.ts b/src/entities/dto/user-management/create-user.dto.ts index bf12a074..c78df51c 100644 --- a/src/entities/dto/user-management/create-user.dto.ts +++ b/src/entities/dto/user-management/create-user.dto.ts @@ -3,24 +3,24 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsEmail, IsString, Length } from "class-validator"; export class CreateUserDto { - @ApiProperty({ required: true }) - @IsString() - @Length(1, 50) - @IsNotBlank("name") - name: string; + @ApiProperty({ required: true }) + @IsString() + @Length(1, 50) + @IsNotBlank("name") + name: string; - @ApiProperty({ required: true }) - @IsEmail() - email: string; + @ApiProperty({ required: true }) + @IsEmail() + email: string; - @ApiProperty({ required: true }) - @Length(6, 50) - @IsString() - password: string; + @ApiProperty({ required: true }) + @Length(6, 50) + @IsString() + password: string; - @ApiProperty({ required: true }) - active: boolean; + @ApiProperty({ required: true }) + active: boolean; - @ApiProperty({ required: false }) - globalAdmin?: boolean; + @ApiProperty({ required: false }) + globalAdmin?: boolean; } diff --git a/src/entities/dto/user-management/permission-type.dto.ts b/src/entities/dto/user-management/permission-type.dto.ts index 9a48328e..8f2e6ab4 100644 --- a/src/entities/dto/user-management/permission-type.dto.ts +++ b/src/entities/dto/user-management/permission-type.dto.ts @@ -2,6 +2,6 @@ import { PermissionType } from "@enum/permission-type.enum"; import { IsEnum } from "class-validator"; export class PermissionTypeDto { - @IsEnum(PermissionType) - type: PermissionType; + @IsEnum(PermissionType) + type: PermissionType; } diff --git a/src/entities/dto/user-management/reject-user.dto.ts b/src/entities/dto/user-management/reject-user.dto.ts index 97d07ff6..8ddbeb68 100644 --- a/src/entities/dto/user-management/reject-user.dto.ts +++ b/src/entities/dto/user-management/reject-user.dto.ts @@ -2,11 +2,11 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNumber } from "class-validator"; export class RejectUserDto { - @ApiProperty({ required: true }) - @IsNumber() - orgId: number; + @ApiProperty({ required: true }) + @IsNumber() + orgId: number; - @ApiProperty({ required: true }) - @IsNumber() - userIdToReject: number; + @ApiProperty({ required: true }) + @IsNumber() + userIdToReject: number; } diff --git a/src/entities/dto/user-management/update-permission.dto.ts b/src/entities/dto/user-management/update-permission.dto.ts index 0d072fb7..73fa14e9 100644 --- a/src/entities/dto/user-management/update-permission.dto.ts +++ b/src/entities/dto/user-management/update-permission.dto.ts @@ -2,29 +2,29 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsString, Length } from "class-validator"; export class UpdatePermissionDto { - @ApiProperty({ required: true }) - @IsString() - @Length(1, 1024) - name: string; + @ApiProperty({ required: true }) + @IsString() + @Length(1, 1024) + name: string; - @ApiProperty({ - required: true, - type: "array", - items: { - type: "number", - }, - }) - userIds: number[]; + @ApiProperty({ + required: true, + type: "array", + items: { + type: "number", + }, + }) + userIds: number[]; - @ApiProperty({ - required: true, - type: "array", - items: { - type: "number", - }, - }) - applicationIds?: number[]; + @ApiProperty({ + required: true, + type: "array", + items: { + type: "number", + }, + }) + applicationIds?: number[]; - @ApiProperty({ required: false }) - automaticallyAddNewApplications?: boolean; + @ApiProperty({ required: false }) + automaticallyAddNewApplications?: boolean; } diff --git a/src/entities/dto/user-management/update-user-orgs.dto.ts b/src/entities/dto/user-management/update-user-orgs.dto.ts index c7c297fa..cd51b2f4 100644 --- a/src/entities/dto/user-management/update-user-orgs.dto.ts +++ b/src/entities/dto/user-management/update-user-orgs.dto.ts @@ -3,7 +3,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { ArrayMinSize } from "class-validator"; export class UpdateUserOrgsDto { - @ApiProperty({ required: true }) - @ArrayMinSize(1) - requestedOrganizationIds: number[]; + @ApiProperty({ required: true }) + @ArrayMinSize(1) + requestedOrganizationIds: number[]; } diff --git a/src/entities/dto/user-management/update-user.dto.ts b/src/entities/dto/user-management/update-user.dto.ts index 0d0aec40..df7acde1 100644 --- a/src/entities/dto/user-management/update-user.dto.ts +++ b/src/entities/dto/user-management/update-user.dto.ts @@ -4,11 +4,11 @@ import { IsEmail, IsOptional } from "class-validator"; import { CreateUserDto } from "./create-user.dto"; export class UpdateUserDto extends OmitType(CreateUserDto, ["email", "password"]) { - @ApiPropertyOptional() - @IsOptional() - @IsEmail() - email?: string; + @ApiPropertyOptional() + @IsOptional() + @IsEmail() + email?: string; - @ApiPropertyOptional() - password?: string; + @ApiPropertyOptional() + password?: string; } diff --git a/src/entities/dto/user-management/xml-object.ts b/src/entities/dto/user-management/xml-object.ts index 0146332f..b309dfdb 100644 --- a/src/entities/dto/user-management/xml-object.ts +++ b/src/entities/dto/user-management/xml-object.ts @@ -1,7 +1,7 @@ export type XMLValue = string | number | boolean | null | XMLObject | XMLValue[]; export type XMLObject = { - [key: string]: XMLValue; + [key: string]: XMLValue; }; export type XMLOutput = Record; diff --git a/src/entities/enum/application-status.enum.ts b/src/entities/enum/application-status.enum.ts index 4c06050e..33e49bf0 100644 --- a/src/entities/enum/application-status.enum.ts +++ b/src/entities/enum/application-status.enum.ts @@ -1,7 +1,7 @@ export enum ApplicationStatus { - "NONE" = "NONE", - "IN-OPERATION" = "IN-OPERATION", - "PROJECT" = "PROJECT", - "PROTOTYPE" = "PROTOTYPE", - "OTHER" = "OTHER", + "NONE" = "NONE", + "IN-OPERATION" = "IN-OPERATION", + "PROJECT" = "PROJECT", + "PROTOTYPE" = "PROTOTYPE", + "OTHER" = "OTHER", } diff --git a/src/entities/enum/authentication-type.enum.ts b/src/entities/enum/authentication-type.enum.ts index f502555f..1226361c 100644 --- a/src/entities/enum/authentication-type.enum.ts +++ b/src/entities/enum/authentication-type.enum.ts @@ -1,4 +1,4 @@ export enum AuthenticationType { - PASSWORD = 'PASSWORD', - CERTIFICATE = 'CERTIFICATE' + PASSWORD = "PASSWORD", + CERTIFICATE = "CERTIFICATE", } diff --git a/src/entities/enum/authorization-type.enum.ts b/src/entities/enum/authorization-type.enum.ts index b2eb0e76..e40b5c74 100644 --- a/src/entities/enum/authorization-type.enum.ts +++ b/src/entities/enum/authorization-type.enum.ts @@ -1,6 +1,6 @@ export enum AuthorizationType { - NO_AUTHORIZATION = "NO_AUTHORIZATION", - HTTP_BASIC_AUTHORIZATION = "HTTP_BASIC_AUTHORIZATION", - HEADER_BASED_AUTHORIZATION = "HEADER_BASED_AUTHORIZATION", - OAUTH_AUTHORIZATION = "OAUTH_AUTHORIZATION" + NO_AUTHORIZATION = "NO_AUTHORIZATION", + HTTP_BASIC_AUTHORIZATION = "HTTP_BASIC_AUTHORIZATION", + HEADER_BASED_AUTHORIZATION = "HEADER_BASED_AUTHORIZATION", + OAUTH_AUTHORIZATION = "OAUTH_AUTHORIZATION", } diff --git a/src/entities/enum/controlled-property.enum.ts b/src/entities/enum/controlled-property.enum.ts index d74074c0..ff064246 100644 --- a/src/entities/enum/controlled-property.enum.ts +++ b/src/entities/enum/controlled-property.enum.ts @@ -1,43 +1,43 @@ export enum ControlledPropertyTypes { - AIRPOLLUTION = "airPollution", - ATMOSPHERICPRESSURE = "atmosphericPressure", - CDOM = "cdom", - CONDUCTANCE = "conductance", - CONDUCTIVITY = "conductivity", - DEPTH = "depth", - EATINGACTIVITY = "eatingActivity", - ELECTRICITYCONSUMPTION = "electricityConsumption", - ENERGY = "energy", - FILLINGLEVEL = "fillingLevel", - GASCOMSUMPTION = "gasComsumption", - HEADING = "heading", - HUMIDITY = "humidity", - LIGHT = "light", - LOCATION = "location", - MILKING = "milking", - MOTION = "motion", - MOVEMENTACTIVITY = "movementActivity", - NOISELEVEL = "noiseLevel", - OCCUPANCY = "occupancy", - ORP = "orp", - PH = "pH", - POWER = "power", - PRECIPITATION = "precipitation", - PRESSURE = "pressure", - SALINITY = "salinity", - SMOKE = "smoke", - SOILMOISTURE = "soilMoisture", - SOLARRADIATION = "solarRadiation", - SPEED = "speed", - TDS = "tds", - TEMPERATURE = "temperature", - TRAFFICFLOW = "trafficFlow", - TSS = "tss", - TURBIDITY = "turbidity", - WATERCONSUMPTION = "waterConsumption", - WATERPOLLUTION = "waterPollution", - WEATHERCONDITIONS = "weatherConditions", - WEIGHT = "weight", - WINDDIRECTION = "windDirection", - WINDSPEED = "windSpeed", + AIRPOLLUTION = "airPollution", + ATMOSPHERICPRESSURE = "atmosphericPressure", + CDOM = "cdom", + CONDUCTANCE = "conductance", + CONDUCTIVITY = "conductivity", + DEPTH = "depth", + EATINGACTIVITY = "eatingActivity", + ELECTRICITYCONSUMPTION = "electricityConsumption", + ENERGY = "energy", + FILLINGLEVEL = "fillingLevel", + GASCOMSUMPTION = "gasComsumption", + HEADING = "heading", + HUMIDITY = "humidity", + LIGHT = "light", + LOCATION = "location", + MILKING = "milking", + MOTION = "motion", + MOVEMENTACTIVITY = "movementActivity", + NOISELEVEL = "noiseLevel", + OCCUPANCY = "occupancy", + ORP = "orp", + PH = "pH", + POWER = "power", + PRECIPITATION = "precipitation", + PRESSURE = "pressure", + SALINITY = "salinity", + SMOKE = "smoke", + SOILMOISTURE = "soilMoisture", + SOLARRADIATION = "solarRadiation", + SPEED = "speed", + TDS = "tds", + TEMPERATURE = "temperature", + TRAFFICFLOW = "trafficFlow", + TSS = "tss", + TURBIDITY = "turbidity", + WATERCONSUMPTION = "waterConsumption", + WATERPOLLUTION = "waterPollution", + WEATHERCONDITIONS = "weatherConditions", + WEIGHT = "weight", + WINDDIRECTION = "windDirection", + WINDSPEED = "windSpeed", } diff --git a/src/entities/enum/data-target-type-mapping.ts b/src/entities/enum/data-target-type-mapping.ts index d79f9ea7..1bf29b0b 100644 --- a/src/entities/enum/data-target-type-mapping.ts +++ b/src/entities/enum/data-target-type-mapping.ts @@ -5,8 +5,8 @@ import { OpenDataDkDataTarget } from "@entities/open-data-dk-push-data-target.en import { DataTargetType } from "@enum/data-target-type.enum"; export const dataTargetTypeMap = { - [DataTargetType.HttpPush]: HttpPushDataTarget, - [DataTargetType.Fiware]: FiwareDataTarget, - [DataTargetType.MQTT]: MqttDataTarget, - [DataTargetType.OpenDataDK]: OpenDataDkDataTarget, + [DataTargetType.HttpPush]: HttpPushDataTarget, + [DataTargetType.Fiware]: FiwareDataTarget, + [DataTargetType.MQTT]: MqttDataTarget, + [DataTargetType.OpenDataDK]: OpenDataDkDataTarget, }; diff --git a/src/entities/enum/data-target-type.enum.ts b/src/entities/enum/data-target-type.enum.ts index 43d78712..45b8dd34 100644 --- a/src/entities/enum/data-target-type.enum.ts +++ b/src/entities/enum/data-target-type.enum.ts @@ -1,6 +1,6 @@ export enum DataTargetType { - HttpPush = "HTTP_PUSH", - Fiware = "FIWARE", - MQTT = "MQTT", - OpenDataDK = "OPENDATADK", + HttpPush = "HTTP_PUSH", + Fiware = "FIWARE", + MQTT = "MQTT", + OpenDataDK = "OPENDATADK", } diff --git a/src/entities/enum/device-type-mapping.ts b/src/entities/enum/device-type-mapping.ts index 64c403d8..7ad95d90 100644 --- a/src/entities/enum/device-type-mapping.ts +++ b/src/entities/enum/device-type-mapping.ts @@ -6,9 +6,9 @@ import { MQTTInternalBrokerDevice } from "@entities/mqtt-internal-broker-device. import { MQTTExternalBrokerDevice } from "@entities/mqtt-external-broker-device.entity"; export const iotDeviceTypeMap = { - [IoTDeviceType.GenericHttp]: GenericHTTPDevice, - [IoTDeviceType.LoRaWAN]: LoRaWANDevice, - [IoTDeviceType.SigFox]: SigFoxDevice, - [IoTDeviceType.MQTTInternalBroker]: MQTTInternalBrokerDevice, - [IoTDeviceType.MQTTExternalBroker]: MQTTExternalBrokerDevice, + [IoTDeviceType.GenericHttp]: GenericHTTPDevice, + [IoTDeviceType.LoRaWAN]: LoRaWANDevice, + [IoTDeviceType.SigFox]: SigFoxDevice, + [IoTDeviceType.MQTTInternalBroker]: MQTTInternalBrokerDevice, + [IoTDeviceType.MQTTExternalBroker]: MQTTExternalBrokerDevice, }; diff --git a/src/entities/enum/device-type.enum.ts b/src/entities/enum/device-type.enum.ts index 1848f943..e1c326b4 100644 --- a/src/entities/enum/device-type.enum.ts +++ b/src/entities/enum/device-type.enum.ts @@ -1,13 +1,13 @@ export enum IoTDeviceType { - GenericHttp = "GENERIC_HTTP", - LoRaWAN = "LORAWAN", - MQTTInternalBroker = "MQTT_INTERNAL_BROKER", - MQTTExternalBroker = "MQTT_EXTERNAL_BROKER", - SigFox = "SIGFOX", + GenericHttp = "GENERIC_HTTP", + LoRaWAN = "LORAWAN", + MQTTInternalBroker = "MQTT_INTERNAL_BROKER", + MQTTExternalBroker = "MQTT_EXTERNAL_BROKER", + SigFox = "SIGFOX", } enum ApplicationDeviceTypeExtra { - Other = "OTHER", + Other = "OTHER", } export type ApplicationDeviceTypeUnion = IoTDeviceType | ApplicationDeviceTypeExtra; diff --git a/src/entities/enum/error-codes.enum.ts b/src/entities/enum/error-codes.enum.ts index 89ddd791..c94075cb 100644 --- a/src/entities/enum/error-codes.enum.ts +++ b/src/entities/enum/error-codes.enum.ts @@ -1,53 +1,53 @@ export enum ErrorCodes { - EmailAlreadyExists = "MESSAGE.USER-ALREADY-HAVE-MAIL", - EmailAlreadyInUse = "MESSAGE.EMAIL-ALREADY-IN-USE", - IdDoesNotExists = "MESSAGE.ID-DOES-NOT-EXIST", - IdMissing = "MESSAGE.ID-MISSING-FROM-REQUEST", - NameInvalidOrAlreadyInUse = "MESSAGE.NAME-INVALID-OR-ALREADY-IN-USE", - IdInvalidOrAlreadyInUse = "MESSAGE.ID-INVALID-OR-ALREADY-IN-USE", - InvalidApiKey = "MESSAGE.DEVICE-DOES-NOT-EXIST", - InvalidPost = "MESSAGE.POST-FAILED", - WrongLength = "MESSAGE.WRONG-LENGTH", - NotValidFormat = "MESSAGE.NOT-VALID-FORMAT", - BadEncoding = "MESSAGE.BAD-ENCODING", - MissingOTAAInfo = "MESSAGE.OTAA-INFO-MISSING", - MissingABPInfo = "MESSAGE.ABP-INFO-MISSING", - UserAlreadyExists = "MESSAGE.USER-ALREADY-EXISTS", - OrganizationAlreadyExists = "MESSAGE.ORGANIZATION-ALREADY-EXISTS", - OrganizationDoesNotExists = "MESSAGE.ORGANIZATION-DOES-NOT-EXISTS", - DeviceModelOrganizationDoesNotMatch = "MESSAGE.DEVICE-MODEL-ORGANIZATION-DOES-NOT-MATCH", - UserInactive = "MESSAGE.USER-INACTIVE", - NotSameApplication = "MESSAGE.NOT-SAME-APPLICATION", - PasswordNotMetRequirements = "MESSAGE.PASSWORD-DOES-NOT-MEET-REQUIREMENTS", - SigFoxBadLogin = "MESSAGE.SIGFOX-BAD-LOGIN", - GatewayIdNotAllowedInUpdate = "MESSAGE.GATEWAY-ID-NOT-ALLOWED-IN-UPDATE", - GroupCanOnlyBeCreatedOncePrOrganization = "MESSAGE.GROUP-CAN-ONLY-BE-CREATED-ONCE-PR-ORGANIZATION", - DeviceDoesNotExistInSigFoxForGroup = "MESSAGE.DEVICE-DOES-NOT-EXIST-IN-SIGFOX-FOR-GROUP", - DownlinkNotSupportedForDeviceType = "MESSAGE.DOWNLINK-NOT-SUPPORTED-FOR-DEVICE-TYPE", - DownlinkLengthWrongForSigfox = "MESSAGE.DOWNLINK-LENGTH-WRONG-FOR-SIGFOX", - OnlyAllowedForLoRaWANAndSigfox = "MESSAGE.ONLY-ALLOWED-FOR-LORAWAN-AND-SIGFOX", - DeviceIsNotActivatedInChirpstack = "MESSAGE.DEVICE-IS-NOT-ACTIVATED-IN-CHIRPSTACK", - QueryMustNotBeEmpty = "MESSAGE.QUERY-MUST-NOT-BE-EMPTY", - IsUsed = "MESSAGE.IS-USED", - CannotModifyOnKombitUser = "MESSAGE.CANNOT-MODIFY-ON-KOMBIT-USER", - SigfoxError = "MESSAGE.SIGFOX-ERROR", - NoData = "NoData", - MissingRole = "MissingRole", - DeleteNotAllowedItemIsInUse = "MESSAGE.DELETE-NOT-ALLOWED-ITEM-IS-IN-USE", - DeleteNotAllowedHasSigfoxDevice = "MESSAGE.DELETE-NOT-ALLOWED-HAS-SIGFOX-DEVICE", - DeleteNotAllowedHasLoRaWANDevices = "MESSAGE.DELETE-NOT-ALLOWED-HAS-LORAWAN-DEVICE", - KOMBITLoginFailed = "MESSAGE.KOMBIT-LOGIN-FAILED", - ApiKeyAuthFailed = "MESSAGE.API-KEY-AUTH-FAILED", - TooMuchData = "MESSAGE.TOO-MUCH-DATA", - ApplicationDoesNotExist = "MESSAGE.APPLICATION-DOES-NOT-EXIST", - FailedToCreateOrUpdateIotDevice = "MESSAGE.FAILED-TO-CREATE-OR-UPDATE-IOT-DEVICE", - DeviceModelDoesNotExist = "MESSAGE.DEVICE-MODEL-DOES-NOT-EXIST", - InvalidKeyInKeyValuePair = "MESSAGE.INVALID-KEY-IN-KEY-VALUE-PAIR", - InvalidValueInKeyValuePair = "MESSAGE.INVALID-VALUE-IN-KEY-VALUE-PAIR", - DuplicatePermissionTypes = "MESSAGE.DUPLICATE-PERMISSION-TYPES", - SendMailError = "MESSAGE.SEND-MAIL-ERROR", - UserDoesNotExistInArray = "MESSAGE.USER-DOES-NOT-EXIST", - UserAlreadyInPermission = "MESSAGE.USER-ALREADY-IN-PERMISSION", - CouldntGetApplications = "MESSAGE.COULD-NOT-GET-CS-APPLICATIONS", - DifferentServiceProfile = "MESSAGE.DIFFERENT-CREATION-SERVICE-PROFILE", + EmailAlreadyExists = "MESSAGE.USER-ALREADY-HAVE-MAIL", + EmailAlreadyInUse = "MESSAGE.EMAIL-ALREADY-IN-USE", + IdDoesNotExists = "MESSAGE.ID-DOES-NOT-EXIST", + IdMissing = "MESSAGE.ID-MISSING-FROM-REQUEST", + NameInvalidOrAlreadyInUse = "MESSAGE.NAME-INVALID-OR-ALREADY-IN-USE", + IdInvalidOrAlreadyInUse = "MESSAGE.ID-INVALID-OR-ALREADY-IN-USE", + InvalidApiKey = "MESSAGE.DEVICE-DOES-NOT-EXIST", + InvalidPost = "MESSAGE.POST-FAILED", + WrongLength = "MESSAGE.WRONG-LENGTH", + NotValidFormat = "MESSAGE.NOT-VALID-FORMAT", + BadEncoding = "MESSAGE.BAD-ENCODING", + MissingOTAAInfo = "MESSAGE.OTAA-INFO-MISSING", + MissingABPInfo = "MESSAGE.ABP-INFO-MISSING", + UserAlreadyExists = "MESSAGE.USER-ALREADY-EXISTS", + OrganizationAlreadyExists = "MESSAGE.ORGANIZATION-ALREADY-EXISTS", + OrganizationDoesNotExists = "MESSAGE.ORGANIZATION-DOES-NOT-EXISTS", + DeviceModelOrganizationDoesNotMatch = "MESSAGE.DEVICE-MODEL-ORGANIZATION-DOES-NOT-MATCH", + UserInactive = "MESSAGE.USER-INACTIVE", + NotSameApplication = "MESSAGE.NOT-SAME-APPLICATION", + PasswordNotMetRequirements = "MESSAGE.PASSWORD-DOES-NOT-MEET-REQUIREMENTS", + SigFoxBadLogin = "MESSAGE.SIGFOX-BAD-LOGIN", + GatewayIdNotAllowedInUpdate = "MESSAGE.GATEWAY-ID-NOT-ALLOWED-IN-UPDATE", + GroupCanOnlyBeCreatedOncePrOrganization = "MESSAGE.GROUP-CAN-ONLY-BE-CREATED-ONCE-PR-ORGANIZATION", + DeviceDoesNotExistInSigFoxForGroup = "MESSAGE.DEVICE-DOES-NOT-EXIST-IN-SIGFOX-FOR-GROUP", + DownlinkNotSupportedForDeviceType = "MESSAGE.DOWNLINK-NOT-SUPPORTED-FOR-DEVICE-TYPE", + DownlinkLengthWrongForSigfox = "MESSAGE.DOWNLINK-LENGTH-WRONG-FOR-SIGFOX", + OnlyAllowedForLoRaWANAndSigfox = "MESSAGE.ONLY-ALLOWED-FOR-LORAWAN-AND-SIGFOX", + DeviceIsNotActivatedInChirpstack = "MESSAGE.DEVICE-IS-NOT-ACTIVATED-IN-CHIRPSTACK", + QueryMustNotBeEmpty = "MESSAGE.QUERY-MUST-NOT-BE-EMPTY", + IsUsed = "MESSAGE.IS-USED", + CannotModifyOnKombitUser = "MESSAGE.CANNOT-MODIFY-ON-KOMBIT-USER", + SigfoxError = "MESSAGE.SIGFOX-ERROR", + NoData = "NoData", + MissingRole = "MissingRole", + DeleteNotAllowedItemIsInUse = "MESSAGE.DELETE-NOT-ALLOWED-ITEM-IS-IN-USE", + DeleteNotAllowedHasSigfoxDevice = "MESSAGE.DELETE-NOT-ALLOWED-HAS-SIGFOX-DEVICE", + DeleteNotAllowedHasLoRaWANDevices = "MESSAGE.DELETE-NOT-ALLOWED-HAS-LORAWAN-DEVICE", + KOMBITLoginFailed = "MESSAGE.KOMBIT-LOGIN-FAILED", + ApiKeyAuthFailed = "MESSAGE.API-KEY-AUTH-FAILED", + TooMuchData = "MESSAGE.TOO-MUCH-DATA", + ApplicationDoesNotExist = "MESSAGE.APPLICATION-DOES-NOT-EXIST", + FailedToCreateOrUpdateIotDevice = "MESSAGE.FAILED-TO-CREATE-OR-UPDATE-IOT-DEVICE", + DeviceModelDoesNotExist = "MESSAGE.DEVICE-MODEL-DOES-NOT-EXIST", + InvalidKeyInKeyValuePair = "MESSAGE.INVALID-KEY-IN-KEY-VALUE-PAIR", + InvalidValueInKeyValuePair = "MESSAGE.INVALID-VALUE-IN-KEY-VALUE-PAIR", + DuplicatePermissionTypes = "MESSAGE.DUPLICATE-PERMISSION-TYPES", + SendMailError = "MESSAGE.SEND-MAIL-ERROR", + UserDoesNotExistInArray = "MESSAGE.USER-DOES-NOT-EXIST", + UserAlreadyInPermission = "MESSAGE.USER-ALREADY-IN-PERMISSION", + CouldntGetApplications = "MESSAGE.COULD-NOT-GET-CS-APPLICATIONS", + DifferentServiceProfile = "MESSAGE.DIFFERENT-CREATION-SERVICE-PROFILE", } diff --git a/src/entities/enum/gateway-status-interval.enum.ts b/src/entities/enum/gateway-status-interval.enum.ts index d9fc604d..8bd1787b 100644 --- a/src/entities/enum/gateway-status-interval.enum.ts +++ b/src/entities/enum/gateway-status-interval.enum.ts @@ -1,20 +1,20 @@ import { subtractDays } from "@helpers/date.helper"; export enum GatewayStatusInterval { - DAY = "DAY", - WEEK = "WEEK", - MONTH = "MONTH", + DAY = "DAY", + WEEK = "WEEK", + MONTH = "MONTH", } export const gatewayStatusIntervalToDate = (interval: GatewayStatusInterval): Date => { - const now = new Date(); + const now = new Date(); - switch (interval) { - case GatewayStatusInterval.WEEK: - return subtractDays(now, 7); - case GatewayStatusInterval.MONTH: - return subtractDays(now, 30); - default: - return subtractDays(now, 1); - } + switch (interval) { + case GatewayStatusInterval.WEEK: + return subtractDays(now, 7); + case GatewayStatusInterval.MONTH: + return subtractDays(now, 30); + default: + return subtractDays(now, 1); + } }; diff --git a/src/entities/enum/gateway.enum.ts b/src/entities/enum/gateway.enum.ts index 87068b90..f3df5db8 100644 --- a/src/entities/enum/gateway.enum.ts +++ b/src/entities/enum/gateway.enum.ts @@ -1,14 +1,14 @@ export enum GatewayPlacement { - "NONE" = "NONE", - Indoors = "INDOORS", - Outdoors = "OUTDOORS", - Other = "OTHER", + "NONE" = "NONE", + Indoors = "INDOORS", + Outdoors = "OUTDOORS", + Other = "OTHER", } export enum GatewayStatus { - "NONE" = "NONE", - "IN-OPERATION" = "IN-OPERATION", - "PROJECT" = "PROJECT", - "PROTOTYPE" = "PROTOTYPE", - "OTHER" = "OTHER", + "NONE" = "NONE", + "IN-OPERATION" = "IN-OPERATION", + "PROJECT" = "PROJECT", + "PROTOTYPE" = "PROTOTYPE", + "OTHER" = "OTHER", } diff --git a/src/entities/enum/kafka-topic-mappings.ts b/src/entities/enum/kafka-topic-mappings.ts index 2ccb7c82..73663209 100644 --- a/src/entities/enum/kafka-topic-mappings.ts +++ b/src/entities/enum/kafka-topic-mappings.ts @@ -2,5 +2,5 @@ import { TransformedPayloadDto } from "@dto/kafka/transformed-payload.dto"; import { KafkaTopic } from "@enum/kafka-topic.enum"; export const KafkaTopicTypeMap = { - [KafkaTopic.TRANSFORMED_REQUEST]: TransformedPayloadDto, + [KafkaTopic.TRANSFORMED_REQUEST]: TransformedPayloadDto, }; diff --git a/src/entities/enum/kafka-topic.enum.ts b/src/entities/enum/kafka-topic.enum.ts index af881201..4049463c 100644 --- a/src/entities/enum/kafka-topic.enum.ts +++ b/src/entities/enum/kafka-topic.enum.ts @@ -1,5 +1,5 @@ export enum KafkaTopic { - RAW_REQUEST = "request.raw", - TRANSFORMED_REQUEST = "request.transformed", - RAW_GATEWAY_STATE = "request.gateway.state" + RAW_REQUEST = "request.raw", + TRANSFORMED_REQUEST = "request.transformed", + RAW_GATEWAY_STATE = "request.gateway.state", } diff --git a/src/entities/enum/lorawan-activation-type.enum.ts b/src/entities/enum/lorawan-activation-type.enum.ts index 386c3cb7..7b062538 100644 --- a/src/entities/enum/lorawan-activation-type.enum.ts +++ b/src/entities/enum/lorawan-activation-type.enum.ts @@ -1,5 +1,5 @@ export enum ActivationType { - NONE = "NONE", - OTAA = "OTAA", - ABP = "ABP", + NONE = "NONE", + OTAA = "OTAA", + ABP = "ABP", } diff --git a/src/entities/enum/mqtt-permission-level.enum.ts b/src/entities/enum/mqtt-permission-level.enum.ts index bca312b0..ce64eb85 100644 --- a/src/entities/enum/mqtt-permission-level.enum.ts +++ b/src/entities/enum/mqtt-permission-level.enum.ts @@ -1,5 +1,5 @@ export enum MQTTPermissionLevel { - read = "read", - write = "write", - superUser = "superUser", + read = "read", + write = "write", + superUser = "superUser", } diff --git a/src/entities/enum/multicast-type.enum.ts b/src/entities/enum/multicast-type.enum.ts index 7141a31a..2868bf40 100644 --- a/src/entities/enum/multicast-type.enum.ts +++ b/src/entities/enum/multicast-type.enum.ts @@ -1,3 +1,3 @@ export enum multicastGroup { - ClassC = "CLASS_C", + ClassC = "CLASS_C", } diff --git a/src/entities/enum/permission-type.enum.ts b/src/entities/enum/permission-type.enum.ts index 16fa326d..fea26566 100644 --- a/src/entities/enum/permission-type.enum.ts +++ b/src/entities/enum/permission-type.enum.ts @@ -1,7 +1,7 @@ export enum PermissionType { - GlobalAdmin = "GlobalAdmin", - OrganizationUserAdmin = "OrganizationUserAdmin", - OrganizationGatewayAdmin = "OrganizationGatewayAdmin", - OrganizationApplicationAdmin = "OrganizationApplicationAdmin", - Read = "Read", + GlobalAdmin = "GlobalAdmin", + OrganizationUserAdmin = "OrganizationUserAdmin", + OrganizationGatewayAdmin = "OrganizationGatewayAdmin", + OrganizationApplicationAdmin = "OrganizationApplicationAdmin", + Read = "Read", } diff --git a/src/entities/enum/qos.enum.ts b/src/entities/enum/qos.enum.ts index 05e68b6e..f1215089 100644 --- a/src/entities/enum/qos.enum.ts +++ b/src/entities/enum/qos.enum.ts @@ -2,7 +2,7 @@ * @see https://www.hivemq.com/blog/mqtt-essentials-part-6-mqtt-quality-of-service-levels/ */ export enum QoS { - QoS_0 = 0, - QoS_1 = 1, - QoS_2 = 2, + QoS_0 = 0, + QoS_1 = 1, + QoS_2 = 2, } diff --git a/src/entities/enum/send-status.enum.ts b/src/entities/enum/send-status.enum.ts index 99727284..0a82e0fc 100644 --- a/src/entities/enum/send-status.enum.ts +++ b/src/entities/enum/send-status.enum.ts @@ -1,4 +1,4 @@ export enum SendStatus { - OK = "OK", - ERROR = "ERROR", + OK = "OK", + ERROR = "ERROR", } diff --git a/src/entities/enum/sigfox.enum.ts b/src/entities/enum/sigfox.enum.ts index de146cd4..06134538 100644 --- a/src/entities/enum/sigfox.enum.ts +++ b/src/entities/enum/sigfox.enum.ts @@ -1,15 +1,15 @@ export enum SigFoxDownlinkMode { - DIRECT = 0, - CALLBACK = 1, - NONE = 2, - MANAGED = 3, + DIRECT = 0, + CALLBACK = 1, + NONE = 2, + MANAGED = 3, } export enum SigFoxPayloadType { - RegularRawPayload = 2, - CustomGrammar = 3, - Geolocation = 4, - DisplayInASCII = 5, - RadioPlanningFrame = 6, - Sensitv2 = 9, + RegularRawPayload = 2, + CustomGrammar = 3, + Geolocation = 4, + DisplayInASCII = 5, + RadioPlanningFrame = 6, + Sensitv2 = 9, } diff --git a/src/entities/fiware-data-target.entity.ts b/src/entities/fiware-data-target.entity.ts index cb90d255..7009c2ca 100644 --- a/src/entities/fiware-data-target.entity.ts +++ b/src/entities/fiware-data-target.entity.ts @@ -8,51 +8,51 @@ import { FiwareDataTargetConfiguration } from "./interfaces/fiware-data-target-c @ChildEntity(DataTargetType.Fiware) export class FiwareDataTarget extends DataTarget { - @Column() - url: string; - - @Column({ default: 30000, comment: "HTTP call timeout in milliseconds" }) - timeout: number; - - @Column({ nullable: true }) - authorizationHeader?: string; - - @Column({ nullable: true }) - tenant: string; - - @Column({ nullable: true }) - context: string; - - @Column({ nullable: true }) - clientId?: string; - - @Column({ nullable: true, select: false }) - clientSecret?: string; - - @Column({ nullable: true }) - tokenEndpoint?: string; - - @BeforeInsert() - private beforeInsert() { - this.type = DataTargetType.Fiware; - } - - toConfiguration(): FiwareDataTargetConfiguration { - return { - url: this.url, - timeout: this.timeout, - authorizationType: this.tokenEndpoint - ? AuthorizationType.OAUTH_AUTHORIZATION - : this.authorizationHeader - ? AuthorizationType.HEADER_BASED_AUTHORIZATION - : AuthorizationType.NO_AUTHORIZATION, - authorizationHeader: this.authorizationHeader, - tenant: this.tenant, - context: this.context, - clientId: this.clientId, - clientSecret: this.clientSecret, - tokenEndpoint: this.tokenEndpoint, - updatedAt: this.updatedAt, - }; - } + @Column() + url: string; + + @Column({ default: 30000, comment: "HTTP call timeout in milliseconds" }) + timeout: number; + + @Column({ nullable: true }) + authorizationHeader?: string; + + @Column({ nullable: true }) + tenant: string; + + @Column({ nullable: true }) + context: string; + + @Column({ nullable: true }) + clientId?: string; + + @Column({ nullable: true, select: false }) + clientSecret?: string; + + @Column({ nullable: true }) + tokenEndpoint?: string; + + @BeforeInsert() + private beforeInsert() { + this.type = DataTargetType.Fiware; + } + + toConfiguration(): FiwareDataTargetConfiguration { + return { + url: this.url, + timeout: this.timeout, + authorizationType: this.tokenEndpoint + ? AuthorizationType.OAUTH_AUTHORIZATION + : this.authorizationHeader + ? AuthorizationType.HEADER_BASED_AUTHORIZATION + : AuthorizationType.NO_AUTHORIZATION, + authorizationHeader: this.authorizationHeader, + tenant: this.tenant, + context: this.context, + clientId: this.clientId, + clientSecret: this.clientSecret, + tokenEndpoint: this.tokenEndpoint, + updatedAt: this.updatedAt, + }; + } } diff --git a/src/entities/gateway-status-history.entity.ts b/src/entities/gateway-status-history.entity.ts index 622c6120..38bde2b5 100644 --- a/src/entities/gateway-status-history.entity.ts +++ b/src/entities/gateway-status-history.entity.ts @@ -5,12 +5,12 @@ import { DbBaseEntity } from "./base.entity"; @Entity("gateway_status_history") @Unique([nameof("mac"), nameof("timestamp")]) export class GatewayStatusHistory extends DbBaseEntity { - @Column() - mac: string; + @Column() + mac: string; - @Column() - wasOnline: boolean; + @Column() + wasOnline: boolean; - @CreateDateColumn() - timestamp: Date; + @CreateDateColumn() + timestamp: Date; } diff --git a/src/entities/gateway.entity.ts b/src/entities/gateway.entity.ts index de7d7173..349fb970 100644 --- a/src/entities/gateway.entity.ts +++ b/src/entities/gateway.entity.ts @@ -7,66 +7,66 @@ import { GatewayPlacement, GatewayStatus } from "@enum/gateway.enum"; @Entity("gateway") export class Gateway extends DbBaseEntity { - @Column() - name: string; + @Column() + name: string; - @Column({ nullable: true }) - description?: string; + @Column({ nullable: true }) + description?: string; - @Column() - @Length(16, 16, { message: "Must be 16 characters" }) - gatewayId: string; + @Column() + @Length(16, 16, { message: "Must be 16 characters" }) + gatewayId: string; - @ManyToOne(_ => Organization, organization => organization.gateways, { onDelete: "CASCADE" }) - organization: Organization; + @ManyToOne(_ => Organization, organization => organization.gateways, { onDelete: "CASCADE" }) + organization: Organization; - @Column() - rxPacketsReceived: number; + @Column() + rxPacketsReceived: number; - @Column() - txPacketsEmitted: number; + @Column() + txPacketsEmitted: number; - @Column() - tags: string; + @Column() + tags: string; - @Column({ - type: "geometry", - nullable: true, - spatialFeatureType: "Point", - srid: 4326, - }) - location?: Point; + @Column({ + type: "geometry", + nullable: true, + spatialFeatureType: "Point", + srid: 4326, + }) + location?: Point; - @Column({ type: "decimal", nullable: true }) - altitude?: number; + @Column({ type: "decimal", nullable: true }) + altitude?: number; - @Column({ nullable: true }) - lastSeenAt?: Date; + @Column({ nullable: true }) + lastSeenAt?: Date; - @Column("enum", { nullable: true, enum: GatewayPlacement }) - placement?: GatewayPlacement; + @Column("enum", { nullable: true, enum: GatewayPlacement }) + placement?: GatewayPlacement; - @Column({ nullable: true }) - modelName?: string; + @Column({ nullable: true }) + modelName?: string; - @Column({ nullable: true }) - antennaType?: string; + @Column({ nullable: true }) + antennaType?: string; - @Column("enum", { nullable: true, enum: GatewayStatus }) - status?: GatewayStatus; + @Column("enum", { nullable: true, enum: GatewayStatus }) + status?: GatewayStatus; - @Column({ nullable: true }) - gatewayResponsibleName?: string; + @Column({ nullable: true }) + gatewayResponsibleName?: string; - @Column({ nullable: true }) - gatewayResponsibleEmail?: string; + @Column({ nullable: true }) + gatewayResponsibleEmail?: string; - @Column({ nullable: true }) - gatewayResponsiblePhoneNumber?: string; + @Column({ nullable: true }) + gatewayResponsiblePhoneNumber?: string; - @Column({ nullable: true }) - operationalResponsibleName?: string; + @Column({ nullable: true }) + operationalResponsibleName?: string; - @Column({ nullable: true }) - operationalResponsibleEmail?: string; + @Column({ nullable: true }) + operationalResponsibleEmail?: string; } diff --git a/src/entities/generic-http-device.entity.ts b/src/entities/generic-http-device.entity.ts index 3064178d..d202a862 100644 --- a/src/entities/generic-http-device.entity.ts +++ b/src/entities/generic-http-device.entity.ts @@ -6,19 +6,19 @@ import { IoTDeviceType } from "@enum/device-type.enum"; @ChildEntity(IoTDeviceType.GenericHttp) export class GenericHTTPDevice extends IoTDevice { - @Column({ - nullable: true, - type: "varchar", - comment: "Used for GenericHTTPDevice", - }) - apiKey: string; + @Column({ + nullable: true, + type: "varchar", + comment: "Used for GenericHTTPDevice", + }) + apiKey: string; - @BeforeInsert() - private beforeInsert() { - /** - * Generate uuid (version 4 = random) to be used as the apiKey for this GenericHTTPDevice - */ - this.apiKey = uuidv4(); - this.type = IoTDeviceType.GenericHttp; - } + @BeforeInsert() + private beforeInsert() { + /** + * Generate uuid (version 4 = random) to be used as the apiKey for this GenericHTTPDevice + */ + this.apiKey = uuidv4(); + this.type = IoTDeviceType.GenericHttp; + } } diff --git a/src/entities/http-push-data-target.entity.ts b/src/entities/http-push-data-target.entity.ts index 4a5a418c..dd52c34a 100644 --- a/src/entities/http-push-data-target.entity.ts +++ b/src/entities/http-push-data-target.entity.ts @@ -8,32 +8,32 @@ import { HttpPushDataTargetConfiguration } from "./interfaces/http-push-data-tar @ChildEntity(DataTargetType.HttpPush) export class HttpPushDataTarget extends DataTarget { - @Column() - url: string; + @Column() + url: string; - @Column({ default: 30000, comment: "HTTP call timeout in milliseconds" }) - timeout: number; + @Column({ default: 30000, comment: "HTTP call timeout in milliseconds" }) + timeout: number; - @Column({ nullable: true }) - authorizationHeader?: string; + @Column({ nullable: true }) + authorizationHeader?: string; - @BeforeInsert() - private beforeInsert() { - /** - * Generate uuid (version 4 = random) to be used as the apiKey for this GenericHTTPDevice - */ - this.type = DataTargetType.HttpPush; - } + @BeforeInsert() + private beforeInsert() { + /** + * Generate uuid (version 4 = random) to be used as the apiKey for this GenericHTTPDevice + */ + this.type = DataTargetType.HttpPush; + } - toConfiguration(): HttpPushDataTargetConfiguration { - return { - url: this.url, - timeout: this.timeout, - authorizationType: - this.authorizationHeader != "" - ? AuthorizationType.HEADER_BASED_AUTHORIZATION - : AuthorizationType.NO_AUTHORIZATION, - authorizationHeader: this.authorizationHeader, - }; - } + toConfiguration(): HttpPushDataTargetConfiguration { + return { + url: this.url, + timeout: this.timeout, + authorizationType: + this.authorizationHeader != "" + ? AuthorizationType.HEADER_BASED_AUTHORIZATION + : AuthorizationType.NO_AUTHORIZATION, + authorizationHeader: this.authorizationHeader, + }; + } } diff --git a/src/entities/interfaces/chirpstack-id-response.interface.ts b/src/entities/interfaces/chirpstack-id-response.interface.ts index 7f23bafb..f45e2cc5 100644 --- a/src/entities/interfaces/chirpstack-id-response.interface.ts +++ b/src/entities/interfaces/chirpstack-id-response.interface.ts @@ -1,3 +1,3 @@ export interface IdResponse { - id: string; + id: string; } diff --git a/src/entities/interfaces/data-target-send-status.interface.ts b/src/entities/interfaces/data-target-send-status.interface.ts index 768109b5..a97940e5 100644 --- a/src/entities/interfaces/data-target-send-status.interface.ts +++ b/src/entities/interfaces/data-target-send-status.interface.ts @@ -1,6 +1,6 @@ import { SendStatus } from "@enum/send-status.enum"; export interface DataTargetSendStatus { - errorMessage?: string; - status: SendStatus; + errorMessage?: string; + status: SendStatus; } diff --git a/src/entities/interfaces/fiware-data-target-configuration.interface.ts b/src/entities/interfaces/fiware-data-target-configuration.interface.ts index f615da57..c2dd6225 100644 --- a/src/entities/interfaces/fiware-data-target-configuration.interface.ts +++ b/src/entities/interfaces/fiware-data-target-configuration.interface.ts @@ -1,16 +1,16 @@ import { AuthorizationType } from "@enum/authorization-type.enum"; export interface FiwareDataTargetConfiguration { - url: string; - timeout: number; - authorizationType: AuthorizationType; - username?: string; - password?: string; - authorizationHeader?: string; - tenant?: string; - context?: string; - clientId?: string; - clientSecret?: string; - tokenEndpoint?: string; - updatedAt: Date; + url: string; + timeout: number; + authorizationType: AuthorizationType; + username?: string; + password?: string; + authorizationHeader?: string; + tenant?: string; + context?: string; + clientId?: string; + clientSecret?: string; + tokenEndpoint?: string; + updatedAt: Date; } diff --git a/src/entities/interfaces/http-push-data-target-configuration.interface.ts b/src/entities/interfaces/http-push-data-target-configuration.interface.ts index cd41c2e3..5ff13f60 100644 --- a/src/entities/interfaces/http-push-data-target-configuration.interface.ts +++ b/src/entities/interfaces/http-push-data-target-configuration.interface.ts @@ -1,10 +1,10 @@ import { AuthorizationType } from "@enum/authorization-type.enum"; export interface HttpPushDataTargetConfiguration { - url: string; - timeout: number; - authorizationType: AuthorizationType; - username?: string; - password?: string; - authorizationHeader?: string; + url: string; + timeout: number; + authorizationType: AuthorizationType; + username?: string; + password?: string; + authorizationHeader?: string; } diff --git a/src/entities/interfaces/http-push-data-target-data.interface.ts b/src/entities/interfaces/http-push-data-target-data.interface.ts index 085c8ca9..6b39408d 100644 --- a/src/entities/interfaces/http-push-data-target-data.interface.ts +++ b/src/entities/interfaces/http-push-data-target-data.interface.ts @@ -1,4 +1,4 @@ export interface HttpPushDataTargetData { - rawBody: string; - mimeType: string; + rawBody: string; + mimeType: string; } diff --git a/src/entities/interfaces/mqtt-data-target-configuration.interface.ts b/src/entities/interfaces/mqtt-data-target-configuration.interface.ts index 96c79dda..0b9a365e 100644 --- a/src/entities/interfaces/mqtt-data-target-configuration.interface.ts +++ b/src/entities/interfaces/mqtt-data-target-configuration.interface.ts @@ -1,9 +1,9 @@ export interface MqttDataTargetConfiguration { - url: string; - port: number; - topic: string; - qos: 0 | 1 | 2; - timeout: number; - username?: string; - password?: string; + url: string; + port: number; + topic: string; + qos: 0 | 1 | 2; + timeout: number; + username?: string; + password?: string; } diff --git a/src/entities/iot-device-payload-decoder-data-target-connection.entity.ts b/src/entities/iot-device-payload-decoder-data-target-connection.entity.ts index 986ba9c1..43ddd16f 100644 --- a/src/entities/iot-device-payload-decoder-data-target-connection.entity.ts +++ b/src/entities/iot-device-payload-decoder-data-target-connection.entity.ts @@ -12,15 +12,15 @@ import { PayloadDecoder } from "@entities/payload-decoder.entity"; @Index(["payloadDecoder"]) @Index(["dataTarget"]) export class IoTDevicePayloadDecoderDataTargetConnection extends DbBaseEntity { - @ManyToMany(() => IoTDevice, iotdevice => iotdevice.connections) - @JoinTable() - iotDevices: IoTDevice[]; + @ManyToMany(() => IoTDevice, iotdevice => iotdevice.connections) + @JoinTable() + iotDevices: IoTDevice[]; - @ManyToOne(() => PayloadDecoder, { nullable: true, onDelete: "RESTRICT" }) - @JoinColumn() - payloadDecoder?: PayloadDecoder; + @ManyToOne(() => PayloadDecoder, { nullable: true, onDelete: "RESTRICT" }) + @JoinColumn() + payloadDecoder?: PayloadDecoder; - @ManyToOne(() => DataTarget, { onDelete: "CASCADE" }) - @JoinColumn() - dataTarget: DataTarget; + @ManyToOne(() => DataTarget, { onDelete: "CASCADE" }) + @JoinColumn() + dataTarget: DataTarget; } diff --git a/src/entities/iot-device.entity.ts b/src/entities/iot-device.entity.ts index 97cb15e1..e2f3999e 100644 --- a/src/entities/iot-device.entity.ts +++ b/src/entities/iot-device.entity.ts @@ -1,15 +1,15 @@ import { Length } from "class-validator"; import { Point } from "geojson"; import { - Column, - Entity, - JoinColumn, - JoinTable, - ManyToMany, - ManyToOne, - OneToMany, - OneToOne, - TableInheritance, + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + OneToOne, + TableInheritance, } from "typeorm"; import { Application } from "@entities/application.entity"; @@ -24,84 +24,77 @@ import { ReceivedMessageSigFoxSignals } from "./received-message-sigfox-signals. @Entity("iot_device") @TableInheritance({ - column: { type: "enum", name: "type", enum: IoTDeviceType }, + column: { type: "enum", name: "type", enum: IoTDeviceType }, }) export abstract class IoTDevice extends DbBaseEntity { - @Column() - name: string; + @Column() + name: string; - @ManyToOne( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Application, - application => application.iotDevices, - { onDelete: "CASCADE" } - ) - application: Application; + @ManyToOne( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => Application, + application => application.iotDevices, + { onDelete: "CASCADE" } + ) + application: Application; - @Column({ - type: "geometry", - nullable: true, - spatialFeatureType: "Point", - srid: 4326, - }) - location?: Point; + @Column({ + type: "geometry", + nullable: true, + spatialFeatureType: "Point", + srid: 4326, + }) + location?: Point; - @Column({ nullable: true }) - @Length(0, 1024) - commentOnLocation?: string; + @Column({ nullable: true }) + @Length(0, 1024) + commentOnLocation?: string; - @Column({ nullable: true }) - @Length(0, 1024) - comment?: string; + @Column({ nullable: true }) + @Length(0, 1024) + comment?: string; - @Column({ type: "jsonb", nullable: true }) - metadata: JSON; + @Column({ type: "jsonb", nullable: true }) + metadata: JSON; - @Column("enum", { - enum: IoTDeviceType, - }) - type: IoTDeviceType; + @Column("enum", { + enum: IoTDeviceType, + }) + type: IoTDeviceType; - @OneToOne( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => ReceivedMessage, - latestReceivedMessage => latestReceivedMessage.device, - { onDelete: "CASCADE" } - ) - latestReceivedMessage: ReceivedMessage; + @OneToOne( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => ReceivedMessage, + latestReceivedMessage => latestReceivedMessage.device, + { onDelete: "CASCADE" } + ) + latestReceivedMessage: ReceivedMessage; - @OneToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => ReceivedMessageMetadata, - receivedMessagesMetadata => receivedMessagesMetadata.device, - { onDelete: "CASCADE" } - ) - receivedMessagesMetadata: ReceivedMessageMetadata[]; + @OneToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => ReceivedMessageMetadata, + receivedMessagesMetadata => receivedMessagesMetadata.device, + { onDelete: "CASCADE" } + ) + receivedMessagesMetadata: ReceivedMessageMetadata[]; - @ManyToMany( - () => IoTDevicePayloadDecoderDataTargetConnection, - connection => connection.iotDevices - ) - connections: IoTDevicePayloadDecoderDataTargetConnection[]; + @ManyToMany(() => IoTDevicePayloadDecoderDataTargetConnection, connection => connection.iotDevices) + connections: IoTDevicePayloadDecoderDataTargetConnection[]; - @ManyToOne(() => DeviceModel, deviceModel => deviceModel.devices, { - onDelete: "RESTRICT", - }) - @JoinColumn() - deviceModel?: DeviceModel; + @ManyToOne(() => DeviceModel, deviceModel => deviceModel.devices, { + onDelete: "RESTRICT", + }) + @JoinColumn() + deviceModel?: DeviceModel; - @ManyToMany(() => Multicast) - @JoinTable() - multicasts: Multicast[]; + @ManyToMany(() => Multicast) + @JoinTable() + multicasts: Multicast[]; - @OneToMany( - () => ReceivedMessageSigFoxSignals, - message => message.device, - { onDelete: "CASCADE" } - ) - receivedSigFoxSignalsMessages: ReceivedMessageSigFoxSignals[]; + @OneToMany(() => ReceivedMessageSigFoxSignals, message => message.device, { onDelete: "CASCADE" }) + receivedSigFoxSignalsMessages: ReceivedMessageSigFoxSignals[]; - toString(): string { - return `IoTDevices: id: ${this.id} - name: ${this.name}`; - } + toString(): string { + return `IoTDevices: id: ${this.id} - name: ${this.name}`; + } } diff --git a/src/entities/lorawan-device.entity.ts b/src/entities/lorawan-device.entity.ts index 00d7e3b7..ccd6bde1 100644 --- a/src/entities/lorawan-device.entity.ts +++ b/src/entities/lorawan-device.entity.ts @@ -6,22 +6,22 @@ import { IoTDeviceType } from "@enum/device-type.enum"; @ChildEntity(IoTDeviceType.LoRaWAN) export class LoRaWANDevice extends IoTDevice { - @Column({ nullable: true }) - @Length(16, 16, { message: "Must be 16 characters" }) - deviceEUI: string; + @Column({ nullable: true }) + @Length(16, 16, { message: "Must be 16 characters" }) + deviceEUI: string; - @Column({ nullable: true }) - @Length(32, 32, { message: "Must be 32 characters" }) - OTAAapplicationKey: string; + @Column({ nullable: true }) + @Length(32, 32, { message: "Must be 32 characters" }) + OTAAapplicationKey: string; - @Column({ nullable: true }) - deviceProfileName: string; + @Column({ nullable: true }) + deviceProfileName: string; - @Column({ nullable: true }) - chirpstackApplicationId: string; + @Column({ nullable: true }) + chirpstackApplicationId: string; - @BeforeInsert() - private beforeInsert() { - this.type = IoTDeviceType.LoRaWAN; - } + @BeforeInsert() + private beforeInsert() { + this.type = IoTDeviceType.LoRaWAN; + } } diff --git a/src/entities/lorawan-multicast.entity.ts b/src/entities/lorawan-multicast.entity.ts index d781bc9a..b03fa930 100644 --- a/src/entities/lorawan-multicast.entity.ts +++ b/src/entities/lorawan-multicast.entity.ts @@ -6,30 +6,30 @@ import { DbBaseEntity } from "./base.entity"; @Entity("lorawan-multicast") export class LorawanMulticastDefinition extends DbBaseEntity { - @Column() - address: string; + @Column() + address: string; - @Column() - networkSessionKey: string; + @Column() + networkSessionKey: string; - @Column() - applicationSessionKey: string; + @Column() + applicationSessionKey: string; - @Column() - frameCounter: number; + @Column() + frameCounter: number; - @Column() - dataRate: number; + @Column() + dataRate: number; - @Column() - frequency: number; + @Column() + frequency: number; - @Column() - groupType: multicastGroup; + @Column() + groupType: multicastGroup; - @OneToOne(type => Multicast, multicast => multicast.lorawanMulticastDefinition) - multicast: Multicast; + @OneToOne(type => Multicast, multicast => multicast.lorawanMulticastDefinition) + multicast: Multicast; - @Column({nullable: true}) - chirpstackGroupId?: string; + @Column({ nullable: true }) + chirpstackGroupId?: string; } diff --git a/src/entities/mqtt-data-target.entity.ts b/src/entities/mqtt-data-target.entity.ts index e2657cc2..771c7a8e 100644 --- a/src/entities/mqtt-data-target.entity.ts +++ b/src/entities/mqtt-data-target.entity.ts @@ -6,41 +6,41 @@ import { BeforeInsert, ChildEntity, Column } from "typeorm"; @ChildEntity(DataTargetType.MQTT) export class MqttDataTarget extends DataTarget { - @Column() - url: string; - - @Column({ default: 30000, comment: "HTTP call timeout in milliseconds" }) - timeout: number; - - @Column({ nullable: true }) - mqttPort?: number; - - @Column({ nullable: true }) - mqttTopic?: string; - - @Column({ nullable: true }) - mqttQos?: QoS; - - @Column({ nullable: true }) - mqttUsername?: string; - - @Column({ nullable: true }) - mqttPassword?: string; - - @BeforeInsert() - private beforeInsert() { - this.type = DataTargetType.MQTT; - } - - toConfiguration(): MqttDataTargetConfiguration { - return { - url: this.url, - port: this.mqttPort, - topic: this.mqttTopic, - qos: this.mqttQos, - timeout: this.timeout, - username: this.mqttUsername, - password: this.mqttPassword, - }; - } + @Column() + url: string; + + @Column({ default: 30000, comment: "HTTP call timeout in milliseconds" }) + timeout: number; + + @Column({ nullable: true }) + mqttPort?: number; + + @Column({ nullable: true }) + mqttTopic?: string; + + @Column({ nullable: true }) + mqttQos?: QoS; + + @Column({ nullable: true }) + mqttUsername?: string; + + @Column({ nullable: true }) + mqttPassword?: string; + + @BeforeInsert() + private beforeInsert() { + this.type = DataTargetType.MQTT; + } + + toConfiguration(): MqttDataTargetConfiguration { + return { + url: this.url, + port: this.mqttPort, + topic: this.mqttTopic, + qos: this.mqttQos, + timeout: this.timeout, + username: this.mqttUsername, + password: this.mqttPassword, + }; + } } diff --git a/src/entities/mqtt-external-broker-device.entity.ts b/src/entities/mqtt-external-broker-device.entity.ts index 87031492..62f29773 100644 --- a/src/entities/mqtt-external-broker-device.entity.ts +++ b/src/entities/mqtt-external-broker-device.entity.ts @@ -5,40 +5,40 @@ import { IoTDeviceType } from "@enum/device-type.enum"; @ChildEntity(IoTDeviceType.MQTTExternalBroker) export class MQTTExternalBrokerDevice extends IoTDevice { - @Column("enum", { - enum: AuthenticationType, - }) - authenticationType: AuthenticationType; + @Column("enum", { + enum: AuthenticationType, + }) + authenticationType: AuthenticationType; - @Column({ nullable: true }) - caCertificate: string; + @Column({ nullable: true }) + caCertificate: string; - @Column({ nullable: true }) - deviceCertificate: string; + @Column({ nullable: true }) + deviceCertificate: string; - @Column({ nullable: true }) - deviceCertificateKey: string; + @Column({ nullable: true }) + deviceCertificateKey: string; - @Column({ nullable: true }) - mqttusername: string; + @Column({ nullable: true }) + mqttusername: string; - @Column({ nullable: true }) - mqttpassword: string; + @Column({ nullable: true }) + mqttpassword: string; - @Column({ nullable: true }) - mqttURL: string; + @Column({ nullable: true }) + mqttURL: string; - @Column({ nullable: true }) - mqttPort: number; + @Column({ nullable: true }) + mqttPort: number; - @Column({ nullable: true }) - mqtttopicname: string; + @Column({ nullable: true }) + mqtttopicname: string; - @Column({ default: false }) - invalidMqttConfig: boolean; + @Column({ default: false }) + invalidMqttConfig: boolean; - @BeforeInsert() - private beforeInsert() { - this.type = IoTDeviceType.MQTTExternalBroker; - } + @BeforeInsert() + private beforeInsert() { + this.type = IoTDeviceType.MQTTExternalBroker; + } } diff --git a/src/entities/mqtt-internal-broker-device.entity.ts b/src/entities/mqtt-internal-broker-device.entity.ts index 58f06ed0..3fe43d46 100644 --- a/src/entities/mqtt-internal-broker-device.entity.ts +++ b/src/entities/mqtt-internal-broker-device.entity.ts @@ -6,46 +6,46 @@ import { MQTTPermissionLevel } from "@enum/mqtt-permission-level.enum"; @ChildEntity(IoTDeviceType.MQTTInternalBroker) export class MQTTInternalBrokerDevice extends IoTDevice { - @Column("enum", { - enum: AuthenticationType, - }) - authenticationType: AuthenticationType; + @Column("enum", { + enum: AuthenticationType, + }) + authenticationType: AuthenticationType; - @Column({ nullable: true }) - caCertificate: string; + @Column({ nullable: true }) + caCertificate: string; - @Column({ nullable: true }) - deviceCertificate: string; + @Column({ nullable: true }) + deviceCertificate: string; - @Column({ nullable: true }) - deviceCertificateKey: string; // Should be encrypted at a minimum + @Column({ nullable: true }) + deviceCertificateKey: string; // Should be encrypted at a minimum - @Column({ nullable: true }) - mqttusername: string; + @Column({ nullable: true }) + mqttusername: string; - @Column({ nullable: true }) - mqttpassword: string; + @Column({ nullable: true }) + mqttpassword: string; - @Column({ nullable: true }) - mqttURL: string; + @Column({ nullable: true }) + mqttURL: string; - @Column({ nullable: true }) - mqttPort: number; + @Column({ nullable: true }) + mqttPort: number; - @Column({ nullable: true }) - mqtttopicname: string; + @Column({ nullable: true }) + mqtttopicname: string; - @Column("enum", { - nullable: true, - enum: MQTTPermissionLevel, - }) - permissions: MQTTPermissionLevel; + @Column("enum", { + nullable: true, + enum: MQTTPermissionLevel, + }) + permissions: MQTTPermissionLevel; - @Column({ nullable: true }) - mqttpasswordhash: string; + @Column({ nullable: true }) + mqttpasswordhash: string; - @BeforeInsert() - private beforeInsert() { - this.type = IoTDeviceType.MQTTInternalBroker; - } + @BeforeInsert() + private beforeInsert() { + this.type = IoTDeviceType.MQTTInternalBroker; + } } diff --git a/src/entities/multicast.entity.ts b/src/entities/multicast.entity.ts index c3371eed..c048ea08 100644 --- a/src/entities/multicast.entity.ts +++ b/src/entities/multicast.entity.ts @@ -1,12 +1,4 @@ -import { - Column, - Entity, - JoinColumn, - JoinTable, - ManyToMany, - ManyToOne, - OneToOne, -} from "typeorm"; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToOne } from "typeorm"; import { Application } from "@entities/application.entity"; import { IoTDevice } from "./iot-device.entity"; @@ -15,25 +7,19 @@ import { DbBaseEntity } from "./base.entity"; @Entity("multicast") export class Multicast extends DbBaseEntity { - @Column() - groupName: string; + @Column() + groupName: string; - @ManyToOne( - _ => Application, - application => application.multicasts, - { onDelete: "CASCADE" } - ) - application: Application; + @ManyToOne(_ => Application, application => application.multicasts, { onDelete: "CASCADE" }) + application: Application; - @ManyToMany(() => IoTDevice, iotDevices => iotDevices.multicasts) - @JoinTable() - iotDevices: IoTDevice[]; + @ManyToMany(() => IoTDevice, iotDevices => iotDevices.multicasts) + @JoinTable() + iotDevices: IoTDevice[]; - @OneToOne( - type => LorawanMulticastDefinition, - lorawanMulticastDefinition => lorawanMulticastDefinition.multicast, - { cascade: true } - ) - @JoinColumn() - lorawanMulticastDefinition: LorawanMulticastDefinition; + @OneToOne(type => LorawanMulticastDefinition, lorawanMulticastDefinition => lorawanMulticastDefinition.multicast, { + cascade: true, + }) + @JoinColumn() + lorawanMulticastDefinition: LorawanMulticastDefinition; } diff --git a/src/entities/open-data-dk-dataset.entity.ts b/src/entities/open-data-dk-dataset.entity.ts index 2781b6a2..399c7120 100644 --- a/src/entities/open-data-dk-dataset.entity.ts +++ b/src/entities/open-data-dk-dataset.entity.ts @@ -4,29 +4,29 @@ import { DataTarget } from "./data-target.entity"; @Entity("open_data_dk_dataset") export class OpenDataDkDataset extends DbBaseEntity { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @OneToOne(type => DataTarget, dt => dt.openDataDkDataset, { onDelete: "CASCADE" }) - @JoinColumn() - dataTarget: DataTarget; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @OneToOne(type => DataTarget, dt => dt.openDataDkDataset, { onDelete: "CASCADE" }) + @JoinColumn() + dataTarget: DataTarget; - @Column() - name: string; + @Column() + name: string; - @Column({ nullable: false, default: "" }) - description: string; + @Column({ nullable: false, default: "" }) + description: string; - @Column("text", { array: true, nullable: true }) - keywords?: string[]; + @Column("text", { array: true, nullable: true }) + keywords?: string[]; - @Column() - license: string; + @Column() + license: string; - @Column() - authorName: string; + @Column() + authorName: string; - @Column() - authorEmail: string; + @Column() + authorEmail: string; - @Column({ nullable: false, default: "" }) - resourceTitle: string; + @Column({ nullable: false, default: "" }) + resourceTitle: string; } diff --git a/src/entities/open-data-dk-push-data-target.entity.ts b/src/entities/open-data-dk-push-data-target.entity.ts index fac6cf35..02fde8ac 100644 --- a/src/entities/open-data-dk-push-data-target.entity.ts +++ b/src/entities/open-data-dk-push-data-target.entity.ts @@ -4,8 +4,8 @@ import { BeforeInsert, ChildEntity } from "typeorm"; @ChildEntity(DataTargetType.OpenDataDK) export class OpenDataDkDataTarget extends DataTarget { - @BeforeInsert() - private beforeInsert() { - this.type = DataTargetType.OpenDataDK; - } + @BeforeInsert() + private beforeInsert() { + this.type = DataTargetType.OpenDataDK; + } } diff --git a/src/entities/organization.entity.ts b/src/entities/organization.entity.ts index 54f4f85c..c3428c8e 100644 --- a/src/entities/organization.entity.ts +++ b/src/entities/organization.entity.ts @@ -13,55 +13,55 @@ import { Gateway } from "@entities/gateway.entity"; @Entity("organization") @Unique(["name"]) export class Organization extends DbBaseEntity { - @Column({ unique: true }) - name: string; + @Column({ unique: true }) + name: string; - @Column({ default: false }) - openDataDkRegistered: boolean; + @Column({ default: false }) + openDataDkRegistered: boolean; - @OneToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Application, - application => application.belongsTo, - { onDelete: "CASCADE" } - ) - applications: Application[]; - - @OneToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Permission, - permission => permission.organization, - { onDelete: "CASCADE" } - ) - permissions: Permission[]; - - @OneToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => PayloadDecoder, - payloadDecoder => payloadDecoder.organization, - { onDelete: "CASCADE", nullable: true } - ) - payloadDecoders?: PayloadDecoder[]; + @OneToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => Application, + application => application.belongsTo, + { onDelete: "CASCADE" } + ) + applications: Application[]; + @OneToMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars - @OneToMany(type => SigFoxGroup, sigfoxGroup => sigfoxGroup.belongsTo, { - onDelete: "CASCADE", - nullable: true, - }) - sigfoxGroups: SigFoxGroup[]; + type => Permission, + permission => permission.organization, + { onDelete: "CASCADE" } + ) + permissions: Permission[]; + @OneToMany( // eslint-disable-next-line @typescript-eslint/no-unused-vars - @OneToMany(type => DeviceModel, deviceModel => deviceModel.belongsTo, { - onDelete: "CASCADE", - nullable: true, - }) - deviceModels?: DeviceModel[]; + type => PayloadDecoder, + payloadDecoder => payloadDecoder.organization, + { onDelete: "CASCADE", nullable: true } + ) + payloadDecoders?: PayloadDecoder[]; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @OneToMany(type => SigFoxGroup, sigfoxGroup => sigfoxGroup.belongsTo, { + onDelete: "CASCADE", + nullable: true, + }) + sigfoxGroups: SigFoxGroup[]; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @OneToMany(type => DeviceModel, deviceModel => deviceModel.belongsTo, { + onDelete: "CASCADE", + nullable: true, + }) + deviceModels?: DeviceModel[]; - @ManyToMany(_ => User, user => user.requestedOrganizations, { - nullable: true, - }) - awaitingUsers?: User[]; + @ManyToMany(_ => User, user => user.requestedOrganizations, { + nullable: true, + }) + awaitingUsers?: User[]; - @OneToMany(_ => Gateway, gateway => gateway.organization, { nullable: true }) - gateways?: Gateway[]; + @OneToMany(_ => Gateway, gateway => gateway.organization, { nullable: true }) + gateways?: Gateway[]; } diff --git a/src/entities/payload-decoder.entity.ts b/src/entities/payload-decoder.entity.ts index 938ef318..9f014b1c 100644 --- a/src/entities/payload-decoder.entity.ts +++ b/src/entities/payload-decoder.entity.ts @@ -5,17 +5,17 @@ import { Organization } from "@entities/organization.entity"; @Entity("payload_decoder") export class PayloadDecoder extends DbBaseEntity { - @Column() - name: string; + @Column() + name: string; - @Column({ type: "text" }) - decodingFunction: string; + @Column({ type: "text" }) + decodingFunction: string; - @ManyToOne( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => Organization, - organization => organization.payloadDecoders, - { onDelete: "CASCADE", nullable: true } - ) - organization?: Organization; + @ManyToOne( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => Organization, + organization => organization.payloadDecoders, + { onDelete: "CASCADE", nullable: true } + ) + organization?: Organization; } diff --git a/src/entities/permissions/permission-type.entity.ts b/src/entities/permissions/permission-type.entity.ts index 988944be..ebc698c5 100644 --- a/src/entities/permissions/permission-type.entity.ts +++ b/src/entities/permissions/permission-type.entity.ts @@ -7,15 +7,15 @@ import { nameof } from "@helpers/type-helper"; @Entity("permission_type") @Unique([nameof("type"), nameof("permission")]) export class PermissionTypeEntity extends DbBaseEntity { - @Column("enum", { - enum: PermissionType - }) - type: PermissionType; + @Column("enum", { + enum: PermissionType, + }) + type: PermissionType; - @ManyToOne(() => Permission, p => p.type, { - onDelete: "CASCADE", - // Delete the row instead of null'ing application. Useful for updates - orphanedRowAction: "delete", - }) - permission: Permission; + @ManyToOne(() => Permission, p => p.type, { + onDelete: "CASCADE", + // Delete the row instead of null'ing application. Useful for updates + orphanedRowAction: "delete", + }) + permission: Permission; } diff --git a/src/entities/permissions/permission.entity.ts b/src/entities/permissions/permission.entity.ts index ae60ef9f..fa919cf2 100644 --- a/src/entities/permissions/permission.entity.ts +++ b/src/entities/permissions/permission.entity.ts @@ -1,15 +1,7 @@ import { DbBaseEntity } from "@entities/base.entity"; import { User } from "@entities/user.entity"; import { PermissionType } from "@enum/permission-type.enum"; -import { - Column, - Entity, - ManyToMany, - TableInheritance, - OneToMany, - ManyToOne, - RelationId, -} from "typeorm"; +import { Column, Entity, ManyToMany, TableInheritance, OneToMany, ManyToOne, RelationId } from "typeorm"; import { PermissionTypeEntity } from "./permission-type.entity"; import { Application } from "@entities/application.entity"; import { Organization } from "@entities/organization.entity"; @@ -18,42 +10,42 @@ import { nameof } from "@helpers/type-helper"; @Entity("permission") export class Permission extends DbBaseEntity { - constructor(name: string, org?: Organization, addNewApps = false) { - super(); - this.name = name; - this.organization = org; - this.automaticallyAddNewApplications = addNewApps; - } - - @OneToMany(() => PermissionTypeEntity, entity => entity.permission, { - nullable: false, - cascade: true, - }) - type: PermissionTypeEntity[]; - - @Column() - name: string; - - @ManyToMany( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - () => User, - user => user.permissions - ) - users: User[]; - - @ManyToMany(() => Application, application => application.permissions) - applications: Application[]; - - // ORM-specific column for extracting only the ids of related applications - @RelationId(nameof("applications")) - applicationIds: Application["id"][]; - - @Column({ nullable: true, default: false, type: Boolean }) - automaticallyAddNewApplications = false; - - @ManyToOne(() => Organization, { onDelete: "CASCADE" }) - organization: Organization; - - @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) - apiKeys: ApiKey[]; + constructor(name: string, org?: Organization, addNewApps = false) { + super(); + this.name = name; + this.organization = org; + this.automaticallyAddNewApplications = addNewApps; + } + + @OneToMany(() => PermissionTypeEntity, entity => entity.permission, { + nullable: false, + cascade: true, + }) + type: PermissionTypeEntity[]; + + @Column() + name: string; + + @ManyToMany( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + () => User, + user => user.permissions + ) + users: User[]; + + @ManyToMany(() => Application, application => application.permissions) + applications: Application[]; + + // ORM-specific column for extracting only the ids of related applications + @RelationId(nameof("applications")) + applicationIds: Application["id"][]; + + @Column({ nullable: true, default: false, type: Boolean }) + automaticallyAddNewApplications = false; + + @ManyToOne(() => Organization, { onDelete: "CASCADE" }) + organization: Organization; + + @ManyToMany(_ => ApiKey, key => key.permissions, { onDelete: "CASCADE" }) + apiKeys: ApiKey[]; } diff --git a/src/entities/received-message-metadata.entity.ts b/src/entities/received-message-metadata.entity.ts index e2b679de..68a60bdd 100644 --- a/src/entities/received-message-metadata.entity.ts +++ b/src/entities/received-message-metadata.entity.ts @@ -6,19 +6,19 @@ import { IoTDevice } from "@entities/iot-device.entity"; @Entity("received_message_metadata") @Index(["device", "sentTime"]) export class ReceivedMessageMetadata extends DbBaseEntity { - @ManyToOne( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => IoTDevice, - device => device.receivedMessagesMetadata, - { onDelete: "CASCADE" } - ) - device: IoTDevice; + @ManyToOne( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => IoTDevice, + device => device.receivedMessagesMetadata, + { onDelete: "CASCADE" } + ) + device: IoTDevice; - @Column({ - comment: "Time reported by device (if possible, otherwise time received)", - }) - sentTime: Date; + @Column({ + comment: "Time reported by device (if possible, otherwise time received)", + }) + sentTime: Date; - @Column({ type: "jsonb", nullable: true }) - signalData?: JSON; + @Column({ type: "jsonb", nullable: true }) + signalData?: JSON; } diff --git a/src/entities/received-message-sigfox-signals.entity.ts b/src/entities/received-message-sigfox-signals.entity.ts index 30993df1..dbb41366 100644 --- a/src/entities/received-message-sigfox-signals.entity.ts +++ b/src/entities/received-message-sigfox-signals.entity.ts @@ -4,19 +4,19 @@ import { Column, Entity, ManyToOne } from "typeorm"; @Entity("received_message_sigfox_signals") export class ReceivedMessageSigFoxSignals extends DbBaseEntity { - @ManyToOne(() => IoTDevice, device => device.receivedSigFoxSignalsMessages, { - onDelete: "CASCADE", - }) - device: IoTDevice; + @ManyToOne(() => IoTDevice, device => device.receivedSigFoxSignalsMessages, { + onDelete: "CASCADE", + }) + device: IoTDevice; - @Column({ - comment: "Time reported by device (if possible, otherwise time received)", - }) - sentTime: Date; + @Column({ + comment: "Time reported by device (if possible, otherwise time received)", + }) + sentTime: Date; - @Column({ nullable: true }) - rssi?: number; + @Column({ nullable: true }) + rssi?: number; - @Column({ nullable: true }) - snr?: number; + @Column({ nullable: true }) + snr?: number; } diff --git a/src/entities/received-message.entity.ts b/src/entities/received-message.entity.ts index 89dd44ad..0677f35c 100644 --- a/src/entities/received-message.entity.ts +++ b/src/entities/received-message.entity.ts @@ -4,26 +4,26 @@ import { Column, Entity, JoinColumn, OneToOne } from "typeorm"; @Entity("received_message") export class ReceivedMessage extends DbBaseEntity { - @OneToOne( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type => IoTDevice, - device => device.latestReceivedMessage, - { onDelete: "CASCADE" } - ) - @JoinColumn() - device: IoTDevice; + @OneToOne( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type => IoTDevice, + device => device.latestReceivedMessage, + { onDelete: "CASCADE" } + ) + @JoinColumn() + device: IoTDevice; - @Column({ type: "jsonb" }) - rawData: JSON; + @Column({ type: "jsonb" }) + rawData: JSON; - @Column({ - comment: "Time reported by device (if possible, otherwise time received)", - }) - sentTime: Date; + @Column({ + comment: "Time reported by device (if possible, otherwise time received)", + }) + sentTime: Date; - @Column({ nullable: true }) - rssi?: number; + @Column({ nullable: true }) + rssi?: number; - @Column({ nullable: true }) - snr?: number; + @Column({ nullable: true }) + snr?: number; } diff --git a/src/entities/sigfox-device.entity.ts b/src/entities/sigfox-device.entity.ts index feef8c96..26da2cea 100644 --- a/src/entities/sigfox-device.entity.ts +++ b/src/entities/sigfox-device.entity.ts @@ -6,22 +6,22 @@ import { IoTDeviceType } from "@enum/device-type.enum"; @ChildEntity(IoTDeviceType.SigFox) export class SigFoxDevice extends IoTDevice { - @Column({ nullable: true }) - @Max(8, { message: "Must at most be 8 characters" }) - deviceId: string; + @Column({ nullable: true }) + @Max(8, { message: "Must at most be 8 characters" }) + deviceId: string; - @Column({ nullable: true }) - @Max(24, { message: "Must at most be 24 characters" }) - deviceTypeId: string; + @Column({ nullable: true }) + @Max(24, { message: "Must at most be 24 characters" }) + deviceTypeId: string; - @Column({ nullable: true }) - groupId: string; + @Column({ nullable: true }) + groupId: string; - @Column({ nullable: true }) - downlinkPayload: string; + @Column({ nullable: true }) + downlinkPayload: string; - @BeforeInsert() - private beforeInsert() { - this.type = IoTDeviceType.SigFox; - } + @BeforeInsert() + private beforeInsert() { + this.type = IoTDeviceType.SigFox; + } } diff --git a/src/entities/sigfox-group.entity.ts b/src/entities/sigfox-group.entity.ts index b3ba278a..5370be46 100644 --- a/src/entities/sigfox-group.entity.ts +++ b/src/entities/sigfox-group.entity.ts @@ -7,20 +7,20 @@ import { SigFoxApiGroupsContent } from "@dto/sigfox/external/sigfox-api-groups-r @Entity("sigfox_group") @Unique(["username", "belongsTo"]) export class SigFoxGroup extends DbBaseEntity { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToOne(type => Organization, organization => organization.sigfoxGroups, { - onDelete: "CASCADE", - }) - belongsTo: Organization; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @ManyToOne(type => Organization, organization => organization.sigfoxGroups, { + onDelete: "CASCADE", + }) + belongsTo: Organization; - @Column({ nullable: false }) - username: string; + @Column({ nullable: false }) + username: string; - @Column({ nullable: false, select: false }) - password: string; + @Column({ nullable: false, select: false }) + password: string; - @Column({ nullable: false, default: "" }) - sigfoxGroupId: string; + @Column({ nullable: false, default: "" }) + sigfoxGroupId: string; - sigfoxGroupData?: SigFoxApiGroupsContent; + sigfoxGroupData?: SigFoxApiGroupsContent; } diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 1e287a49..f601ce12 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -7,51 +7,47 @@ import { Organization } from "./organization.entity"; @Entity("user") @Unique(["email"]) export class User extends DbBaseEntity { - @Column() - name: string; + @Column() + name: string; - @Column({ nullable: true }) - email: string; + @Column({ nullable: true }) + email: string; - @Column({ select: false, nullable: true }) - passwordHash: string; + @Column({ select: false, nullable: true }) + passwordHash: string; - @Column({ default: true }) - active: boolean; + @Column({ default: true }) + active: boolean; - @Column({ nullable: true }) - lastLogin?: Date; + @Column({ nullable: true }) + lastLogin?: Date; - @Column({ nullable: true }) - nameId: string; + @Column({ nullable: true }) + nameId: string; - @Column({ nullable: true }) - awaitingConfirmation: boolean; + @Column({ nullable: true }) + awaitingConfirmation: boolean; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - @ManyToMany(type => Permission, permission => permission.users) - @JoinTable() - permissions: Permission[]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + @ManyToMany(type => Permission, permission => permission.users) + @JoinTable() + permissions: Permission[]; - @ManyToMany( - _ => Organization, - requestedOrganizations => requestedOrganizations.awaitingUsers, - { - nullable: true, - } - ) - @JoinTable() - requestedOrganizations: Organization[]; + @ManyToMany(_ => Organization, requestedOrganizations => requestedOrganizations.awaitingUsers, { + nullable: true, + }) + @JoinTable() + requestedOrganizations: Organization[]; - @OneToOne(type => ApiKey, a => a.systemUser, { - nullable: true, - cascade: false, - }) - apiKeyRef: ApiKey; + @OneToOne(type => ApiKey, a => a.systemUser, { + nullable: true, + cascade: false, + }) + apiKeyRef: ApiKey; - @Column({ default: false }) - isSystemUser: boolean; + @Column({ default: false }) + isSystemUser: boolean; - @Column({ default: false }) - showWelcomeScreen: boolean; + @Column({ default: false }) + showWelcomeScreen: boolean; } diff --git a/src/helpers/array-distinct.validator.ts b/src/helpers/array-distinct.validator.ts index 9c5a4df2..fa2a5683 100644 --- a/src/helpers/array-distinct.validator.ts +++ b/src/helpers/array-distinct.validator.ts @@ -1,8 +1,4 @@ -import { - registerDecorator, - ValidationArguments, - ValidationOptions, -} from "class-validator"; +import { registerDecorator, ValidationArguments, ValidationOptions } from "class-validator"; import { ErrorCodes } from "@enum/error-codes.enum"; /** @@ -11,31 +7,26 @@ import { ErrorCodes } from "@enum/error-codes.enum"; * @param validationOptions * @see https://github.com/typestack/class-validator/issues/592#issuecomment-621645012 */ -export function ArrayDistinct( - property: string, - validationOptions?: ValidationOptions -) { - return (object: unknown, propertyName: string): void => { - registerDecorator({ - name: "ArrayDistinct", - target: object.constructor, - propertyName, - constraints: [property], - options: validationOptions, - validator: { - validate(value: unknown): boolean { - if (Array.isArray(value)) { - const distinct = [ - ...new Set(value.map((v): unknown => v[property])), - ]; - return distinct.length === value.length; - } - return false; - }, - defaultMessage(args: ValidationArguments): string { - return ErrorCodes.DuplicatePermissionTypes; - }, - }, - }); - }; +export function ArrayDistinct(property: string, validationOptions?: ValidationOptions) { + return (object: unknown, propertyName: string): void => { + registerDecorator({ + name: "ArrayDistinct", + target: object.constructor, + propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: unknown): boolean { + if (Array.isArray(value)) { + const distinct = [...new Set(value.map((v): unknown => v[property]))]; + return distinct.length === value.length; + } + return false; + }, + defaultMessage(args: ValidationArguments): string { + return ErrorCodes.DuplicatePermissionTypes; + }, + }, + }); + }; } diff --git a/src/helpers/date.helper.ts b/src/helpers/date.helper.ts index 1258e287..fcc7ccfa 100644 --- a/src/helpers/date.helper.ts +++ b/src/helpers/date.helper.ts @@ -1,32 +1,32 @@ import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; export const subtractHours = (date: Date, hours = 1): Date => { - const newDate = new Date(); - newDate.setTime(date.getTime() - 1000 * (60 * 60 * hours)); - return newDate; + const newDate = new Date(); + newDate.setTime(date.getTime() - 1000 * (60 * 60 * hours)); + return newDate; }; export const subtractDays = (date: Date, days = 1): Date => { - const newDate = new Date(); - newDate.setDate(date.getDate() - days); - return newDate; + const newDate = new Date(); + newDate.setDate(date.getDate() - days); + return newDate; }; export const subtractYears = (date: Date, years = 1): Date => { - const newDate = new Date(); - newDate.setDate(date.getDate() - years * 365); - return newDate; + const newDate = new Date(); + newDate.setDate(date.getDate() - years * 365); + return newDate; }; export const dateToTimestamp = (date: Date): Timestamp => { - const timestamp = new Timestamp(); - timestamp.fromDate(date); - return timestamp; + const timestamp = new Timestamp(); + timestamp.fromDate(date); + return timestamp; }; export const timestampToDate = (timestamp: Timestamp.AsObject): Date => { - const seconds = timestamp.seconds; - const nanoseconds = timestamp.nanos / 1e6; // Convert nanoseconds to milliseconds - const milliseconds = seconds * 1000 + nanoseconds; - return new Date(milliseconds); + const seconds = timestamp.seconds; + const nanoseconds = timestamp.nanos / 1e6; // Convert nanoseconds to milliseconds + const milliseconds = seconds * 1000 + nanoseconds; + return new Date(milliseconds); }; diff --git a/src/helpers/date.validator.ts b/src/helpers/date.validator.ts index 0e97bf37..e5bc3374 100644 --- a/src/helpers/date.validator.ts +++ b/src/helpers/date.validator.ts @@ -5,10 +5,10 @@ import { IsDate, IsDateString } from "class-validator"; * Checks if a value can be converted to a date */ export const ValidateDate = (): PropertyDecorator => { - return (propertyValue: unknown, propertyName: string): void => { - // Validate whether the value is a valid ISO8601 date - IsDateString(propertyValue); - // Cast the value - Type(() => Date)(propertyValue, propertyName); - }; + return (propertyValue: unknown, propertyName: string): void => { + // Validate whether the value is a valid ISO8601 date + IsDateString(propertyValue); + // Cast the value + Type(() => Date)(propertyValue, propertyName); + }; }; diff --git a/src/helpers/env-variable-helper.ts b/src/helpers/env-variable-helper.ts index 3c223222..8c5ad3c6 100644 --- a/src/helpers/env-variable-helper.ts +++ b/src/helpers/env-variable-helper.ts @@ -4,30 +4,30 @@ const logLevels: LogLevel[] = ["log", "error", "warn", "debug", "verbose"]; // Gets a list of log levels equal to, or higher, than the supplied minimum level export function GetLogLevels(minLevel: string): LogLevel[] { - // If nothing was supplied, use default values - if (!minLevel) { - return undefined; - } + // If nothing was supplied, use default values + if (!minLevel) { + return undefined; + } - const logIndex = logLevels.indexOf(minLevel as LogLevel); - if (logIndex < 0) { - throw new Error(`Invalid log level: "${minLevel}" supplied in environment`); - } + const logIndex = logLevels.indexOf(minLevel as LogLevel); + if (logIndex < 0) { + throw new Error(`Invalid log level: "${minLevel}" supplied in environment`); + } - // We want to return all the HIGHER log levels and they are ranked high->low - const higherLevels = logLevels.slice(0, logIndex + 1); - return higherLevels; + // We want to return all the HIGHER log levels and they are ranked high->low + const higherLevels = logLevels.slice(0, logIndex + 1); + return higherLevels; } export function formatEmail(email: string | undefined): typeof email { - if (!email || typeof email !== "string") { - return email; - } + if (!email || typeof email !== "string") { + return email; + } - if (email.includes(" ") || !email.includes("@")) { - return email; - } + if (email.includes(" ") || !email.includes("@")) { + return email; + } - // Prefix default sender display name - return `OS2iot ${email}` -}; + // Prefix default sender display name + return `OS2iot ${email}`; +} diff --git a/src/helpers/fiware-token.helper.ts b/src/helpers/fiware-token.helper.ts index 7dc29302..7b29a139 100644 --- a/src/helpers/fiware-token.helper.ts +++ b/src/helpers/fiware-token.helper.ts @@ -4,86 +4,82 @@ import { Cache } from "cache-manager"; import { FiwareDataTargetConfiguration } from "../entities/interfaces/fiware-data-target-configuration.interface"; type TokenEndpointResponse = { - data: { - access_token: string; - expires_in: number; - }; + data: { + access_token: string; + expires_in: number; + }; }; export const CLIENT_SECRET_PROVIDER = "ClientSecretProvider"; export interface ClientSecretProvider { - getClientSecret(secretRef: string): Promise; - store(secret: string): Promise; + getClientSecret(secretRef: string): Promise; + store(secret: string): Promise; } @Injectable() export class PlainTextClientSecretProvider implements ClientSecretProvider { - getClientSecret(secretRef: string): Promise { - return Promise.resolve(secretRef); - } + getClientSecret(secretRef: string): Promise { + return Promise.resolve(secretRef); + } - store(secret: string): Promise { - return Promise.resolve(secret); - } + store(secret: string): Promise { + return Promise.resolve(secret); + } } @Injectable() export class AuthenticationTokenProvider { - private readonly logger = new Logger(AuthenticationTokenProvider.name); + private readonly logger = new Logger(AuthenticationTokenProvider.name); - constructor( - private httpService: HttpService, - @Inject(CLIENT_SECRET_PROVIDER) - private clientSecretProvider: ClientSecretProvider, - @Inject(CACHE_MANAGER) private cacheManager: Cache - ) {} + constructor( + private httpService: HttpService, + @Inject(CLIENT_SECRET_PROVIDER) + private clientSecretProvider: ClientSecretProvider, + @Inject(CACHE_MANAGER) private cacheManager: Cache + ) {} - async clearConfig(config: FiwareDataTargetConfiguration): Promise { - if (config.clientId) { - this.logger.debug( - `AuthenticationTokenProvider clearing token for ${config.clientId}` - ); - const key = config.clientId + config.updatedAt.getTime(); - return this.cacheManager.del(key); - } + async clearConfig(config: FiwareDataTargetConfiguration): Promise { + if (config.clientId) { + this.logger.debug(`AuthenticationTokenProvider clearing token for ${config.clientId}`); + const key = config.clientId + config.updatedAt.getTime(); + return this.cacheManager.del(key); } + } - async getToken(config: FiwareDataTargetConfiguration): Promise { - const key = config.clientId + config.updatedAt.getTime(); + async getToken(config: FiwareDataTargetConfiguration): Promise { + const key = config.clientId + config.updatedAt.getTime(); - const token = await this.cacheManager.get(key); - if (token) { - return token; - } else { - try { - const clientSecret = await this.clientSecretProvider.getClientSecret( - config.clientSecret - ); - const params = new URLSearchParams([ - ["grant_type", "client_credentials"], - ["client_id", config.clientId], - ["client_secret", clientSecret], - ]); - const { data }: TokenEndpointResponse = await this.httpService - .post(config.tokenEndpoint, params, { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - .toPromise(); + const token = await this.cacheManager.get(key); + if (token) { + return token; + } else { + try { + const clientSecret = await this.clientSecretProvider.getClientSecret(config.clientSecret); + const params = new URLSearchParams([ + ["grant_type", "client_credentials"], + ["client_id", config.clientId], + ["client_secret", clientSecret], + ]); + const { data }: TokenEndpointResponse = await this.httpService + .post(config.tokenEndpoint, params, { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + .toPromise(); - // NOTE: TTL offset include some time for clock differences between authentication server and local server + network delay - const ttlOffset = 30; - const ttl = data.expires_in - ttlOffset; - this.logger.debug( - `AuthenticationTokenProvider caching token for ${config.clientId} (expires in ${ttl} seconds)` - ); - await this.cacheManager.set(key, data.access_token, { ttl }); - return data.access_token; - } catch (err) { - this.logger.error(`AuthenticationTokenProvider got error ${err}`); - throw err; - } - } + // NOTE: TTL offset include some time for clock differences between authentication server and local server + network delay + const ttlOffset = 30; + const ttl = data.expires_in - ttlOffset; + this.logger.debug( + `AuthenticationTokenProvider caching token for ${config.clientId} (expires in ${ttl} seconds)` + ); + await this.cacheManager.set(key, data.access_token, { ttl }); + return data.access_token; + } catch (err) { + this.logger.error(`AuthenticationTokenProvider got error ${err}`); + throw err; + } } + } } diff --git a/src/helpers/iot-device.helper.ts b/src/helpers/iot-device.helper.ts index c71b6bc7..b5e2b408 100644 --- a/src/helpers/iot-device.helper.ts +++ b/src/helpers/iot-device.helper.ts @@ -7,10 +7,7 @@ import { SigFoxDeviceWithBackendDataDto } from "@dto/sigfox-device-with-backend- import { UpdateIoTDeviceDto } from "@dto/update-iot-device.dto"; import { IoTDevice } from "@entities/iot-device.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - ApplicationAccessScope, - checkIfUserHasAccessToApplication, -} from "./security-helper"; +import { ApplicationAccessScope, checkIfUserHasAccessToApplication } from "./security-helper"; import { CreateMqttExternalBrokerSettingsDto } from "@dto/create-mqtt-external-broker-settings.dto"; import { AuthenticationType } from "@enum/authentication-type.enum"; import { CreateMqttInternalBrokerSettingsDto } from "@dto/create-mqtt-internal-broker-settings.dto"; @@ -19,147 +16,130 @@ import { CreateMqttInternalBrokerSettingsDto } from "@dto/create-mqtt-internal-b * Iterate through the devices once, splitting it into a tuple with the data we want to log * @param response */ -export function buildIoTDeviceCreateUpdateAuditData( - response: IotDeviceBatchResponseDto[] -): { deviceIds: number[]; deviceNames: string[] } { - return response.reduce( - (res: { deviceIds: number[]; deviceNames: string[] }, device) => { - if (!device.data || device.error) { - return res; - } - device.data.id && res.deviceIds.push(device.data.id); - device.data.name && res.deviceNames.push(device.data.name); - return res; - }, - { deviceIds: [], deviceNames: [] } - ); +export function buildIoTDeviceCreateUpdateAuditData(response: IotDeviceBatchResponseDto[]): { + deviceIds: number[]; + deviceNames: string[]; +} { + return response.reduce( + (res: { deviceIds: number[]; deviceNames: string[] }, device) => { + if (!device.data || device.error) { + return res; + } + device.data.id && res.deviceIds.push(device.data.id); + device.data.name && res.deviceNames.push(device.data.name); + return res; + }, + { deviceIds: [], deviceNames: [] } + ); } export function ensureUpdatePayload( - validDevices: UpdateIoTDeviceBatchDto, - oldIotDevices: ( - | IoTDevice - | LoRaWANDeviceWithChirpstackDataDto - | SigFoxDeviceWithBackendDataDto - )[], - devicesNotFound: IotDeviceBatchResponseDto[], - req: AuthenticatedRequest + validDevices: UpdateIoTDeviceBatchDto, + oldIotDevices: (IoTDevice | LoRaWANDeviceWithChirpstackDataDto | SigFoxDeviceWithBackendDataDto)[], + devicesNotFound: IotDeviceBatchResponseDto[], + req: AuthenticatedRequest ): ( - previousValue: UpdateIoTDeviceDto[], - currentValue: UpdateIoTDeviceDto, - currentIndex: number, - array: UpdateIoTDeviceDto[] + previousValue: UpdateIoTDeviceDto[], + currentValue: UpdateIoTDeviceDto, + currentIndex: number, + array: UpdateIoTDeviceDto[] ) => UpdateIoTDeviceDto[] { - return (res: typeof validDevices["data"], updateDeviceDto) => { - const oldDevice = oldIotDevices.find( - oldDevice => oldDevice.id === updateDeviceDto.id - ); + return (res: (typeof validDevices)["data"], updateDeviceDto) => { + const oldDevice = oldIotDevices.find(oldDevice => oldDevice.id === updateDeviceDto.id); - if (!oldDevice) { - devicesNotFound.push({ - idMetadata: { - applicationId: updateDeviceDto.applicationId, - name: updateDeviceDto.name, - }, - error: { - message: ErrorCodes.IdDoesNotExists, - }, - }); - return res; - } + if (!oldDevice) { + devicesNotFound.push({ + idMetadata: { + applicationId: updateDeviceDto.applicationId, + name: updateDeviceDto.name, + }, + error: { + message: ErrorCodes.IdDoesNotExists, + }, + }); + return res; + } - checkIfUserHasAccessToApplication( - req, - oldDevice.application.id, - ApplicationAccessScope.Write - ); + checkIfUserHasAccessToApplication(req, oldDevice.application.id, ApplicationAccessScope.Write); - if (updateDeviceDto.applicationId !== oldDevice.application.id) { - // New application - checkIfUserHasAccessToApplication( - req, - updateDeviceDto.applicationId, - ApplicationAccessScope.Write - ); - } - res.push(updateDeviceDto); - return res; - }; + if (updateDeviceDto.applicationId !== oldDevice.application.id) { + // New application + checkIfUserHasAccessToApplication(req, updateDeviceDto.applicationId, ApplicationAccessScope.Write); + } + res.push(updateDeviceDto); + return res; + }; } export function isValidIoTDeviceMap(iotDeviceMap: CreateIoTDeviceMapDto): boolean { - return !iotDeviceMap.error && !!iotDeviceMap.iotDevice; + return !iotDeviceMap.error && !!iotDeviceMap.iotDevice; } -export function filterValidIotDeviceMaps( - iotDeviceMap: CreateIoTDeviceMapDto[] -): CreateIoTDeviceMapDto[] { - return iotDeviceMap.filter(map => !map.error && map.iotDevice); +export function filterValidIotDeviceMaps(iotDeviceMap: CreateIoTDeviceMapDto[]): CreateIoTDeviceMapDto[] { + return iotDeviceMap.filter(map => !map.error && map.iotDevice); } /** * @param dbIotDevices * @returns A new list of processed and failed devices */ -export function mapAllDevicesByProcessed( - dbIotDevices: IoTDevice[] -): ( - value: CreateIoTDeviceMapDto, - index: number, - array: CreateIoTDeviceMapDto[] +export function mapAllDevicesByProcessed(dbIotDevices: IoTDevice[]): ( + value: CreateIoTDeviceMapDto, + index: number, + array: CreateIoTDeviceMapDto[] ) => { - data: IoTDevice; - idMetadata: { name: string; applicationId: number }; - error: Omit; + data: IoTDevice; + idMetadata: { name: string; applicationId: number }; + error: Omit; } { - return iotDeviceMap => ({ - data: dbIotDevices.find(dbDevice => dbDevice.id === iotDeviceMap.iotDevice?.id), - idMetadata: { - name: iotDeviceMap.iotDeviceDto.name, - applicationId: iotDeviceMap.iotDeviceDto.applicationId, - }, - error: iotDeviceMap.error, - }); + return iotDeviceMap => ({ + data: dbIotDevices.find(dbDevice => dbDevice.id === iotDeviceMap.iotDevice?.id), + idMetadata: { + name: iotDeviceMap.iotDeviceDto.name, + applicationId: iotDeviceMap.iotDeviceDto.applicationId, + }, + error: iotDeviceMap.error, + }); } export function validateMQTTInternalBroker(settings: CreateMqttInternalBrokerSettingsDto) { - if (settings.authenticationType === undefined) { - throw new Error("Autentifikationstype er påkrevet"); - } - if ( - settings.authenticationType === AuthenticationType.PASSWORD && - (settings.mqttpassword === undefined || settings.mqttusername === undefined) - ) { - throw new Error("Brugernavn eller kodeord er ikke sat"); - } + if (settings.authenticationType === undefined) { + throw new Error("Autentifikationstype er påkrevet"); + } + if ( + settings.authenticationType === AuthenticationType.PASSWORD && + (settings.mqttpassword === undefined || settings.mqttusername === undefined) + ) { + throw new Error("Brugernavn eller kodeord er ikke sat"); + } } export function validateMQTTExternalBroker(settings: CreateMqttExternalBrokerSettingsDto) { - if ( - settings.mqtttopicname === undefined || - settings.mqttURL === undefined || - settings.mqttPort === undefined || - settings.authenticationType === undefined - ) { - throw new Error("Påkrævede felter til mqtt forbindelsen ikke sat korrekt"); - } + if ( + settings.mqtttopicname === undefined || + settings.mqttURL === undefined || + settings.mqttPort === undefined || + settings.authenticationType === undefined + ) { + throw new Error("Påkrævede felter til mqtt forbindelsen ikke sat korrekt"); + } - if ( - settings.authenticationType === AuthenticationType.PASSWORD && - (settings.mqttpassword === undefined || settings.mqttusername === undefined) - ) { - throw new Error("Brugernavn eller kodeord er ikke sat"); - } + if ( + settings.authenticationType === AuthenticationType.PASSWORD && + (settings.mqttpassword === undefined || settings.mqttusername === undefined) + ) { + throw new Error("Brugernavn eller kodeord er ikke sat"); + } - if ( - settings.authenticationType === AuthenticationType.CERTIFICATE && - (settings.caCertificate === undefined || - settings.deviceCertificate === undefined || - settings.deviceCertificateKey === undefined) - ) { - throw new Error("Et certifikat er ikke sat"); - } - if (settings.mqttPort < 0 || settings.mqttPort > 65535) { - throw new Error("Port skal være mellem 0 og 65535"); - } + if ( + settings.authenticationType === AuthenticationType.CERTIFICATE && + (settings.caCertificate === undefined || + settings.deviceCertificate === undefined || + settings.deviceCertificateKey === undefined) + ) { + throw new Error("Et certifikat er ikke sat"); + } + if (settings.mqttPort < 0 || settings.mqttPort > 65535) { + throw new Error("Port skal være mellem 0 og 65535"); + } } diff --git a/src/helpers/is-json-or-null.validator.ts b/src/helpers/is-json-or-null.validator.ts index 11fbe04f..cec6f846 100644 --- a/src/helpers/is-json-or-null.validator.ts +++ b/src/helpers/is-json-or-null.validator.ts @@ -1,29 +1,26 @@ import { registerDecorator, ValidationOptions } from "class-validator"; import { isJSON } from "validator"; -export function IsJSONOrNull( - property: string, - validationOptions?: ValidationOptions -) { - return function (object: unknown, propertyName: string): void { - if (!validationOptions?.message) { - validationOptions = { - ...validationOptions, - message: `${propertyName} must be a valid JSON string`, - }; - } +export function IsJSONOrNull(property: string, validationOptions?: ValidationOptions) { + return function (object: unknown, propertyName: string): void { + if (!validationOptions?.message) { + validationOptions = { + ...validationOptions, + message: `${propertyName} must be a valid JSON string`, + }; + } - registerDecorator({ - name: "IsJSONOrNull", - target: object.constructor, - propertyName: propertyName, - constraints: [property], - options: validationOptions, - validator: { - validate(value: unknown) { - return !value || isJSON(value as string) - }, - }, - }); - }; + registerDecorator({ + name: "IsJSONOrNull", + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: unknown) { + return !value || isJSON(value as string); + }, + }, + }); + }; } diff --git a/src/helpers/is-metadata-json.validator.ts b/src/helpers/is-metadata-json.validator.ts index 1c1e80e9..2d1e98bb 100644 --- a/src/helpers/is-metadata-json.validator.ts +++ b/src/helpers/is-metadata-json.validator.ts @@ -1,57 +1,57 @@ import { ErrorCodes } from "@enum/error-codes.enum"; import { - registerDecorator, - ValidationArguments, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, + registerDecorator, + ValidationArguments, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, } from "class-validator"; @ValidatorConstraint({ name: "isMetadataJson", async: false }) export class IsMetadataJsonConstraint implements ValidatorConstraintInterface { - private message = ""; + private message = ""; - public validate(value: string, args: ValidationArguments): boolean { - const [propertyName] = args.constraints; - this.message = `${propertyName} must be a valid list of metadata keys and values`; + public validate(value: string, args: ValidationArguments): boolean { + const [propertyName] = args.constraints; + this.message = `${propertyName} must be a valid list of metadata keys and values`; - if (typeof value !== "string") { - return false; - } - try { - const json = JSON.parse(value) as Record; + if (typeof value !== "string") { + return false; + } + try { + const json = JSON.parse(value) as Record; - for (const key of Object.keys(json)) { - if (typeof key !== "string" || key.trim() === "") { - this.message = ErrorCodes.InvalidKeyInKeyValuePair; - return false; - } - if (typeof json[key] !== "string" || json[key].trim() === "") { - this.message = ErrorCodes.InvalidValueInKeyValuePair; - return false; - } - } - } catch (error) { - return false; + for (const key of Object.keys(json)) { + if (typeof key !== "string" || key.trim() === "") { + this.message = ErrorCodes.InvalidKeyInKeyValuePair; + return false; } - - return true; + if (typeof json[key] !== "string" || json[key].trim() === "") { + this.message = ErrorCodes.InvalidValueInKeyValuePair; + return false; + } + } + } catch (error) { + return false; } - public defaultMessage(_args: ValidationArguments): string { - return this.message; - } + return true; + } + + public defaultMessage(_args: ValidationArguments): string { + return this.message; + } } export function IsMetadataJson(property: string, validationOptions?: ValidationOptions) { - return function (object: unknown, propertyName: string): void { - registerDecorator({ - name: "isMetadataJson", - target: object.constructor, - propertyName: propertyName, - constraints: [property], - options: validationOptions, - validator: IsMetadataJsonConstraint, - }); - }; + return function (object: unknown, propertyName: string): void { + registerDecorator({ + name: "isMetadataJson", + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: IsMetadataJsonConstraint, + }); + }; } diff --git a/src/helpers/is-not-blank.validator.ts b/src/helpers/is-not-blank.validator.ts index df569f3c..0a245e7b 100644 --- a/src/helpers/is-not-blank.validator.ts +++ b/src/helpers/is-not-blank.validator.ts @@ -1,18 +1,18 @@ import { registerDecorator, ValidationOptions } from "class-validator"; export function IsNotBlank(property: string, validationOptions?: ValidationOptions) { - return function (object: unknown, propertyName: string): void { - registerDecorator({ - name: "isNotBlank", - target: object.constructor, - propertyName: propertyName, - constraints: [property], - options: validationOptions, - validator: { - validate(value: any) { - return typeof value === "string" && value.trim().length > 0; - }, - }, - }); - }; + return function (object: unknown, propertyName: string): void { + registerDecorator({ + name: "isNotBlank", + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: any) { + return typeof value === "string" && value.trim().length > 0; + }, + }, + }); + }; } diff --git a/src/helpers/message-payload.helper.ts b/src/helpers/message-payload.helper.ts index 4c6465a0..e9dc7d66 100644 --- a/src/helpers/message-payload.helper.ts +++ b/src/helpers/message-payload.helper.ts @@ -1,82 +1,82 @@ import { hasProps, nameof } from "./type-helper"; export interface LoRaWANPayload { - rxInfo: LoRaWANSignalData[]; + rxInfo: LoRaWANSignalData[]; } export interface LoRaWANSignalData { - name: string; - rssi: number; - time: string; - snr: number; - location: { - altitude: number; - latitude: number; - longitude: number; - }; - uplinkID: string; - gatewayID: string; + name: string; + rssi: number; + time: string; + snr: number; + location: { + altitude: number; + latitude: number; + longitude: number; + }; + uplinkID: string; + gatewayID: string; } export interface SigFoxPayload { - duplicates: SigFoxSignalData[]; + duplicates: SigFoxSignalData[]; } /** * @see https://support.sigfox.com/docs/data-advanced */ export interface SigFoxSignalData { - rssi: number; - snr: number; + rssi: number; + snr: number; } export const isValidLoRaWANPayload = (payload: unknown): payload is LoRaWANPayload => { - return ( - payload && - typeof payload === "object" && - hasProps(payload, nameof("rxInfo")) && - isValidLoRaWANSignalData(payload.rxInfo) - ); + return ( + payload && + typeof payload === "object" && + hasProps(payload, nameof("rxInfo")) && + isValidLoRaWANSignalData(payload.rxInfo) + ); }; export const isValidLoRaWANSignalData = (rxInfo: unknown): rxInfo is LoRaWANSignalData[] => { - if (Array.isArray(rxInfo)) { - // TS narrows it to any[]. Issue: https://github.com/microsoft/TypeScript/issues/17002 - const unknownInfo = rxInfo as unknown[]; - return ( - typeof unknownInfo[0] === "object" && - hasProps(unknownInfo[0], nameof("rssi")) && - typeof unknownInfo[0].rssi === "number" && - hasProps(unknownInfo[0], nameof("snr")) && - typeof unknownInfo[0].snr === "number" - ); - } + if (Array.isArray(rxInfo)) { + // TS narrows it to any[]. Issue: https://github.com/microsoft/TypeScript/issues/17002 + const unknownInfo = rxInfo as unknown[]; + return ( + typeof unknownInfo[0] === "object" && + hasProps(unknownInfo[0], nameof("rssi")) && + typeof unknownInfo[0].rssi === "number" && + hasProps(unknownInfo[0], nameof("snr")) && + typeof unknownInfo[0].snr === "number" + ); + } - return false; + return false; }; export const isValidSigFoxPayload = (payload: unknown): payload is SigFoxPayload => { - return ( - payload && - typeof payload === "object" && - hasProps(payload, nameof("duplicates")) && - isValidSigFoxSignalData(payload.duplicates) - ); + return ( + payload && + typeof payload === "object" && + hasProps(payload, nameof("duplicates")) && + isValidSigFoxSignalData(payload.duplicates) + ); }; export const isValidSigFoxSignalData = (rxInfo: unknown): rxInfo is SigFoxSignalData[] => { - if (Array.isArray(rxInfo)) { - // TS narrows it to any[]. Issue: https://github.com/microsoft/TypeScript/issues/17002 - const unknownInfo = rxInfo as unknown[]; + if (Array.isArray(rxInfo)) { + // TS narrows it to any[]. Issue: https://github.com/microsoft/TypeScript/issues/17002 + const unknownInfo = rxInfo as unknown[]; - return ( - typeof unknownInfo[0] === "object" && - hasProps(unknownInfo[0], nameof("rssi")) && - typeof unknownInfo[0].rssi === "number" && - hasProps(unknownInfo[0], nameof("snr")) && - typeof unknownInfo[0].snr === "number" - ); - } + return ( + typeof unknownInfo[0] === "object" && + hasProps(unknownInfo[0], nameof("rssi")) && + typeof unknownInfo[0].rssi === "number" && + hasProps(unknownInfo[0], nameof("snr")) && + typeof unknownInfo[0].snr === "number" + ); + } - return false; + return false; }; diff --git a/src/helpers/optional-validator.ts b/src/helpers/optional-validator.ts index fc8153e0..c56e3f58 100644 --- a/src/helpers/optional-validator.ts +++ b/src/helpers/optional-validator.ts @@ -5,10 +5,10 @@ import { IsOptional } from "class-validator"; * Sets a property as optional on the swagger and controller level */ export const IsSwaggerOptional = (swaggerOptions?: ApiPropertyOptions): PropertyDecorator => { - return (propertyValue: unknown, propertyName: string): void => { - // Set as optional in the swagger document - ApiPropertyOptional(swaggerOptions)(propertyValue, propertyName); - // If no value is passed, then ignore all validators - IsOptional()(propertyValue, propertyName); - }; + return (propertyValue: unknown, propertyName: string): void => { + // Set as optional in the swagger document + ApiPropertyOptional(swaggerOptions)(propertyValue, propertyName); + // If no value is passed, then ignore all validators + IsOptional()(propertyValue, propertyName); + }; }; diff --git a/src/helpers/permission.helper.ts b/src/helpers/permission.helper.ts index 9c161610..f0f9c2f0 100644 --- a/src/helpers/permission.helper.ts +++ b/src/helpers/permission.helper.ts @@ -4,79 +4,52 @@ import { PermissionType } from "@enum/permission-type.enum"; import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; export abstract class PermissionCreator { - private static create( - name: string, - org?: Organization, - addNewApps = false - ): Permission { - const pm = new Permission(name, org, addNewApps); - return pm; - } - - static createByTypes( - name: string, - types: PermissionType[], - org?: Organization, - addNewApps = false - ): Permission { - const pm = new Permission(name, org, addNewApps); - pm.type = types.map(type => { - const entity = new PermissionTypeEntity(); - entity.type = type; - return entity; - }); - return pm; - } - - static createGlobalAdmin(): Permission { - const pm = this.create("GlobalAdmin"); - pm.type = [{ type: PermissionType.GlobalAdmin } as PermissionTypeEntity]; - return pm; - } - - static createRead(name: string, org?: Organization, addNewApps = false): Permission { - const pm = this.create(name, org, addNewApps); - - pm.type = [{ type: PermissionType.Read } as PermissionTypeEntity]; - return pm; - } - - static createApplicationAdmin( - name: string, - org?: Organization, - addNewApps = false - ): Permission { - const pm = this.create(name, org, addNewApps); - - pm.type = [ - { type: PermissionType.OrganizationApplicationAdmin } as PermissionTypeEntity, - ]; - return pm; - } - - static createUserAdmin( - name: string, - org?: Organization, - addNewApps = false - ): Permission { - const pm = this.create(name, org, addNewApps); - - pm.type = [ - { type: PermissionType.OrganizationUserAdmin } as PermissionTypeEntity, - ]; - return pm; - } - - static createGatewayAdmin( - name: string, - org?: Organization, - addNewApps = false - ): Permission { - const pm = this.create(name, org, addNewApps); - - pm.type = [ - { type: PermissionType.OrganizationGatewayAdmin } as PermissionTypeEntity, - ]; - return pm; - } + private static create(name: string, org?: Organization, addNewApps = false): Permission { + const pm = new Permission(name, org, addNewApps); + return pm; + } + + static createByTypes(name: string, types: PermissionType[], org?: Organization, addNewApps = false): Permission { + const pm = new Permission(name, org, addNewApps); + pm.type = types.map(type => { + const entity = new PermissionTypeEntity(); + entity.type = type; + return entity; + }); + return pm; + } + + static createGlobalAdmin(): Permission { + const pm = this.create("GlobalAdmin"); + pm.type = [{ type: PermissionType.GlobalAdmin } as PermissionTypeEntity]; + return pm; + } + + static createRead(name: string, org?: Organization, addNewApps = false): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [{ type: PermissionType.Read } as PermissionTypeEntity]; + return pm; + } + + static createApplicationAdmin(name: string, org?: Organization, addNewApps = false): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [{ type: PermissionType.OrganizationApplicationAdmin } as PermissionTypeEntity]; + return pm; + } + + static createUserAdmin(name: string, org?: Organization, addNewApps = false): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [{ type: PermissionType.OrganizationUserAdmin } as PermissionTypeEntity]; + return pm; + } + + static createGatewayAdmin(name: string, org?: Organization, addNewApps = false): Permission { + const pm = this.create(name, org, addNewApps); + + pm.type = [{ type: PermissionType.OrganizationGatewayAdmin } as PermissionTypeEntity]; + return pm; + } } diff --git a/src/helpers/phone-number.validator.ts b/src/helpers/phone-number.validator.ts index 1ebf9d2f..83a1e59a 100644 --- a/src/helpers/phone-number.validator.ts +++ b/src/helpers/phone-number.validator.ts @@ -2,29 +2,26 @@ import { registerDecorator, ValidationOptions } from "class-validator"; const phoneNumberRegex = /[-+0-9]{6,}/; -export function IsPhoneNumberString( - property: string, - validationOptions?: ValidationOptions -) { - return function (object: unknown, propertyName: string): void { - if (!validationOptions?.message) { - validationOptions = { - ...validationOptions, - message: `${propertyName} must be a valid phone number`, - }; - } +export function IsPhoneNumberString(property: string, validationOptions?: ValidationOptions) { + return function (object: unknown, propertyName: string): void { + if (!validationOptions?.message) { + validationOptions = { + ...validationOptions, + message: `${propertyName} must be a valid phone number`, + }; + } - registerDecorator({ - name: "isPhoneNumberString", - target: object.constructor, - propertyName: propertyName, - constraints: [property], - options: validationOptions, - validator: { - validate(value: unknown) { - return typeof value === "string" && phoneNumberRegex.test(value); - }, - }, - }); - }; + registerDecorator({ + name: "isPhoneNumberString", + target: object.constructor, + propertyName: propertyName, + constraints: [property], + options: validationOptions, + validator: { + validate(value: unknown) { + return typeof value === "string" && phoneNumberRegex.test(value); + }, + }, + }); + }; } diff --git a/src/helpers/record.helper.ts b/src/helpers/record.helper.ts index 4726a341..4b51fb3c 100644 --- a/src/helpers/record.helper.ts +++ b/src/helpers/record.helper.ts @@ -1,27 +1,22 @@ type KeyValue> = { - key: keyof T; - value: T[keyof T]; + key: keyof T; + value: T[keyof T]; }; -export const recordToEntries = >( - record: T -): KeyValue[] => { - return Object.keys(record) - .filter(entry => isNaN(Number(entry))) - .map((key: keyof typeof record) => ({ - key, - value: record[key], - })); +export const recordToEntries = >(record: T): KeyValue[] => { + return Object.keys(record) + .filter(entry => isNaN(Number(entry))) + .map((key: keyof typeof record) => ({ + key, + value: record[key], + })); }; -export const findValuesInRecord = >( - record: T, - values: string[] -): string[] => { - return recordToEntries(record).reduce((res: typeof values, { value }) => { - if (values.includes(value)) { - res.push(value); - } - return res; - }, []); +export const findValuesInRecord = >(record: T, values: string[]): string[] => { + return recordToEntries(record).reduce((res: typeof values, { value }) => { + if (values.includes(value)) { + res.push(value); + } + return res; + }, []); }; diff --git a/src/helpers/security-helper.ts b/src/helpers/security-helper.ts index 195f87e1..6ba3d16b 100644 --- a/src/helpers/security-helper.ts +++ b/src/helpers/security-helper.ts @@ -6,119 +6,111 @@ import * as _ from "lodash"; import { PermissionTypeEntity } from "@entities/permissions/permission-type.entity"; export enum OrganizationAccessScope { - ApplicationRead, - ApplicationWrite, - GatewayWrite, - UserAdministrationRead, - UserAdministrationWrite, + ApplicationRead, + ApplicationWrite, + GatewayWrite, + UserAdministrationRead, + UserAdministrationWrite, } export enum ApplicationAccessScope { - Read, - Write, + Read, + Write, } export function checkIfUserHasAccessToOrganization( - req: AuthenticatedRequest, - organizationId: number, - scope: OrganizationAccessScope + req: AuthenticatedRequest, + organizationId: number, + scope: OrganizationAccessScope ): void { - if (!Number.isInteger(+organizationId)) return; - - let allowedOrganizations: number[] = []; - - switch (scope) { - case OrganizationAccessScope.ApplicationRead: - allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); - break; - case OrganizationAccessScope.ApplicationWrite: - allowedOrganizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); - break; - case OrganizationAccessScope.GatewayWrite: - allowedOrganizations = req.user.permissions.getAllOrganizationsWithGatewayAdmin(); - break; - case OrganizationAccessScope.UserAdministrationRead: - allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead(); - break; - case OrganizationAccessScope.UserAdministrationWrite: - allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); - break; - default: - // Should never happen - throw new BadRequestException("Bad organization access scope"); - } - - checkIfGlobalAdminOrInList(req, allowedOrganizations, organizationId); + if (!Number.isInteger(+organizationId)) return; + + let allowedOrganizations: number[] = []; + + switch (scope) { + case OrganizationAccessScope.ApplicationRead: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastApplicationRead(); + break; + case OrganizationAccessScope.ApplicationWrite: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); + break; + case OrganizationAccessScope.GatewayWrite: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithGatewayAdmin(); + break; + case OrganizationAccessScope.UserAdministrationRead: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithAtLeastUserAdminRead(); + break; + case OrganizationAccessScope.UserAdministrationWrite: + allowedOrganizations = req.user.permissions.getAllOrganizationsWithUserAdmin(); + break; + default: + // Should never happen + throw new BadRequestException("Bad organization access scope"); + } + + checkIfGlobalAdminOrInList(req, allowedOrganizations, organizationId); } export function checkIfUserHasAccessToApplication( - req: AuthenticatedRequest, - applicationId: number, - scope: ApplicationAccessScope + req: AuthenticatedRequest, + applicationId: number, + scope: ApplicationAccessScope ): void { - if (!Number.isInteger(applicationId)) return; - - let allowedOrganizations: number[] = []; - - switch (scope) { - case ApplicationAccessScope.Read: - allowedOrganizations = req.user.permissions.getAllApplicationsWithAtLeastRead(); - break; - case ApplicationAccessScope.Write: - allowedOrganizations = req.user.permissions.getAllApplicationsWithAdmin(); - break; - default: - // Should never happen - throw new BadRequestException("Bad application access scope"); - } - - checkIfGlobalAdminOrInList(req, allowedOrganizations, applicationId); + if (!Number.isInteger(applicationId)) return; + + let allowedOrganizations: number[] = []; + + switch (scope) { + case ApplicationAccessScope.Read: + allowedOrganizations = req.user.permissions.getAllApplicationsWithAtLeastRead(); + break; + case ApplicationAccessScope.Write: + allowedOrganizations = req.user.permissions.getAllApplicationsWithAdmin(); + break; + default: + // Should never happen + throw new BadRequestException("Bad application access scope"); + } + + checkIfGlobalAdminOrInList(req, allowedOrganizations, applicationId); } export function checkIfUserIsGlobalAdmin(req: AuthenticatedRequest): void { - if (!req.user.permissions.isGlobalAdmin) { - throw new ForbiddenException(); - } + if (!req.user.permissions.isGlobalAdmin) { + throw new ForbiddenException(); + } } -function checkIfGlobalAdminOrInList( - req: AuthenticatedRequest, - list: number[], - id: number -): void { - if (req.user.permissions.isGlobalAdmin) { - return; - } +function checkIfGlobalAdminOrInList(req: AuthenticatedRequest, list: number[], id: number): void { + if (req.user.permissions.isGlobalAdmin) { + return; + } - if (!_.includes(list, +id)) { - throw new ForbiddenException(); - } + if (!_.includes(list, +id)) { + throw new ForbiddenException(); + } } export function isOrganizationPermission(p: Permission): p is Permission { - return [ - PermissionType.OrganizationUserAdmin, - PermissionType.OrganizationApplicationAdmin, - PermissionType.OrganizationGatewayAdmin, - PermissionType.Read, - ].some(orgPermission => p.type.some(({ type }) => type === orgPermission)); + return [ + PermissionType.OrganizationUserAdmin, + PermissionType.OrganizationApplicationAdmin, + PermissionType.OrganizationGatewayAdmin, + PermissionType.Read, + ].some(orgPermission => p.type.some(({ type }) => type === orgPermission)); } -export function isOrganizationApplicationPermission(p: { - type: PermissionTypeEntity[]; -}): p is Permission { - return p.type.some( - ({ type }) => - type === PermissionType.Read || - type === PermissionType.OrganizationApplicationAdmin - ); +export function isOrganizationApplicationPermission(p: { type: PermissionTypeEntity[] }): p is Permission { + return p.type.some( + ({ type }) => type === PermissionType.Read || type === PermissionType.OrganizationApplicationAdmin + ); } export function isPermissionType( - p: { - type: PermissionTypeEntity[]; - }, - targetType: PermissionType + p: { + type: PermissionTypeEntity[]; + }, + targetType: PermissionType ): p is Permission { - return p.type.some(({ type }) => type === targetType); + return p.type.some(({ type }) => type === targetType); } diff --git a/src/helpers/string-to-number-validator.ts b/src/helpers/string-to-number-validator.ts index 809e20bf..7ca95c90 100644 --- a/src/helpers/string-to-number-validator.ts +++ b/src/helpers/string-to-number-validator.ts @@ -5,12 +5,12 @@ import { IsNumber } from "class-validator"; * Checks if a value can be converted to a number */ export const StringToNumber = (): PropertyDecorator => { - return (propertyValue: unknown, propertyName: string): void => { - // Cast the value to a number - Type(() => Number)(propertyValue, propertyName); - // Validate whether the value is a number - IsNumber()(propertyValue, propertyName); - }; + return (propertyValue: unknown, propertyName: string): void => { + // Cast the value to a number + Type(() => Number)(propertyValue, propertyName); + // Validate whether the value is a number + IsNumber()(propertyValue, propertyName); + }; }; /** @@ -20,9 +20,9 @@ export const StringToNumber = (): PropertyDecorator => { * @param value */ export const NullableStringToNumber = (value: unknown): number | null => { - if (value === null || value === "null") { - return null; - } + if (value === null || value === "null") { + return null; + } - return Number(value); + return Number(value); }; diff --git a/src/helpers/string.helper.ts b/src/helpers/string.helper.ts index 09d2fd06..9558c5d7 100644 --- a/src/helpers/string.helper.ts +++ b/src/helpers/string.helper.ts @@ -1,4 +1,4 @@ export function isNullOrWhitespace(str: string) { - // Using == for nullish check for both null and undefined - return str == null || str.trim() === ""; + // Using == for nullish check for both null and undefined + return str == null || str.trim() === ""; } diff --git a/src/helpers/type-helper.ts b/src/helpers/type-helper.ts index 3bf70083..c435bf95 100644 --- a/src/helpers/type-helper.ts +++ b/src/helpers/type-helper.ts @@ -1,16 +1,12 @@ // eslint-disable-next-line @typescript-eslint/ban-types -type HasProp = T & - { [P in K]: unknown }; +type HasProp = T & { [P in K]: unknown }; /* * Type guard to ensure that an arbitrary object has a given properties */ // eslint-disable-next-line @typescript-eslint/ban-types -export const hasProps = ( - obj: T, - ...props: K[] -): obj is HasProp => { - return props.every(prop => prop in obj); +export const hasProps = (obj: T, ...props: K[]): obj is HasProp => { + return props.every(prop => prop in obj); }; export const nameof = (name: Extract): typeof name => name; diff --git a/src/loaders/nestjs.ts b/src/loaders/nestjs.ts index f6544ce7..3ebeda75 100644 --- a/src/loaders/nestjs.ts +++ b/src/loaders/nestjs.ts @@ -1,9 +1,9 @@ import { - BadRequestException, - INestApplication, - ValidationPipe, - Logger as BuiltInLogger, - LogLevel, + BadRequestException, + INestApplication, + ValidationPipe, + Logger as BuiltInLogger, + LogLevel, } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; import * as compression from "compression"; @@ -12,37 +12,32 @@ import { ExpressAdapter } from "@nestjs/platform-express/adapters/express-adapte import * as cookieParser from "cookie-parser"; import { Express } from "express"; - export async function setupNestJs( - config: { - NEST_PORT: number; - API_PREFIX: string; - CURRENT_VERSION_PREFIX: string; - SWAGGER_PREFIX: string; - LOG_LEVELS: LogLevel[]; - }, - server: Express + config: { + NEST_PORT: number; + API_PREFIX: string; + CURRENT_VERSION_PREFIX: string; + SWAGGER_PREFIX: string; + LOG_LEVELS: LogLevel[]; + }, + server: Express ): Promise { - const app = await NestFactory.create(AppModule, new ExpressAdapter(server), { logger: config.LOG_LEVELS }); - app.setGlobalPrefix(config.CURRENT_VERSION_PREFIX); - app.useGlobalPipes( - new ValidationPipe({ - exceptionFactory: errors => { - // Throw exception if any controller validation fails. Will also fail if a property has a type - // but doesn't have the proper decorator (like @IsNumber() for a number property) - return new BadRequestException(errors); - }, - }) - ); - app.enableCors(); - app.use(compression()); - app.use(cookieParser()); + const app = await NestFactory.create(AppModule, new ExpressAdapter(server), { logger: config.LOG_LEVELS }); + app.setGlobalPrefix(config.CURRENT_VERSION_PREFIX); + app.useGlobalPipes( + new ValidationPipe({ + exceptionFactory: errors => { + // Throw exception if any controller validation fails. Will also fail if a property has a type + // but doesn't have the proper decorator (like @IsNumber() for a number property) + return new BadRequestException(errors); + }, + }) + ); + app.enableCors(); + app.use(compression()); + app.use(cookieParser()); - BuiltInLogger.log( - `Kafka: ${process.env.KAFKA_HOSTNAME || "localhost"}:${ - process.env.KAFKA_PORT || "9092" - }` - ); + BuiltInLogger.log(`Kafka: ${process.env.KAFKA_HOSTNAME || "localhost"}:${process.env.KAFKA_PORT || "9092"}`); - return app; + return app; } diff --git a/src/loaders/swagger.ts b/src/loaders/swagger.ts index 336b56e6..a5bd253f 100644 --- a/src/loaders/swagger.ts +++ b/src/loaders/swagger.ts @@ -2,14 +2,14 @@ import { INestApplication } from "@nestjs/common"; import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger"; export function setupSwagger(app: INestApplication, SWAGGER_PREFIX: string): void { - const options = new DocumentBuilder() - .setTitle("OS2IoT - Backend") - .setDescription("The back-end for OS2IoT") - .setVersion("1.0") - .addTag("os2iot") - .addApiKey({ type: "apiKey", name: "X-API-KEY", in: "header" }, "X-API-KEY") - .addBearerAuth({ type: "http", scheme: "bearer", bearerFormat: "JWT" }) - .build(); - const document = SwaggerModule.createDocument(app, options); - SwaggerModule.setup(SWAGGER_PREFIX, app, document); + const options = new DocumentBuilder() + .setTitle("OS2IoT - Backend") + .setDescription("The back-end for OS2IoT") + .setVersion("1.0") + .addTag("os2iot") + .addApiKey({ type: "apiKey", name: "X-API-KEY", in: "header" }, "X-API-KEY") + .addBearerAuth({ type: "http", scheme: "bearer", bearerFormat: "JWT" }) + .build(); + const document = SwaggerModule.createDocument(app, options); + SwaggerModule.setup(SWAGGER_PREFIX, app, document); } diff --git a/src/main.ts b/src/main.ts index 526716ad..5fd10f55 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,36 +10,36 @@ import { setupSwagger } from "@loaders/swagger"; import configuration from "@config/configuration"; async function bootstrap() { - // Load .env file as environment before startup. - dotenv.config({ path: "../.env", debug: true }); + // Load .env file as environment before startup. + dotenv.config({ path: "../.env", debug: true }); - const config = { - NEST_PORT: 3000, - API_PREFIX: "/api", - CURRENT_VERSION_PREFIX: "/api" + "/v1", - SWAGGER_PREFIX: "/api" + "/v1" + "/docs", - LOG_LEVELS: configuration()["logLevels"], - }; - const server = express(); + const config = { + NEST_PORT: 3000, + API_PREFIX: "/api", + CURRENT_VERSION_PREFIX: "/api" + "/v1", + SWAGGER_PREFIX: "/api" + "/v1" + "/docs", + LOG_LEVELS: configuration()["logLevels"], + }; + const server = express(); - const app = await setupNestJs(config, server); - setupSwagger(app, config.SWAGGER_PREFIX); + const app = await setupNestJs(config, server); + setupSwagger(app, config.SWAGGER_PREFIX); - BuiltInLogger.debug(`BaseUrl: '${configuration()["backend"]["baseurl"]}'`, "Kombit"); - BuiltInLogger.debug(`EntryPoint: '${configuration()["kombit"]["entryPoint"]}'`, "Kombit"); + BuiltInLogger.debug(`BaseUrl: '${configuration()["backend"]["baseurl"]}'`, "Kombit"); + BuiltInLogger.debug(`EntryPoint: '${configuration()["kombit"]["entryPoint"]}'`, "Kombit"); - // The .listen call must happen after swagger is setup. - // await app.listen(config.NEST_PORT); - await app.init(); - const httpServer = http.createServer(server).listen(3000); - try { - const httpsOptions = { - key: fs.readFileSync("../secrets/private.key"), - cert: fs.readFileSync("../secrets/publiccert.crt"), - }; - https.createServer(httpsOptions, server).listen(8443); - } catch (err) { - BuiltInLogger.log("Could not setup https, skipping."); - } + // The .listen call must happen after swagger is setup. + // await app.listen(config.NEST_PORT); + await app.init(); + const httpServer = http.createServer(server).listen(3000); + try { + const httpsOptions = { + key: fs.readFileSync("../secrets/private.key"), + cert: fs.readFileSync("../secrets/publiccert.crt"), + }; + https.createServer(httpsOptions, server).listen(8443); + } catch (err) { + BuiltInLogger.log("Could not setup https, skipping."); + } } void bootstrap(); diff --git a/src/modules/api-key-info/api-key-info.module.ts b/src/modules/api-key-info/api-key-info.module.ts index 15a779c4..1580837d 100644 --- a/src/modules/api-key-info/api-key-info.module.ts +++ b/src/modules/api-key-info/api-key-info.module.ts @@ -5,9 +5,9 @@ import { forwardRef, Module } from "@nestjs/common"; import { ApiKeyInfoService } from "@services/api-key-info/api-key-info.service"; @Module({ - imports: [SharedModule, forwardRef(() => OrganizationModule)], - providers: [ApiKeyInfoService], - exports: [ApiKeyInfoService], - controllers: [ApiKeyInfoController], + imports: [SharedModule, forwardRef(() => OrganizationModule)], + providers: [ApiKeyInfoService], + exports: [ApiKeyInfoService], + controllers: [ApiKeyInfoController], }) export class ApiKeyInfoModule {} diff --git a/src/modules/api-key-management/api-key.module.ts b/src/modules/api-key-management/api-key.module.ts index fee963af..52afe516 100644 --- a/src/modules/api-key-management/api-key.module.ts +++ b/src/modules/api-key-management/api-key.module.ts @@ -6,13 +6,9 @@ import { forwardRef, Module } from "@nestjs/common"; import { ApiKeyService } from "@services/api-key-management/api-key.service"; @Module({ - imports: [ - SharedModule, - forwardRef(() => PermissionModule), - forwardRef(() => OrganizationModule), - ], - controllers: [ApiKeyController], - providers: [ApiKeyService], - exports: [ApiKeyService], + imports: [SharedModule, forwardRef(() => PermissionModule), forwardRef(() => OrganizationModule)], + controllers: [ApiKeyController], + providers: [ApiKeyService], + exports: [ApiKeyService], }) export class ApiKeyModule {} diff --git a/src/modules/app.module.ts b/src/modules/app.module.ts index e01db7fd..4e3d6123 100644 --- a/src/modules/app.module.ts +++ b/src/modules/app.module.ts @@ -37,64 +37,64 @@ import { NewKombitCreationModule } from "./user-management/new-kombit-creation.m import { InternalMqttListenerModule } from "@modules/device-integrations/internal-mqtt-listener.module"; @Module({ - imports: [ - ConfigModule.forRoot({ - load: [configuration], - }), - TypeOrmModule.forRootAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: (configService: ConfigService) => ({ - type: "postgres", - host: configService.get("database.host"), - port: configService.get("database.port"), - username: configService.get("database.username"), - password: configService.get("database.password"), - database: "os2iot", - // Don't sync database 1-1 with code. Make migrations necessary - synchronize: false, - logging: false, - autoLoadEntities: true, - retryAttempts: 0, - maxQueryExecutionTime: 1000, // Log queries slower than 1000 ms - ssl: configService.get("database.ssl"), - }), - }), - KafkaModule, - ScheduleModule.forRoot(), - HttpModule, - ApplicationModule, - IoTDeviceModule, - DeviceModelModule, - DataTargetModule, - DataTargetKafkaModule, - DataTargetSenderModule, - ReceiveDataModule, - ChirpstackAdministrationModule, - PayloadDecoderModule, - IoTDevicePayloadDecoderDataTargetConnectionModule, - ChirpstackMqttListenerModule, - InternalMqttListenerModule, - PayloadDecoderKafkaModule, - DefaultModule, - AuthModule, - OrganizationModule, - PermissionModule, - SigFoxListenerModule, - SigFoxAdministrationModule, - SigFoxGroupModule, - SigfoxDeviceTypeModule, - SigfoxContractModule, - SigfoxDeviceModule, - SearchModule, - TestPayloadDecoderModule, - OpenDataDkSharingModule, - MulticastModule, - IoTLoRaWANDeviceModule, - ApiKeyInfoModule, - NewKombitCreationModule, - ], - controllers: [], - providers: [], + imports: [ + ConfigModule.forRoot({ + load: [configuration], + }), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + type: "postgres", + host: configService.get("database.host"), + port: configService.get("database.port"), + username: configService.get("database.username"), + password: configService.get("database.password"), + database: "os2iot", + // Don't sync database 1-1 with code. Make migrations necessary + synchronize: false, + logging: false, + autoLoadEntities: true, + retryAttempts: 0, + maxQueryExecutionTime: 1000, // Log queries slower than 1000 ms + ssl: configService.get("database.ssl"), + }), + }), + KafkaModule, + ScheduleModule.forRoot(), + HttpModule, + ApplicationModule, + IoTDeviceModule, + DeviceModelModule, + DataTargetModule, + DataTargetKafkaModule, + DataTargetSenderModule, + ReceiveDataModule, + ChirpstackAdministrationModule, + PayloadDecoderModule, + IoTDevicePayloadDecoderDataTargetConnectionModule, + ChirpstackMqttListenerModule, + InternalMqttListenerModule, + PayloadDecoderKafkaModule, + DefaultModule, + AuthModule, + OrganizationModule, + PermissionModule, + SigFoxListenerModule, + SigFoxAdministrationModule, + SigFoxGroupModule, + SigfoxDeviceTypeModule, + SigfoxContractModule, + SigfoxDeviceModule, + SearchModule, + TestPayloadDecoderModule, + OpenDataDkSharingModule, + MulticastModule, + IoTLoRaWANDeviceModule, + ApiKeyInfoModule, + NewKombitCreationModule, + ], + controllers: [], + providers: [], }) export class AppModule {} diff --git a/src/modules/data-management/device-integration-persistence.module.ts b/src/modules/data-management/device-integration-persistence.module.ts index f0149d43..4b80fc7c 100644 --- a/src/modules/data-management/device-integration-persistence.module.ts +++ b/src/modules/data-management/device-integration-persistence.module.ts @@ -5,7 +5,7 @@ import { Module } from "@nestjs/common"; import { DeviceIntegrationPersistenceService } from "@services/data-management/device-integration-persistence.service"; @Module({ - imports: [SharedModule, IoTDeviceModule, HttpModule], - providers: [DeviceIntegrationPersistenceService], + imports: [SharedModule, IoTDeviceModule, HttpModule], + providers: [DeviceIntegrationPersistenceService], }) export class DeviceIntegrationPersistenceModule {} diff --git a/src/modules/data-management/gateway-persistence.module.ts b/src/modules/data-management/gateway-persistence.module.ts index fe9bb710..5d8bce3a 100644 --- a/src/modules/data-management/gateway-persistence.module.ts +++ b/src/modules/data-management/gateway-persistence.module.ts @@ -6,12 +6,8 @@ import { ConfigModule } from "@nestjs/config"; import { GatewayPersistenceService } from "@services/data-management/gateway-persistence.service"; @Module({ - imports: [ - SharedModule, - ConfigModule.forRoot({ load: [configuration] }), - LoRaWANGatewayModule, - ], - exports: [], - providers: [GatewayPersistenceService], + imports: [SharedModule, ConfigModule.forRoot({ load: [configuration] }), LoRaWANGatewayModule], + exports: [], + providers: [GatewayPersistenceService], }) export class GatewayPersistenceModule {} diff --git a/src/modules/data-management/payload-decoder-kafka.module.ts b/src/modules/data-management/payload-decoder-kafka.module.ts index 80c21c46..c5524849 100644 --- a/src/modules/data-management/payload-decoder-kafka.module.ts +++ b/src/modules/data-management/payload-decoder-kafka.module.ts @@ -13,19 +13,19 @@ import { PayloadDecoderListenerService } from "@services/data-management/payload import { ChirpstackAdministrationModule } from "@modules/device-integrations/chirpstack-administration.module"; @Module({ - imports: [ - SharedModule, - KafkaModule, - IoTDevicePayloadDecoderDataTargetConnectionModule, - IoTDeviceModule, - DataTargetModule, - PayloadDecoderModule, - HttpModule, - ApplicationModule, - PayloadDecoderExecutorModuleModule, - ChirpstackAdministrationModule, - ], - controllers: [PayloadDecoderController], - providers: [PayloadDecoderListenerService], + imports: [ + SharedModule, + KafkaModule, + IoTDevicePayloadDecoderDataTargetConnectionModule, + IoTDeviceModule, + DataTargetModule, + PayloadDecoderModule, + HttpModule, + ApplicationModule, + PayloadDecoderExecutorModuleModule, + ChirpstackAdministrationModule, + ], + controllers: [PayloadDecoderController], + providers: [PayloadDecoderListenerService], }) export class PayloadDecoderKafkaModule {} diff --git a/src/modules/data-target/data-target-fiware-sender.module.ts b/src/modules/data-target/data-target-fiware-sender.module.ts index 641cdd73..e5394e80 100644 --- a/src/modules/data-target/data-target-fiware-sender.module.ts +++ b/src/modules/data-target/data-target-fiware-sender.module.ts @@ -1,24 +1,28 @@ import { HttpModule } from "@nestjs/axios"; import { CacheModule, Module } from "@nestjs/common"; import { FiwareDataTargetService } from "@services/data-targets/fiware-data-target.service"; -import { AuthenticationTokenProvider, CLIENT_SECRET_PROVIDER, PlainTextClientSecretProvider } from "../../helpers/fiware-token.helper"; +import { + AuthenticationTokenProvider, + CLIENT_SECRET_PROVIDER, + PlainTextClientSecretProvider, +} from "../../helpers/fiware-token.helper"; @Module({ - imports: [HttpModule, CacheModule.register()], - providers: [ - FiwareDataTargetService, - AuthenticationTokenProvider, - { - provide: CLIENT_SECRET_PROVIDER, - useClass: PlainTextClientSecretProvider, - }, - ], - exports: [ - FiwareDataTargetService, - { - provide: CLIENT_SECRET_PROVIDER, - useClass: PlainTextClientSecretProvider, - }, - ], + imports: [HttpModule, CacheModule.register()], + providers: [ + FiwareDataTargetService, + AuthenticationTokenProvider, + { + provide: CLIENT_SECRET_PROVIDER, + useClass: PlainTextClientSecretProvider, + }, + ], + exports: [ + FiwareDataTargetService, + { + provide: CLIENT_SECRET_PROVIDER, + useClass: PlainTextClientSecretProvider, + }, + ], }) export class DataTargetFiwareSenderModule {} diff --git a/src/modules/data-target/data-target-kafka.module.ts b/src/modules/data-target/data-target-kafka.module.ts index b49e4bd0..a2ebe582 100644 --- a/src/modules/data-target/data-target-kafka.module.ts +++ b/src/modules/data-target/data-target-kafka.module.ts @@ -14,20 +14,20 @@ import { DataTargetKafkaListenerService } from "@services/data-targets/data-targ import { DataTargetFiwareSenderModule } from "./data-target-fiware-sender.module"; @Module({ - imports: [ - SharedModule, - HttpModule, - KafkaModule, - DataTargetSenderModule, - DataTargetFiwareSenderModule, - DeviceIntegrationPersistenceModule, - IoTDeviceModule, - ChirpstackAdministrationModule, - IoTDevicePayloadDecoderDataTargetConnectionModule, - ApplicationModule, - DataTargetModule, - GatewayPersistenceModule, - ], - providers: [DataTargetKafkaListenerService], + imports: [ + SharedModule, + HttpModule, + KafkaModule, + DataTargetSenderModule, + DataTargetFiwareSenderModule, + DeviceIntegrationPersistenceModule, + IoTDeviceModule, + ChirpstackAdministrationModule, + IoTDevicePayloadDecoderDataTargetConnectionModule, + ApplicationModule, + DataTargetModule, + GatewayPersistenceModule, + ], + providers: [DataTargetKafkaListenerService], }) export class DataTargetKafkaModule {} diff --git a/src/modules/data-target/data-target-sender.module.ts b/src/modules/data-target/data-target-sender.module.ts index 712cbd9f..88531be9 100644 --- a/src/modules/data-target/data-target-sender.module.ts +++ b/src/modules/data-target/data-target-sender.module.ts @@ -4,8 +4,8 @@ import { HttpPushDataTargetService } from "@services/data-targets/http-push-data import { MqttDataTargetService } from "@services/data-targets/mqtt-data-target.service"; @Module({ - imports: [HttpModule], - providers: [HttpPushDataTargetService, MqttDataTargetService], - exports: [HttpPushDataTargetService, MqttDataTargetService], + imports: [HttpModule], + providers: [HttpPushDataTargetService, MqttDataTargetService], + exports: [HttpPushDataTargetService, MqttDataTargetService], }) export class DataTargetSenderModule {} diff --git a/src/modules/default.module.ts b/src/modules/default.module.ts index b02d5c50..4ba3d1c6 100644 --- a/src/modules/default.module.ts +++ b/src/modules/default.module.ts @@ -4,7 +4,7 @@ import { DefaultController } from "@admin-controller/default.controller"; import { HealthCheckModule } from "@modules/health-check.module"; @Module({ - imports: [HealthCheckModule], - controllers: [DefaultController], + imports: [HealthCheckModule], + controllers: [DefaultController], }) export class DefaultModule {} diff --git a/src/modules/device-integrations/chirpstack-administration.module.ts b/src/modules/device-integrations/chirpstack-administration.module.ts index e006a566..2cd0d135 100644 --- a/src/modules/device-integrations/chirpstack-administration.module.ts +++ b/src/modules/device-integrations/chirpstack-administration.module.ts @@ -13,15 +13,15 @@ import { GenericChirpstackConfigurationService } from "@services/chirpstack/gene import { OrganizationModule } from "@modules/user-management/organization.module"; @Module({ - controllers: [ChirpstackGatewayController, DeviceProfileController], - imports: [SharedModule, HttpModule, OrganizationModule, ConfigModule.forRoot({ load: [configuration] })], - providers: [ - GenericChirpstackConfigurationService, - ChirpstackGatewayService, - DeviceProfileService, - ChirpstackDeviceService, - ApplicationChirpstackService, - ], - exports: [ChirpstackDeviceService, ChirpstackGatewayService, DeviceProfileService, ApplicationChirpstackService], + controllers: [ChirpstackGatewayController, DeviceProfileController], + imports: [SharedModule, HttpModule, OrganizationModule, ConfigModule.forRoot({ load: [configuration] })], + providers: [ + GenericChirpstackConfigurationService, + ChirpstackGatewayService, + DeviceProfileService, + ChirpstackDeviceService, + ApplicationChirpstackService, + ], + exports: [ChirpstackDeviceService, ChirpstackGatewayService, DeviceProfileService, ApplicationChirpstackService], }) export class ChirpstackAdministrationModule {} diff --git a/src/modules/device-integrations/chirpstack-mqtt-listener.module.ts b/src/modules/device-integrations/chirpstack-mqtt-listener.module.ts index 4e1972ef..8e2fdb96 100644 --- a/src/modules/device-integrations/chirpstack-mqtt-listener.module.ts +++ b/src/modules/device-integrations/chirpstack-mqtt-listener.module.ts @@ -5,8 +5,8 @@ import { IoTDeviceModule } from "@modules/device-management/iot-device.module"; import { ChirpstackMQTTListenerService } from "@services/data-management/chirpstack-mqtt-listener.service"; @Module({ - imports: [ReceiveDataModule, IoTDeviceModule], - providers: [ChirpstackMQTTListenerService], - exports: [ChirpstackMQTTListenerService], + imports: [ReceiveDataModule, IoTDeviceModule], + providers: [ChirpstackMQTTListenerService], + exports: [ChirpstackMQTTListenerService], }) export class ChirpstackMqttListenerModule {} diff --git a/src/modules/device-integrations/internal-mqtt-listener.module.ts b/src/modules/device-integrations/internal-mqtt-listener.module.ts index e0a17147..d3e2ffaa 100644 --- a/src/modules/device-integrations/internal-mqtt-listener.module.ts +++ b/src/modules/device-integrations/internal-mqtt-listener.module.ts @@ -7,12 +7,8 @@ import { InternalMqttBrokerListenerService } from "@services/data-management/int import { EncryptionHelperService } from "@services/encryption-helper.service"; @Module({ - imports: [ReceiveDataModule, forwardRef(() => IoTDeviceModule), SharedModule], - providers: [ - InternalMqttClientListenerService, - InternalMqttBrokerListenerService, - EncryptionHelperService, - ], - exports: [InternalMqttClientListenerService, InternalMqttBrokerListenerService], + imports: [ReceiveDataModule, forwardRef(() => IoTDeviceModule), SharedModule], + providers: [InternalMqttClientListenerService, InternalMqttBrokerListenerService, EncryptionHelperService], + exports: [InternalMqttClientListenerService, InternalMqttBrokerListenerService], }) export class InternalMqttListenerModule {} diff --git a/src/modules/device-integrations/lorawan-gateway.module.ts b/src/modules/device-integrations/lorawan-gateway.module.ts index fc919c86..34f7fcdf 100644 --- a/src/modules/device-integrations/lorawan-gateway.module.ts +++ b/src/modules/device-integrations/lorawan-gateway.module.ts @@ -8,9 +8,9 @@ import { HttpModule } from "@nestjs/axios"; import { OrganizationModule } from "@modules/user-management/organization.module"; @Module({ - controllers: [LoRaWANGatewayController], - imports: [SharedModule, HttpModule, OrganizationModule], - providers: [ChirpstackGatewayService, GatewayStatusHistoryService, GatewayBootstrapperService], - exports: [GatewayStatusHistoryService], + controllers: [LoRaWANGatewayController], + imports: [SharedModule, HttpModule, OrganizationModule], + providers: [ChirpstackGatewayService, GatewayStatusHistoryService, GatewayBootstrapperService], + exports: [GatewayStatusHistoryService], }) export class LoRaWANGatewayModule {} diff --git a/src/modules/device-integrations/receive-data.module.ts b/src/modules/device-integrations/receive-data.module.ts index a3e98eaa..c9cea61b 100644 --- a/src/modules/device-integrations/receive-data.module.ts +++ b/src/modules/device-integrations/receive-data.module.ts @@ -9,15 +9,15 @@ import { ReceiveDataService } from "@services/data-management/receive-data.servi import { HttpModule } from "@nestjs/axios"; @Module({ - imports: [ - SharedModule, - ChirpstackAdministrationModule, - HttpModule, - forwardRef(() => ApplicationModule), - forwardRef(() => IoTDeviceModule), - ], - exports: [ReceiveDataService], - controllers: [ReceiveDataController], - providers: [ReceiveDataService], + imports: [ + SharedModule, + ChirpstackAdministrationModule, + HttpModule, + forwardRef(() => ApplicationModule), + forwardRef(() => IoTDeviceModule), + ], + exports: [ReceiveDataService], + controllers: [ReceiveDataController], + providers: [ReceiveDataService], }) export class ReceiveDataModule {} diff --git a/src/modules/device-integrations/sigfox-administration.module.ts b/src/modules/device-integrations/sigfox-administration.module.ts index aa42526e..77f8f0ae 100644 --- a/src/modules/device-integrations/sigfox-administration.module.ts +++ b/src/modules/device-integrations/sigfox-administration.module.ts @@ -4,8 +4,8 @@ import { Module } from "@nestjs/common"; import { GenericSigfoxAdministationService } from "@services/sigfox/generic-sigfox-administation.service"; @Module({ - imports: [SharedModule, HttpModule], - providers: [GenericSigfoxAdministationService], - exports: [GenericSigfoxAdministationService], + imports: [SharedModule, HttpModule], + providers: [GenericSigfoxAdministationService], + exports: [GenericSigfoxAdministationService], }) export class SigFoxAdministrationModule {} diff --git a/src/modules/device-integrations/sigfox-contract.module.ts b/src/modules/device-integrations/sigfox-contract.module.ts index 9083b9d5..9373623d 100644 --- a/src/modules/device-integrations/sigfox-contract.module.ts +++ b/src/modules/device-integrations/sigfox-contract.module.ts @@ -7,14 +7,9 @@ import { SigFoxApiContractService } from "@services/sigfox/sigfox-api-contract.s import { SigFoxApiContractController } from "@admin-controller/sigfox/sigfox-api-contract.controller"; @Module({ - imports: [ - AuthModule, - SigFoxGroupModule, - SigFoxAdministrationModule, - SigFoxUsersModule, - ], - controllers: [SigFoxApiContractController], - providers: [SigFoxApiContractService], - exports: [SigFoxApiContractService], + imports: [AuthModule, SigFoxGroupModule, SigFoxAdministrationModule, SigFoxUsersModule], + controllers: [SigFoxApiContractController], + providers: [SigFoxApiContractService], + exports: [SigFoxApiContractService], }) export class SigfoxContractModule {} diff --git a/src/modules/device-integrations/sigfox-device-type.module.ts b/src/modules/device-integrations/sigfox-device-type.module.ts index b145e2b3..b2ffb8a6 100644 --- a/src/modules/device-integrations/sigfox-device-type.module.ts +++ b/src/modules/device-integrations/sigfox-device-type.module.ts @@ -9,15 +9,15 @@ import { ConfigModule } from "@nestjs/config"; import configuration from "@config/configuration"; @Module({ - imports: [ - ConfigModule.forRoot({ load: [configuration] }), - AuthModule, - SigFoxGroupModule, - SigFoxAdministrationModule, - SigFoxUsersModule, - ], - controllers: [SigfoxDeviceTypeController], - providers: [SigFoxApiDeviceTypeService], - exports: [SigFoxApiDeviceTypeService], + imports: [ + ConfigModule.forRoot({ load: [configuration] }), + AuthModule, + SigFoxGroupModule, + SigFoxAdministrationModule, + SigFoxUsersModule, + ], + controllers: [SigfoxDeviceTypeController], + providers: [SigFoxApiDeviceTypeService], + exports: [SigFoxApiDeviceTypeService], }) export class SigfoxDeviceTypeModule {} diff --git a/src/modules/device-integrations/sigfox-device.module.ts b/src/modules/device-integrations/sigfox-device.module.ts index ae8d2c90..aa089af3 100644 --- a/src/modules/device-integrations/sigfox-device.module.ts +++ b/src/modules/device-integrations/sigfox-device.module.ts @@ -9,15 +9,15 @@ import { SigfoxApiGroupService } from "@services/sigfox/sigfox-api-group.service import { IoTDeviceModule } from "@modules/device-management/iot-device.module"; @Module({ - imports: [ - AuthModule, - SigFoxGroupModule, - SigFoxAdministrationModule, - SigFoxUsersModule, - forwardRef(() => IoTDeviceModule), - ], - controllers: [SigFoxApiDeviceController], - providers: [SigFoxApiDeviceService, SigfoxApiGroupService], - exports: [SigFoxApiDeviceService, SigfoxApiGroupService], + imports: [ + AuthModule, + SigFoxGroupModule, + SigFoxAdministrationModule, + SigFoxUsersModule, + forwardRef(() => IoTDeviceModule), + ], + controllers: [SigFoxApiDeviceController], + providers: [SigFoxApiDeviceService, SigfoxApiGroupService], + exports: [SigFoxApiDeviceService, SigfoxApiGroupService], }) export class SigfoxDeviceModule {} diff --git a/src/modules/device-integrations/sigfox-group.module.ts b/src/modules/device-integrations/sigfox-group.module.ts index c255217d..12c954c8 100644 --- a/src/modules/device-integrations/sigfox-group.module.ts +++ b/src/modules/device-integrations/sigfox-group.module.ts @@ -8,9 +8,9 @@ import { SigFoxAdministrationModule } from "@modules/device-integrations/sigfox- import { SigfoxApiGroupService } from "@services/sigfox/sigfox-api-group.service"; @Module({ - imports: [SharedModule, OrganizationModule, SigFoxAdministrationModule], - controllers: [SigfoxGroupController], - providers: [SigFoxGroupService, SigfoxApiGroupService], - exports: [SigFoxGroupService], + imports: [SharedModule, OrganizationModule, SigFoxAdministrationModule], + controllers: [SigfoxGroupController], + providers: [SigFoxGroupService, SigfoxApiGroupService], + exports: [SigFoxGroupService], }) export class SigFoxGroupModule {} diff --git a/src/modules/device-integrations/sigfox-listener.module.ts b/src/modules/device-integrations/sigfox-listener.module.ts index f116b519..c7df1470 100644 --- a/src/modules/device-integrations/sigfox-listener.module.ts +++ b/src/modules/device-integrations/sigfox-listener.module.ts @@ -6,7 +6,7 @@ import { IoTDeviceModule } from "@modules/device-management/iot-device.module"; import { SharedModule } from "@modules/shared.module"; @Module({ - imports: [SharedModule, IoTDeviceModule, ReceiveDataModule], - controllers: [SigFoxListenerController], + imports: [SharedModule, IoTDeviceModule, ReceiveDataModule], + controllers: [SigFoxListenerController], }) export class SigFoxListenerModule {} diff --git a/src/modules/device-integrations/sigfox-users.module.ts b/src/modules/device-integrations/sigfox-users.module.ts index 073254e8..f5621043 100644 --- a/src/modules/device-integrations/sigfox-users.module.ts +++ b/src/modules/device-integrations/sigfox-users.module.ts @@ -4,8 +4,8 @@ import { SigfoxApiUsersService } from "@services/sigfox/sigfox-api-users.service import { SigFoxAdministrationModule } from "@modules/device-integrations/sigfox-administration.module"; @Module({ - imports: [SigFoxAdministrationModule], - providers: [SigfoxApiUsersService], - exports: [SigfoxApiUsersService], + imports: [SigFoxAdministrationModule], + providers: [SigfoxApiUsersService], + exports: [SigfoxApiUsersService], }) export class SigFoxUsersModule {} diff --git a/src/modules/device-management/application.module.ts b/src/modules/device-management/application.module.ts index 769ed725..1cb87dfb 100644 --- a/src/modules/device-management/application.module.ts +++ b/src/modules/device-management/application.module.ts @@ -9,16 +9,16 @@ import { MulticastModule } from "./multicast.module"; import { DataTargetModule } from "@modules/device-management/data-target.module"; @Module({ - imports: [ - SharedModule, - forwardRef(() => OrganizationModule), - forwardRef(() => PermissionModule), - forwardRef(() => MulticastModule), // because of circular reference - forwardRef(() => DataTargetModule), - ChirpstackAdministrationModule, - ], - exports: [ApplicationService], - controllers: [ApplicationController], - providers: [ApplicationService], + imports: [ + SharedModule, + forwardRef(() => OrganizationModule), + forwardRef(() => PermissionModule), + forwardRef(() => MulticastModule), // because of circular reference + forwardRef(() => DataTargetModule), + ChirpstackAdministrationModule, + ], + exports: [ApplicationService], + controllers: [ApplicationController], + providers: [ApplicationService], }) export class ApplicationModule {} diff --git a/src/modules/device-management/data-target.module.ts b/src/modules/device-management/data-target.module.ts index 902773c9..da007996 100644 --- a/src/modules/device-management/data-target.module.ts +++ b/src/modules/device-management/data-target.module.ts @@ -7,27 +7,24 @@ import { forwardRef, Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { DataTargetService } from "@services/data-targets/data-target.service"; import { OS2IoTMail } from "@services/os2iot-mail.service"; -import { - CLIENT_SECRET_PROVIDER, - PlainTextClientSecretProvider, -} from "../../helpers/fiware-token.helper"; +import { CLIENT_SECRET_PROVIDER, PlainTextClientSecretProvider } from "../../helpers/fiware-token.helper"; @Module({ - imports: [ - SharedModule, - forwardRef(() => ApplicationModule), - OrganizationModule, - ConfigModule.forRoot({ load: [configuration] }), - ], - exports: [DataTargetService], - controllers: [DataTargetController], - providers: [ - DataTargetService, - OS2IoTMail, - { - provide: CLIENT_SECRET_PROVIDER, - useClass: PlainTextClientSecretProvider, - }, - ], + imports: [ + SharedModule, + forwardRef(() => ApplicationModule), + OrganizationModule, + ConfigModule.forRoot({ load: [configuration] }), + ], + exports: [DataTargetService], + controllers: [DataTargetController], + providers: [ + DataTargetService, + OS2IoTMail, + { + provide: CLIENT_SECRET_PROVIDER, + useClass: PlainTextClientSecretProvider, + }, + ], }) export class DataTargetModule {} diff --git a/src/modules/device-management/device-model.module.ts b/src/modules/device-management/device-model.module.ts index 6e8122cb..490e2a17 100644 --- a/src/modules/device-management/device-model.module.ts +++ b/src/modules/device-management/device-model.module.ts @@ -5,9 +5,9 @@ import { SharedModule } from "@modules/shared.module"; import { OrganizationModule } from "@modules/user-management/organization.module"; @Module({ - imports: [SharedModule, OrganizationModule], - providers: [DeviceModelService], - controllers: [DeviceModelController], - exports: [DeviceModelService], + imports: [SharedModule, OrganizationModule], + providers: [DeviceModelService], + controllers: [DeviceModelController], + exports: [DeviceModelService], }) export class DeviceModelModule {} diff --git a/src/modules/device-management/iot-device-payload-decoder-data-target-connection.module.ts b/src/modules/device-management/iot-device-payload-decoder-data-target-connection.module.ts index 85449c72..a96db9e3 100644 --- a/src/modules/device-management/iot-device-payload-decoder-data-target-connection.module.ts +++ b/src/modules/device-management/iot-device-payload-decoder-data-target-connection.module.ts @@ -8,9 +8,9 @@ import { SharedModule } from "@modules/shared.module"; import { IoTDevicePayloadDecoderDataTargetConnectionService } from "@services/device-management/iot-device-payload-decoder-data-target-connection.service"; @Module({ - imports: [SharedModule, IoTDeviceModule, DataTargetModule, PayloadDecoderModule], - providers: [IoTDevicePayloadDecoderDataTargetConnectionService], - exports: [IoTDevicePayloadDecoderDataTargetConnectionService], - controllers: [IoTDevicePayloadDecoderDataTargetConnectionController], + imports: [SharedModule, IoTDeviceModule, DataTargetModule, PayloadDecoderModule], + providers: [IoTDevicePayloadDecoderDataTargetConnectionService], + exports: [IoTDevicePayloadDecoderDataTargetConnectionService], + controllers: [IoTDevicePayloadDecoderDataTargetConnectionController], }) export class IoTDevicePayloadDecoderDataTargetConnectionModule {} diff --git a/src/modules/device-management/iot-device.module.ts b/src/modules/device-management/iot-device.module.ts index f208b1f5..21879fe8 100644 --- a/src/modules/device-management/iot-device.module.ts +++ b/src/modules/device-management/iot-device.module.ts @@ -22,30 +22,30 @@ import { LorawanDeviceDatabaseEnrichJob } from "@services/device-management/lora import { OrganizationModule } from "@modules/user-management/organization.module"; @Module({ - imports: [ - SharedModule, - ChirpstackAdministrationModule, - forwardRef(() => ApplicationModule), - SigFoxGroupModule, - SigfoxDeviceTypeModule, - DeviceModelModule, - OrganizationModule, - ReceiveDataModule, - forwardRef(() => SigfoxDeviceModule), - forwardRef(() => IoTLoRaWANDeviceModule), - InternalMqttListenerModule, - ], - exports: [MqttService, IoTDeviceService], - controllers: [IoTDeviceController, IoTDevicePayloadDecoderController], - providers: [ - PeriodicSigFoxCleanupService, - IoTDeviceDownlinkService, - SigFoxMessagesService, - LorawanDeviceDatabaseEnrichJob, - MqttService, - IoTDeviceService, - EncryptionHelperService, - CsvGeneratorService, - ], + imports: [ + SharedModule, + ChirpstackAdministrationModule, + forwardRef(() => ApplicationModule), + SigFoxGroupModule, + SigfoxDeviceTypeModule, + DeviceModelModule, + OrganizationModule, + ReceiveDataModule, + forwardRef(() => SigfoxDeviceModule), + forwardRef(() => IoTLoRaWANDeviceModule), + InternalMqttListenerModule, + ], + exports: [MqttService, IoTDeviceService], + controllers: [IoTDeviceController, IoTDevicePayloadDecoderController], + providers: [ + PeriodicSigFoxCleanupService, + IoTDeviceDownlinkService, + SigFoxMessagesService, + LorawanDeviceDatabaseEnrichJob, + MqttService, + IoTDeviceService, + EncryptionHelperService, + CsvGeneratorService, + ], }) export class IoTDeviceModule {} diff --git a/src/modules/device-management/iot-lorawan-device.module.ts b/src/modules/device-management/iot-lorawan-device.module.ts index ff26acae..6960f2b4 100644 --- a/src/modules/device-management/iot-lorawan-device.module.ts +++ b/src/modules/device-management/iot-lorawan-device.module.ts @@ -3,9 +3,9 @@ import { Module } from "@nestjs/common"; import { IoTLoRaWANDeviceService } from "@services/device-management/iot-lorawan-device.service"; @Module({ - imports: [SharedModule], - exports: [IoTLoRaWANDeviceService], - controllers: [], - providers: [IoTLoRaWANDeviceService], + imports: [SharedModule], + exports: [IoTLoRaWANDeviceService], + controllers: [], + providers: [IoTLoRaWANDeviceService], }) export class IoTLoRaWANDeviceModule {} diff --git a/src/modules/device-management/multicast.module.ts b/src/modules/device-management/multicast.module.ts index db497135..633e54d0 100644 --- a/src/modules/device-management/multicast.module.ts +++ b/src/modules/device-management/multicast.module.ts @@ -8,15 +8,15 @@ import { ApplicationModule } from "./application.module"; import { IoTDeviceModule } from "./iot-device.module"; @Module({ - imports: [ - SharedModule, - forwardRef(() => ApplicationModule), // because of circular reference - HttpModule, - ChirpstackAdministrationModule, - IoTDeviceModule, - ], - exports: [MulticastService], - controllers: [MulticastController], - providers: [MulticastService], + imports: [ + SharedModule, + forwardRef(() => ApplicationModule), // because of circular reference + HttpModule, + ChirpstackAdministrationModule, + IoTDeviceModule, + ], + exports: [MulticastService], + controllers: [MulticastController], + providers: [MulticastService], }) export class MulticastModule {} diff --git a/src/modules/device-management/payload-decoder.module.ts b/src/modules/device-management/payload-decoder.module.ts index 69e96b96..c14c0a68 100644 --- a/src/modules/device-management/payload-decoder.module.ts +++ b/src/modules/device-management/payload-decoder.module.ts @@ -8,9 +8,9 @@ import { OrganizationModule } from "@modules/user-management/organization.module import { PayloadDecoderService } from "@services/data-management/payload-decoder.service"; @Module({ - imports: [SharedModule, IoTDeviceModule, DataTargetModule, OrganizationModule], - exports: [PayloadDecoderService], - controllers: [PayloadDecoderController], - providers: [PayloadDecoderService], + imports: [SharedModule, IoTDeviceModule, DataTargetModule, OrganizationModule], + exports: [PayloadDecoderService], + controllers: [PayloadDecoderController], + providers: [PayloadDecoderService], }) export class PayloadDecoderModule {} diff --git a/src/modules/kafka.module.ts b/src/modules/kafka.module.ts index d6ba24a9..4b170cfc 100644 --- a/src/modules/kafka.module.ts +++ b/src/modules/kafka.module.ts @@ -4,9 +4,9 @@ import { HealthCheckModule } from "@modules/health-check.module"; @Global() @Module({ - controllers: [], - imports: [HealthCheckModule], - providers: [KafkaService], - exports: [KafkaService], + controllers: [], + imports: [HealthCheckModule], + providers: [KafkaService], + exports: [KafkaService], }) export class KafkaModule {} diff --git a/src/modules/open-data-dk-sharing.module.ts b/src/modules/open-data-dk-sharing.module.ts index 069aca83..c82508c0 100644 --- a/src/modules/open-data-dk-sharing.module.ts +++ b/src/modules/open-data-dk-sharing.module.ts @@ -7,8 +7,8 @@ import { PayloadDecoderExecutorModuleModule } from "@modules/payload-decoder-exe import { ChirpstackAdministrationModule } from "@modules/device-integrations/chirpstack-administration.module"; @Module({ - imports: [SharedModule, OrganizationModule, PayloadDecoderExecutorModuleModule, ChirpstackAdministrationModule], - controllers: [OpenDataDkSharingController], - providers: [OpenDataDkSharingService], + imports: [SharedModule, OrganizationModule, PayloadDecoderExecutorModuleModule, ChirpstackAdministrationModule], + controllers: [OpenDataDkSharingController], + providers: [OpenDataDkSharingService], }) export class OpenDataDkSharingModule {} diff --git a/src/modules/payload-decoder-executor-module.module.ts b/src/modules/payload-decoder-executor-module.module.ts index eba600ee..cf0f1557 100644 --- a/src/modules/payload-decoder-executor-module.module.ts +++ b/src/modules/payload-decoder-executor-module.module.ts @@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"; import { PayloadDecoderExecutorService } from "@services/data-management/payload-decoder-executor.service"; @Module({ - providers: [PayloadDecoderExecutorService], - exports: [PayloadDecoderExecutorService], + providers: [PayloadDecoderExecutorService], + exports: [PayloadDecoderExecutorService], }) export class PayloadDecoderExecutorModuleModule {} diff --git a/src/modules/search.module.ts b/src/modules/search.module.ts index 454ff0b0..9803f91f 100644 --- a/src/modules/search.module.ts +++ b/src/modules/search.module.ts @@ -5,8 +5,8 @@ import { ChirpstackAdministrationModule } from "./device-integrations/chirpstack import { SharedModule } from "./shared.module"; @Module({ - imports: [ChirpstackAdministrationModule, SharedModule], - providers: [SearchService], - controllers: [SearchController], + imports: [ChirpstackAdministrationModule, SharedModule], + providers: [SearchService], + controllers: [SearchController], }) export class SearchModule {} diff --git a/src/modules/shared.module.ts b/src/modules/shared.module.ts index 7724881f..9b638711 100644 --- a/src/modules/shared.module.ts +++ b/src/modules/shared.module.ts @@ -35,43 +35,43 @@ import { MQTTExternalBrokerDevice } from "@entities/mqtt-external-broker-device. import { Gateway } from "@entities/gateway.entity"; @Module({ - imports: [ - TypeOrmModule.forFeature([ - User, - Application, - DataTarget, - GenericHTTPDevice, - HttpPushDataTarget, - FiwareDataTarget, - MqttDataTarget, - OpenDataDkDataTarget, - IoTDevice, - IoTDevicePayloadDecoderDataTargetConnection, - DeviceModel, - LoRaWANDevice, - OpenDataDkDataset, - Organization, - PayloadDecoder, - Permission, - ReceivedMessage, - ReceivedMessageMetadata, - SigFoxDevice, - SigFoxGroup, - User, - Multicast, - LorawanMulticastDefinition, - ControlledProperty, - ApplicationDeviceType, - ApiKey, - ReceivedMessageSigFoxSignals, - PermissionTypeEntity, - GatewayStatusHistory, - MQTTInternalBrokerDevice, - MQTTExternalBrokerDevice, - Gateway, - ]), - ], - providers: [AuditLog], - exports: [TypeOrmModule, AuditLog], + imports: [ + TypeOrmModule.forFeature([ + User, + Application, + DataTarget, + GenericHTTPDevice, + HttpPushDataTarget, + FiwareDataTarget, + MqttDataTarget, + OpenDataDkDataTarget, + IoTDevice, + IoTDevicePayloadDecoderDataTargetConnection, + DeviceModel, + LoRaWANDevice, + OpenDataDkDataset, + Organization, + PayloadDecoder, + Permission, + ReceivedMessage, + ReceivedMessageMetadata, + SigFoxDevice, + SigFoxGroup, + User, + Multicast, + LorawanMulticastDefinition, + ControlledProperty, + ApplicationDeviceType, + ApiKey, + ReceivedMessageSigFoxSignals, + PermissionTypeEntity, + GatewayStatusHistory, + MQTTInternalBrokerDevice, + MQTTExternalBrokerDevice, + Gateway, + ]), + ], + providers: [AuditLog], + exports: [TypeOrmModule, AuditLog], }) export class SharedModule {} diff --git a/src/modules/test-payload-decoder.module.ts b/src/modules/test-payload-decoder.module.ts index e5a244b3..b5c97fc2 100644 --- a/src/modules/test-payload-decoder.module.ts +++ b/src/modules/test-payload-decoder.module.ts @@ -3,7 +3,7 @@ import { TestPayloadDecoderController } from "@admin-controller/test-payload-dec import { PayloadDecoderExecutorModuleModule } from "@modules/payload-decoder-executor-module.module"; @Module({ - imports: [PayloadDecoderExecutorModuleModule], - controllers: [TestPayloadDecoderController], + imports: [PayloadDecoderExecutorModuleModule], + controllers: [TestPayloadDecoderController], }) export class TestPayloadDecoderModule {} diff --git a/src/modules/user-management/auth.module.ts b/src/modules/user-management/auth.module.ts index d5fb9c9e..9fe828a2 100644 --- a/src/modules/user-management/auth.module.ts +++ b/src/modules/user-management/auth.module.ts @@ -17,32 +17,30 @@ import { ApiKeyStrategy } from "@auth/api-key.strategy"; import { ApiKeyModule } from "@modules/api-key-management/api-key.module"; @Module({ - imports: [ - ConfigModule.forRoot({ load: [configuration] }), - PassportModule.register({ defaultStrategy: "jwt" }), - JwtModule.registerAsync({ - imports: [ConfigModule], - inject: [ConfigService], - useFactory: async (configService: ConfigService) => ({ - secret: configService.get("jwt.secret"), - signOptions: { - expiresIn: configService.get("jwt.expiresIn"), - }, - }), - }), - forwardRef(() => UserModule), - forwardRef(() => PermissionModule), - forwardRef(() => OrganizationModule), - forwardRef(() => ApiKeyModule) - ], - providers: [AuthService, LocalStrategy, JwtStrategy, KombitStrategy, ApiKeyStrategy], - exports: [AuthService], - controllers: [AuthController], + imports: [ + ConfigModule.forRoot({ load: [configuration] }), + PassportModule.register({ defaultStrategy: "jwt" }), + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: async (configService: ConfigService) => ({ + secret: configService.get("jwt.secret"), + signOptions: { + expiresIn: configService.get("jwt.expiresIn"), + }, + }), + }), + forwardRef(() => UserModule), + forwardRef(() => PermissionModule), + forwardRef(() => OrganizationModule), + forwardRef(() => ApiKeyModule), + ], + providers: [AuthService, LocalStrategy, JwtStrategy, KombitStrategy, ApiKeyStrategy], + exports: [AuthService], + controllers: [AuthController], }) export class AuthModule { - configure(consumer: MiddlewareConsumer): void { - consumer - .apply(HandleRedirectUrlParameterMiddleware) - .forRoutes("auth/kombit/login"); - } + configure(consumer: MiddlewareConsumer): void { + consumer.apply(HandleRedirectUrlParameterMiddleware).forRoutes("auth/kombit/login"); + } } diff --git a/src/modules/user-management/new-kombit-creation.module.ts b/src/modules/user-management/new-kombit-creation.module.ts index 0c90543d..cbb99afc 100644 --- a/src/modules/user-management/new-kombit-creation.module.ts +++ b/src/modules/user-management/new-kombit-creation.module.ts @@ -7,7 +7,7 @@ import { UserModule } from "./user.module"; import { PermissionModule } from "./permission.module"; @Module({ - imports: [SharedModule, OrganizationModule, UserModule, PermissionModule], - controllers: [NewKombitCreationController], + imports: [SharedModule, OrganizationModule, UserModule, PermissionModule], + controllers: [NewKombitCreationController], }) export class NewKombitCreationModule {} diff --git a/src/modules/user-management/organization.module.ts b/src/modules/user-management/organization.module.ts index 591d331e..8478b0be 100644 --- a/src/modules/user-management/organization.module.ts +++ b/src/modules/user-management/organization.module.ts @@ -6,9 +6,9 @@ import { OrganizationController } from "@user-management-controller/organization import { UserModule } from "./user.module"; @Module({ - imports: [SharedModule, forwardRef(() => PermissionModule), forwardRef(() => UserModule)], - providers: [OrganizationService], - exports: [OrganizationService], - controllers: [OrganizationController], + imports: [SharedModule, forwardRef(() => PermissionModule), forwardRef(() => UserModule)], + providers: [OrganizationService], + exports: [OrganizationService], + controllers: [OrganizationController], }) export class OrganizationModule {} diff --git a/src/modules/user-management/permission.module.ts b/src/modules/user-management/permission.module.ts index d3197be0..3ca2761a 100644 --- a/src/modules/user-management/permission.module.ts +++ b/src/modules/user-management/permission.module.ts @@ -8,14 +8,14 @@ import { PermissionService } from "@services/user-management/permission.service" import { PermissionController } from "@user-management-controller/permission.controller"; @Module({ - imports: [ - SharedModule, - forwardRef(() => ApplicationModule), - forwardRef(() => UserModule), - forwardRef(() => OrganizationModule), - ], - providers: [PermissionService], - exports: [PermissionService], - controllers: [PermissionController], + imports: [ + SharedModule, + forwardRef(() => ApplicationModule), + forwardRef(() => UserModule), + forwardRef(() => OrganizationModule), + ], + providers: [PermissionService], + exports: [PermissionService], + controllers: [PermissionController], }) export class PermissionModule {} diff --git a/src/modules/user-management/user.module.ts b/src/modules/user-management/user.module.ts index 2cd33662..e72069bc 100644 --- a/src/modules/user-management/user.module.ts +++ b/src/modules/user-management/user.module.ts @@ -11,9 +11,14 @@ import configuration from "@config/configuration"; import { OS2IoTMail } from "@services/os2iot-mail.service"; @Module({ - imports: [SharedModule, ConfigModule.forRoot({ load: [configuration] }), forwardRef(() => PermissionModule), forwardRef(() => OrganizationModule)], - controllers: [UserController], - providers: [UserService, UserBootstrapperService, OS2IoTMail], - exports: [UserService], + imports: [ + SharedModule, + ConfigModule.forRoot({ load: [configuration] }), + forwardRef(() => PermissionModule), + forwardRef(() => OrganizationModule), + ], + controllers: [UserController], + providers: [UserService, UserBootstrapperService, OS2IoTMail], + exports: [UserService], }) export class UserModule {} diff --git a/src/repositories/os2iot.repository.ts b/src/repositories/os2iot.repository.ts index 463300a7..19f85364 100644 --- a/src/repositories/os2iot.repository.ts +++ b/src/repositories/os2iot.repository.ts @@ -1,20 +1,20 @@ import { DataSource } from "typeorm"; const os2IotContext = new DataSource({ - type: "postgres", - host: process.env.DATABASE_HOSTNAME || "host.docker.internal", - port: parseInt(process.env.DATABASE_PORT ?? "", 10) || 5433, - username: process.env.DATABASE_USERNAME || "os2iot", - password: process.env.DATABASE_PASSWORD || "toi2so", - database: "os2iot", - synchronize: false, - logging: false, - // For some reason, entities path is from where "typeorm" is executed from console. - // TypeORM commands are executed from the root folder. - entities: ["src/entities/*.ts", "src/entities/permissions/*.ts"], - // From v3, it's no longer used as output path... - migrations: ["src/migration/*.ts"], - ssl: process.env.DATABASE_ENABLE_SSL === "true" + type: "postgres", + host: process.env.DATABASE_HOSTNAME || "host.docker.internal", + port: parseInt(process.env.DATABASE_PORT ?? "", 10) || 5433, + username: process.env.DATABASE_USERNAME || "os2iot", + password: process.env.DATABASE_PASSWORD || "toi2so", + database: "os2iot", + synchronize: false, + logging: false, + // For some reason, entities path is from where "typeorm" is executed from console. + // TypeORM commands are executed from the root folder. + entities: ["src/entities/*.ts", "src/entities/permissions/*.ts"], + // From v3, it's no longer used as output path... + migrations: ["src/migration/*.ts"], + ssl: process.env.DATABASE_ENABLE_SSL === "true", }); /** diff --git a/src/resources/device-model-schema.ts b/src/resources/device-model-schema.ts index a497cbab..83afda9e 100644 --- a/src/resources/device-model-schema.ts +++ b/src/resources/device-model-schema.ts @@ -1,155 +1,131 @@ // From: https://github.com/smart-data-models/dataModel.Device/blob/master/DeviceModel/schema.json export const deviceModelSchema = { - $schema: "http://json-schema.org/schema#", - $schemaVersion: "0.0", - $id: "https://smart-data-models.github.io/dataModel.Device/DeviceModel/schema.json", - title: " - Device Model schema", - description: "This entity captures the static properties of a Device. ", - type: "object", - allOf: [ - { - $ref: - "https://smart-data-models.github.io/data-models/common-schema.json#/definitions/GSMA-Commons", + $schema: "http://json-schema.org/schema#", + $schemaVersion: "0.0", + $id: "https://smart-data-models.github.io/dataModel.Device/DeviceModel/schema.json", + title: " - Device Model schema", + description: "This entity captures the static properties of a Device. ", + type: "object", + allOf: [ + { + $ref: "https://smart-data-models.github.io/data-models/common-schema.json#/definitions/GSMA-Commons", + }, + { + $ref: "https://smart-data-models.github.io/data-models/common-schema.json#/definitions/PhysicalObject-Commons", + }, + { + $ref: "https://smart-data-models.github.io/dataModel.Device/device-schema.json", + }, + { + properties: { + type: { + type: "string", + enum: ["DeviceModel"], + description: "Property. NGSI Entity type. it has to be DeviceModel", }, - { - $ref: - "https://smart-data-models.github.io/data-models/common-schema.json#/definitions/PhysicalObject-Commons", + deviceClass: { + type: "string", + enum: ["C0", "C1", "C2"], + description: + "Property. Model:'https://schema.org/Text'. Class of constrained device as specified by RFC 7228. If the device is not a constrained device this property shall not be present. Normative References: [RFC7228](https://tools.ietf.org/html/rfc7228#section-3). Enum:'C0, C1, C2'", }, - { - $ref: - "https://smart-data-models.github.io/dataModel.Device/device-schema.json", + controlledProperty: { + type: "array", + description: + "Property. Model:'https://schema.org/DateTime'. Enum:'temperature, humidity, light, motion, fillingLevel,occupancy, power, pressure, smoke, energy, airPollution, noiseLevel, weatherConditions, precipitation, windSpeed, windDirection, atmosphericPressure, solarRadiation, depth, pH,conductivity, conductance, tss, tds, turbidity, salinity,orp, cdom, waterPollution, location, speed, heading,weight, waterConsumption, gasComsumption, electricityConsumption, soilMoisture, trafficFlow,eatingActivity, milking, movementActivity'.", + items: { + type: "string", + enum: [ + "temperature", + "humidity", + "light", + "motion", + "fillingLevel", + "occupancy", + "power", + "pressure", + "smoke", + "energy", + "airPollution", + "noiseLevel", + "weatherConditions", + "precipitation", + "windSpeed", + "windDirection", + "atmosphericPressure", + "solarRadiation", + "depth", + "pH", + "conductivity", + "conductance", + "tss", + "tds", + "turbidity", + "salinity", + "orp", + "cdom", + "waterPollution", + "location", + "speed", + "heading", + "weight", + "waterConsumption", + "gasComsumption", + "electricityConsumption", + "soilMoisture", + "trafficFlow", + "eatingActivity", + "milking", + "movementActivity", + ], + }, }, - { - properties: { - type: { - type: "string", - enum: ["DeviceModel"], - description: "Property. NGSI Entity type. it has to be DeviceModel", - }, - deviceClass: { - type: "string", - enum: ["C0", "C1", "C2"], - description: - "Property. Model:'https://schema.org/Text'. Class of constrained device as specified by RFC 7228. If the device is not a constrained device this property shall not be present. Normative References: [RFC7228](https://tools.ietf.org/html/rfc7228#section-3). Enum:'C0, C1, C2'", - }, - controlledProperty: { - type: "array", - description: - "Property. Model:'https://schema.org/DateTime'. Enum:'temperature, humidity, light, motion, fillingLevel,occupancy, power, pressure, smoke, energy, airPollution, noiseLevel, weatherConditions, precipitation, windSpeed, windDirection, atmosphericPressure, solarRadiation, depth, pH,conductivity, conductance, tss, tds, turbidity, salinity,orp, cdom, waterPollution, location, speed, heading,weight, waterConsumption, gasComsumption, electricityConsumption, soilMoisture, trafficFlow,eatingActivity, milking, movementActivity'.", - items: { - type: "string", - enum: [ - "temperature", - "humidity", - "light", - "motion", - "fillingLevel", - "occupancy", - "power", - "pressure", - "smoke", - "energy", - "airPollution", - "noiseLevel", - "weatherConditions", - "precipitation", - "windSpeed", - "windDirection", - "atmosphericPressure", - "solarRadiation", - "depth", - "pH", - "conductivity", - "conductance", - "tss", - "tds", - "turbidity", - "salinity", - "orp", - "cdom", - "waterPollution", - "location", - "speed", - "heading", - "weight", - "waterConsumption", - "gasComsumption", - "electricityConsumption", - "soilMoisture", - "trafficFlow", - "eatingActivity", - "milking", - "movementActivity", - ], - }, - }, - function: { - type: "array", - description: - "Property. Model:'https://schema.org/Text'. The functionality necessary to accomplish the task for which a Device is designed. A device can be designed to perform more than one function. Defined by [SAREF](https://w3id.org/saref#Function). Enum:'levelControl, sensing, onOff, openClose, metering, eventNotification", - items: { - type: "string", - enum: [ - "levelControl", - "sensing", - "onOff", - "openClose", - "metering", - "eventNotification", - ], - }, - }, - supportedUnits: { - type: "array", - description: - "Property. Model:'https://schema.org/Text'. Units of measurement supported by the device. The unit code (text) of measurement given using the [UN/CEFACT Common Code](http://wiki.goodrelations-vocabulary.org/Documentation/UN/CEFACT_Common_Codes) (max. 3 characters).", - items: { - type: "string", - }, - }, - energyLimitationClass: { - type: "string", - description: - "Property. Model:'https://schema.org/Text'. Device's class of energy limitation as per RFC 7228. Normative References: [RFC7228](https://tools.ietf.org/html/rfc7228#page-11). Enum:'E0, E1, E2, E9'", - enum: ["E0", "E1", "E2", "E9"], - }, - documentation: { - type: "string", - format: "uri", - description: - "Property. Model:'https://schema.org/URL'. A link to device's documentation.", - }, - brandName: { - type: "string", - description: - "Property. Model:'https://schema.org/Text'. Device's brand name.", - }, - modelName: { - type: "string", - description: - "Property. Model:'https://schema.org/Text. Device's model name.", - }, - manufacturerName: { - type: "string", - description: - "Property. Model:'https://schema.org/Text'. Device's manufacturer name.", - }, - name: { - type: "string", - description: - "Property. Model:'https://schema.org/Text'. Device's model name in Portal" - } - }, + function: { + type: "array", + description: + "Property. Model:'https://schema.org/Text'. The functionality necessary to accomplish the task for which a Device is designed. A device can be designed to perform more than one function. Defined by [SAREF](https://w3id.org/saref#Function). Enum:'levelControl, sensing, onOff, openClose, metering, eventNotification", + items: { + type: "string", + enum: ["levelControl", "sensing", "onOff", "openClose", "metering", "eventNotification"], + }, }, - ], - required: [ - "id", - "type", - "category", - "controlledProperty", - "manufacturerName", - "brandName", - "modelName", - "name" - ], + supportedUnits: { + type: "array", + description: + "Property. Model:'https://schema.org/Text'. Units of measurement supported by the device. The unit code (text) of measurement given using the [UN/CEFACT Common Code](http://wiki.goodrelations-vocabulary.org/Documentation/UN/CEFACT_Common_Codes) (max. 3 characters).", + items: { + type: "string", + }, + }, + energyLimitationClass: { + type: "string", + description: + "Property. Model:'https://schema.org/Text'. Device's class of energy limitation as per RFC 7228. Normative References: [RFC7228](https://tools.ietf.org/html/rfc7228#page-11). Enum:'E0, E1, E2, E9'", + enum: ["E0", "E1", "E2", "E9"], + }, + documentation: { + type: "string", + format: "uri", + description: "Property. Model:'https://schema.org/URL'. A link to device's documentation.", + }, + brandName: { + type: "string", + description: "Property. Model:'https://schema.org/Text'. Device's brand name.", + }, + modelName: { + type: "string", + description: "Property. Model:'https://schema.org/Text. Device's model name.", + }, + manufacturerName: { + type: "string", + description: "Property. Model:'https://schema.org/Text'. Device's manufacturer name.", + }, + name: { + type: "string", + description: "Property. Model:'https://schema.org/Text'. Device's model name in Portal", + }, + }, + }, + ], + required: ["id", "type", "category", "controlledProperty", "manufacturerName", "brandName", "modelName", "name"], }; diff --git a/src/resources/resource-paths.ts b/src/resources/resource-paths.ts index 64666de3..184afff0 100644 --- a/src/resources/resource-paths.ts +++ b/src/resources/resource-paths.ts @@ -5,38 +5,30 @@ const srcFolder = `${sep}src`; const buildFolder = `${sep}dist`; const traverseUpUntilSrcFolder = (currentPath: string, traverseCount = 0): string => { - const parentFolder = join(currentPath, ".."); - - if (traverseCount > recurseLimit || parentFolder === currentPath) { - return currentPath; - } - - if ( - currentPath.endsWith(srcFolder) || - currentPath.endsWith(`${srcFolder}${sep}`) || - currentPath.endsWith(buildFolder) || - currentPath.endsWith(`${buildFolder}${sep}`) - ) { - return currentPath; - } - - return traverseUpUntilSrcFolder(parentFolder, ++traverseCount); + const parentFolder = join(currentPath, ".."); + + if (traverseCount > recurseLimit || parentFolder === currentPath) { + return currentPath; + } + + if ( + currentPath.endsWith(srcFolder) || + currentPath.endsWith(`${srcFolder}${sep}`) || + currentPath.endsWith(buildFolder) || + currentPath.endsWith(`${buildFolder}${sep}`) + ) { + return currentPath; + } + + return traverseUpUntilSrcFolder(parentFolder, ++traverseCount); }; export const ChirpstackStateTemplatePath = join( - traverseUpUntilSrcFolder(__dirname), - "..", - `resources/chirpstack-state.proto` + traverseUpUntilSrcFolder(__dirname), + "..", + `resources/chirpstack-state.proto` ); -export const caCertPath = join( - traverseUpUntilSrcFolder(__dirname), - "..", - "resources/ca.crt" -); +export const caCertPath = join(traverseUpUntilSrcFolder(__dirname), "..", "resources/ca.crt"); -export const caKeyPath = join( - traverseUpUntilSrcFolder(__dirname), - "..", - "resources/ca.key" -); +export const caKeyPath = join(traverseUpUntilSrcFolder(__dirname), "..", "resources/ca.key"); diff --git a/src/services/api-key-info/api-key-info.service.ts b/src/services/api-key-info/api-key-info.service.ts index 7b105eed..1bff6240 100644 --- a/src/services/api-key-info/api-key-info.service.ts +++ b/src/services/api-key-info/api-key-info.service.ts @@ -4,14 +4,14 @@ import { OrganizationService } from "@services/user-management/organization.serv @Injectable() export class ApiKeyInfoService { - constructor( - @Inject(forwardRef(() => OrganizationService)) - private organizationService: OrganizationService - ) {} + constructor( + @Inject(forwardRef(() => OrganizationService)) + private organizationService: OrganizationService + ) {} - private readonly logger = new Logger(ApiKeyInfoService.name, { timestamp: true }); + private readonly logger = new Logger(ApiKeyInfoService.name, { timestamp: true }); - findOrganization(orgId: number): Promise { - return this.organizationService.findById(orgId); - } + findOrganization(orgId: number): Promise { + return this.organizationService.findById(orgId); + } } diff --git a/src/services/api-key-management/api-key.service.ts b/src/services/api-key-management/api-key.service.ts index 8c71525d..505a06d0 100644 --- a/src/services/api-key-management/api-key.service.ts +++ b/src/services/api-key-management/api-key.service.ts @@ -15,128 +15,111 @@ import { nameof } from "@helpers/type-helper"; @Injectable() export class ApiKeyService { - constructor( - @InjectRepository(ApiKey) - private apiKeyRepository: Repository, - @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService - ) {} - private readonly logger = new Logger(ApiKeyService.name, { timestamp: true }); - - findOne(key: string): Promise { - return this.apiKeyRepository.findOne({ - where: { key }, - relations: ["systemUser"], - }); + constructor( + @InjectRepository(ApiKey) + private apiKeyRepository: Repository, + @Inject(forwardRef(() => PermissionService)) + private permissionService: PermissionService + ) {} + private readonly logger = new Logger(ApiKeyService.name, { timestamp: true }); + + findOne(key: string): Promise { + return this.apiKeyRepository.findOne({ + where: { key }, + relations: ["systemUser"], + }); + } + + findOneByIdWithPermissions(id: number): Promise { + return this.apiKeyRepository.findOne({ + where: { id }, + relations: [nameof("permissions")], + }); + } + + findOneByIdWithRelations(id: number): Promise { + return this.apiKeyRepository.findOne({ + where: { id }, + relations: [nameof("permissions"), nameof("systemUser")], + }); + } + + async findAllByOrganizationId(query: ListAllApiKeysDto): Promise { + const permIds = (await this.permissionService.getAllPermissionsInOrganizations([query.organizationId])).data.map( + x => x.id + ); + + let dbQuery = this.apiKeyRepository + .createQueryBuilder("api_key") + .innerJoinAndSelect("api_key.permissions", "perm") + .innerJoinAndSelect("perm.organization", "org") + .take(query.limit ? +query.limit : 100) + .skip(query.offset ? +query.offset : 0); + + if (permIds.length) { + dbQuery = dbQuery.where("perm.id IN (:...permIds)", { permIds }); } - findOneByIdWithPermissions(id: number): Promise { - return this.apiKeyRepository.findOne({ - where: { id }, - relations: [nameof("permissions")], - }); + if (query.orderOn && query.sort) { + dbQuery = dbQuery.orderBy(`api_key.${query.orderOn}`, query.sort.toUpperCase() as "ASC" | "DESC"); } - findOneByIdWithRelations(id: number): Promise { - return this.apiKeyRepository.findOne({ - where: { id }, - relations: [nameof("permissions"), nameof("systemUser")], - }); + const [data, count] = await dbQuery.getManyAndCount(); + + return { + data, + count, + }; + } + + async create(dto: CreateApiKeyDto, userId: number): Promise { + // Create the key + const apiKey = new ApiKey(); + apiKey.key = uuidv4(); + apiKey.name = dto.name; + apiKey.updatedBy = userId; + apiKey.createdBy = userId; + + // Create the system user + const systemUser = new User(); + systemUser.active = false; + systemUser.isSystemUser = true; + systemUser.passwordHash = uuidv4(); // Random password, user can never log in + systemUser.name = apiKey.name; + apiKey.systemUser = systemUser; + + if (dto.permissionIds?.length > 0) { + const permissionsDb = await this.permissionService.findManyByIds(dto.permissionIds); + + apiKey.permissions = permissionsDb.map(pm => ({ ...pm, apiKeys: null })); } - async findAllByOrganizationId( - query: ListAllApiKeysDto - ): Promise { - const permIds = ( - await this.permissionService.getAllPermissionsInOrganizations([ - query.organizationId, - ]) - ).data.map(x => x.id); - - let dbQuery = this.apiKeyRepository - .createQueryBuilder("api_key") - .innerJoinAndSelect("api_key.permissions", "perm") - .innerJoinAndSelect("perm.organization", "org") - .take(query.limit ? +query.limit : 100) - .skip(query.offset ? +query.offset : 0); - - if (permIds.length) { - dbQuery = dbQuery.where("perm.id IN (:...permIds)", { permIds }); - } - - if (query.orderOn && query.sort) { - dbQuery = dbQuery.orderBy( - `api_key.${query.orderOn}`, - query.sort.toUpperCase() as "ASC" | "DESC" - ); - } - - const [data, count] = await dbQuery.getManyAndCount(); - - return { - data, - count, - }; - } + return await this.apiKeyRepository.save(apiKey); + } - async create(dto: CreateApiKeyDto, userId: number): Promise { - // Create the key - const apiKey = new ApiKey(); - apiKey.key = uuidv4(); - apiKey.name = dto.name; - apiKey.updatedBy = userId; - apiKey.createdBy = userId; - - // Create the system user - const systemUser = new User(); - systemUser.active = false; - systemUser.isSystemUser = true; - systemUser.passwordHash = uuidv4(); // Random password, user can never log in - systemUser.name = apiKey.name; - apiKey.systemUser = systemUser; - - if (dto.permissionIds?.length > 0) { - const permissionsDb = await this.permissionService.findManyByIds( - dto.permissionIds - ); - - apiKey.permissions = permissionsDb.map( - pm => ({ ...pm, apiKeys: null }) - ); - } - - return await this.apiKeyRepository.save(apiKey); - } + async update(id: number, dto: UpdateApiKeyDto, userId: number): Promise { + const apiKey = await this.findOneByIdWithRelations(id); + apiKey.name = dto.name; + apiKey.updatedBy = userId; - async update( - id: number, - dto: UpdateApiKeyDto, - userId: number - ): Promise { - const apiKey = await this.findOneByIdWithRelations(id); - apiKey.name = dto.name; - apiKey.updatedBy = userId; - - if (dto.permissionIds?.length) { - const permissionsDb = await this.permissionService.findManyByIds( - dto.permissionIds - ); - apiKey.permissions = permissionsDb.map(pm => ({ - ...pm, - apiKeys: [], - })); - } - - if (dto.name !== apiKey.name) { - apiKey.systemUser.name = dto.name; - apiKey.name = dto.name; - } - - return await this.apiKeyRepository.save(apiKey); + if (dto.permissionIds?.length) { + const permissionsDb = await this.permissionService.findManyByIds(dto.permissionIds); + apiKey.permissions = permissionsDb.map(pm => ({ + ...pm, + apiKeys: [], + })); } - async delete(id: number): Promise { - const res = await this.apiKeyRepository.delete(id); - return new DeleteResponseDto(res.affected); + if (dto.name !== apiKey.name) { + apiKey.systemUser.name = dto.name; + apiKey.name = dto.name; } + + return await this.apiKeyRepository.save(apiKey); + } + + async delete(id: number): Promise { + const res = await this.apiKeyRepository.delete(id); + return new DeleteResponseDto(res.affected); + } } diff --git a/src/services/audit-log.service.ts b/src/services/audit-log.service.ts index 88d0d1c9..7af661e3 100644 --- a/src/services/audit-log.service.ts +++ b/src/services/audit-log.service.ts @@ -3,45 +3,45 @@ import { Injectable, Logger } from "@nestjs/common"; @Injectable() export class AuditLog { - static readonly logger = new Logger(AuditLog.name, { timestamp: false }); + static readonly logger = new Logger(AuditLog.name, { timestamp: false }); - static log( - actionType: ActionType, - type: string, - userId: number, - id: number | string = null, - name: string = null, - completed = false - ): void { - const auditLogEntry: AuditLogEntry = { - userId: userId, - timestamp: new Date(), - actionType: actionType, - type: type, - id: id, - name: name, - completed: completed, - }; - this.logger.log(JSON.stringify(auditLogEntry)); - } + static log( + actionType: ActionType, + type: string, + userId: number, + id: number | string = null, + name: string = null, + completed = false + ): void { + const auditLogEntry: AuditLogEntry = { + userId: userId, + timestamp: new Date(), + actionType: actionType, + type: type, + id: id, + name: name, + completed: completed, + }; + this.logger.log(JSON.stringify(auditLogEntry)); + } - static success( - actionType: ActionType, - type: string, - userId: number, - id: number | string = null, - name: string = null - ): void { - this.log(actionType, type, userId, id, name, true); - } + static success( + actionType: ActionType, + type: string, + userId: number, + id: number | string = null, + name: string = null + ): void { + this.log(actionType, type, userId, id, name, true); + } - static fail( - actionType: ActionType, - type: string, - userId: number, - id: number | string = null, - name: string = null - ): void { - this.log(actionType, type, userId, id, name, false); - } + static fail( + actionType: ActionType, + type: string, + userId: number, + id: number | string = null, + name: string = null + ): void { + this.log(actionType, type, userId, id, name, false); + } } diff --git a/src/services/chirpstack/chirpstack-application.service.ts b/src/services/chirpstack/chirpstack-application.service.ts index 65a7c66d..a383406d 100644 --- a/src/services/chirpstack/chirpstack-application.service.ts +++ b/src/services/chirpstack/chirpstack-application.service.ts @@ -1,11 +1,11 @@ import { Injectable } from "@nestjs/common"; import { GenericChirpstackConfigurationService } from "./generic-chirpstack-configuration.service"; import { - Application as ChirpstackApplication, - CreateApplicationRequest, - DeleteApplicationRequest, - ListApplicationsRequest, - UpdateApplicationRequest, + Application as ChirpstackApplication, + CreateApplicationRequest, + DeleteApplicationRequest, + ListApplicationsRequest, + UpdateApplicationRequest, } from "@chirpstack/chirpstack-api/api/application_pb"; import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; import { Application } from "@entities/application.entity"; @@ -17,100 +17,97 @@ import { Repository } from "typeorm"; @Injectable() export class ApplicationChirpstackService extends GenericChirpstackConfigurationService { - @InjectRepository(Application) - private applicationRepository: Repository; - constructor() { - super(); - } - applicationNamePrefix = "os2iot-"; - DEFAULT_DESCRIPTION = "Created by OS2IoT"; - - public async findOrCreateDefaultApplication( - applications: ListAllChirpstackApplicationsResponseDto = null, - iotDevice: LoRaWANDevice - ): Promise { - const organizationID = await this.getDefaultOrganizationId(); - const req = new ListApplicationsRequest(); - req.setTenantId(organizationID); - // Fetch applications - applications = - applications ?? - (await this.getAllWithPagination( - `applications?limit=100&organizationID=${organizationID}`, - this.applicationServiceClient, - req, - 100, - undefined - )); + @InjectRepository(Application) + private applicationRepository: Repository; + constructor() { + super(); + } + applicationNamePrefix = "os2iot-"; + DEFAULT_DESCRIPTION = "Created by OS2IoT"; - // if application exist use it - let applicationId = applications.resultList.find( - element => - element.id === iotDevice.chirpstackApplicationId || element.id === iotDevice.application.chirpstackId - )?.id; + public async findOrCreateDefaultApplication( + applications: ListAllChirpstackApplicationsResponseDto = null, + iotDevice: LoRaWANDevice + ): Promise { + const organizationID = await this.getDefaultOrganizationId(); + const req = new ListApplicationsRequest(); + req.setTenantId(organizationID); + // Fetch applications + applications = + applications ?? + (await this.getAllWithPagination( + `applications?limit=100&organizationID=${organizationID}`, + this.applicationServiceClient, + req, + 100, + undefined + )); - // otherwise create new application - if (!applicationId) { - applicationId = await this.createNewApplication(iotDevice.application.name, iotDevice.application.id); - } + // if application exist use it + let applicationId = applications.resultList.find( + element => element.id === iotDevice.chirpstackApplicationId || element.id === iotDevice.application.chirpstackId + )?.id; - return applicationId; + // otherwise create new application + if (!applicationId) { + applicationId = await this.createNewApplication(iotDevice.application.name, iotDevice.application.id); } - public async createNewApplication(name: string, id: number) { - const applicationId = await this.createChirpstackApplication({ - application: { - name: `${name}`, - description: this.DEFAULT_DESCRIPTION, - }, - }); - const existingApplication = await this.applicationRepository.findOneOrFail({ - where: { id: id }, - }); - existingApplication.chirpstackId = applicationId; - await this.applicationRepository.save(existingApplication); - return applicationId; - } + return applicationId; + } - public async createChirpstackApplication(dto: CreateChirpstackApplicationDto): Promise { - const req = new CreateApplicationRequest(); - const application = new ChirpstackApplication(); - application.setDescription( - dto.application.description ? dto.application.description : this.DEFAULT_DESCRIPTION - ); - application.setName(this.applicationNamePrefix + dto.application.name); - application.setTenantId(await this.getDefaultOrganizationId()); + public async createNewApplication(name: string, id: number) { + const applicationId = await this.createChirpstackApplication({ + application: { + name: `${name}`, + description: this.DEFAULT_DESCRIPTION, + }, + }); + const existingApplication = await this.applicationRepository.findOneOrFail({ + where: { id: id }, + }); + existingApplication.chirpstackId = applicationId; + await this.applicationRepository.save(existingApplication); + return applicationId; + } - req.setApplication(application); - const applicationIdObject: IdResponse = await this.post("applications", this.applicationServiceClient, req); - return applicationIdObject.id; - } + public async createChirpstackApplication(dto: CreateChirpstackApplicationDto): Promise { + const req = new CreateApplicationRequest(); + const application = new ChirpstackApplication(); + application.setDescription(dto.application.description ? dto.application.description : this.DEFAULT_DESCRIPTION); + application.setName(this.applicationNamePrefix + dto.application.name); + application.setTenantId(await this.getDefaultOrganizationId()); + + req.setApplication(application); + const applicationIdObject: IdResponse = await this.post("applications", this.applicationServiceClient, req); + return applicationIdObject.id; + } - public async deleteApplication(id: string): Promise { - const req = new DeleteApplicationRequest(); - req.setId(id); - try { - return await this.delete("applications", this.applicationServiceClient, req); - } catch (e) { - throw e; - } + public async deleteApplication(id: string): Promise { + const req = new DeleteApplicationRequest(); + req.setId(id); + try { + return await this.delete("applications", this.applicationServiceClient, req); + } catch (e) { + throw e; + } + } + public async updateApplication(dto: Application): Promise { + if (!dto.chirpstackId) { + return; } - public async updateApplication(dto: Application): Promise { - if (!dto.chirpstackId) { - return; - } - const req = new UpdateApplicationRequest(); - const application = new ChirpstackApplication(); - application.setId(dto.chirpstackId); - application.setDescription(dto.description ? dto.description : this.DEFAULT_DESCRIPTION); - application.setName(this.applicationNamePrefix + dto.name); - application.setTenantId(await this.getDefaultOrganizationId()); - req.setApplication(application); - try { - await this.put("applications", this.applicationServiceClient, req); - } catch (e) { - throw e; - } + const req = new UpdateApplicationRequest(); + const application = new ChirpstackApplication(); + application.setId(dto.chirpstackId); + application.setDescription(dto.description ? dto.description : this.DEFAULT_DESCRIPTION); + application.setName(this.applicationNamePrefix + dto.name); + application.setTenantId(await this.getDefaultOrganizationId()); + req.setApplication(application); + try { + await this.put("applications", this.applicationServiceClient, req); + } catch (e) { + throw e; } + } } diff --git a/src/services/chirpstack/chirpstack-device.service.ts b/src/services/chirpstack/chirpstack-device.service.ts index aad7aa6f..4a873095 100644 --- a/src/services/chirpstack/chirpstack-device.service.ts +++ b/src/services/chirpstack/chirpstack-device.service.ts @@ -7,8 +7,8 @@ import { CreateLoRaWANSettingsDto } from "@dto/create-lorawan-settings.dto"; import { GenericChirpstackConfigurationService } from "@services/chirpstack/generic-chirpstack-configuration.service"; import { CreateChirpstackDeviceQueueItemDto } from "@dto/chirpstack/create-chirpstack-device-queue-item.dto"; import { - DeviceDownlinkQueueResponseDto, - DeviceQueueItem, + DeviceDownlinkQueueResponseDto, + DeviceQueueItem, } from "@dto/chirpstack/chirpstack-device-downlink-queue-response.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; import { IoTDevice } from "@entities/iot-device.entity"; @@ -20,28 +20,28 @@ import { ConfigService } from "@nestjs/config"; import { DeviceProfileService } from "@services/chirpstack/device-profile.service"; import { ServiceError } from "@grpc/grpc-js"; import { - ActivateDeviceRequest, - CreateDeviceKeysRequest, - CreateDeviceRequest, - DeleteDeviceRequest, - Device, - DeviceActivation, - DeviceKeys, - DeviceQueueItem as DeviceQueueItemChirpstack, - EnqueueDeviceQueueItemRequest, - FlushDeviceQueueRequest, - GetDeviceActivationRequest, - GetDeviceActivationResponse, - GetDeviceKeysRequest, - GetDeviceKeysResponse, - GetDeviceLinkMetricsRequest, - GetDeviceLinkMetricsResponse, - GetDeviceQueueItemsRequest, - GetDeviceQueueItemsResponse, - GetDeviceRequest, - GetDeviceResponse, - UpdateDeviceKeysRequest, - UpdateDeviceRequest, + ActivateDeviceRequest, + CreateDeviceKeysRequest, + CreateDeviceRequest, + DeleteDeviceRequest, + Device, + DeviceActivation, + DeviceKeys, + DeviceQueueItem as DeviceQueueItemChirpstack, + EnqueueDeviceQueueItemRequest, + FlushDeviceQueueRequest, + GetDeviceActivationRequest, + GetDeviceActivationResponse, + GetDeviceKeysRequest, + GetDeviceKeysResponse, + GetDeviceLinkMetricsRequest, + GetDeviceLinkMetricsResponse, + GetDeviceQueueItemsRequest, + GetDeviceQueueItemsResponse, + GetDeviceRequest, + GetDeviceResponse, + UpdateDeviceKeysRequest, + UpdateDeviceRequest, } from "@chirpstack/chirpstack-api/api/device_pb"; import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; import { dateToTimestamp } from "@helpers/date.helper"; @@ -50,538 +50,534 @@ import { Aggregation } from "@chirpstack/chirpstack-api/common/common_pb"; import { DeviceMetricsDto, MetricProperties } from "@dto/chirpstack/chirpstack-device-metrics.dto"; @Injectable() export class ChirpstackDeviceService extends GenericChirpstackConfigurationService { - - constructor(private configService: ConfigService, private deviceProfileService: DeviceProfileService) { - super(); - - this.deviceStatsIntervalInDays = configService.get("backend.deviceStatsIntervalInDays"); + constructor(private configService: ConfigService, private deviceProfileService: DeviceProfileService) { + super(); + + this.deviceStatsIntervalInDays = configService.get("backend.deviceStatsIntervalInDays"); + } + + private readonly logger = new Logger(ChirpstackDeviceService.name); + + DEVICE_NAME_PREFIX = "OS2IOT-"; + DEFAULT_DESCRIPTION = "Created by OS2IoT"; + private readonly deviceStatsIntervalInDays: number; + + public makeCreateChirpstackDeviceDto(dto: CreateLoRaWANSettingsDto, name: string): CreateChirpstackDeviceDto { + const csDto = new ChirpstackDeviceContentsDto(); + csDto.name = `${this.DEVICE_NAME_PREFIX}${name}`.toLowerCase(); + csDto.description = this.DEFAULT_DESCRIPTION; + csDto.devEUI = dto.devEUI; + csDto.deviceProfileID = dto.deviceProfileID; + + csDto.isDisabled = dto.isDisabled; + csDto.skipFCntCheck = dto.skipFCntCheck; + + return { device: csDto }; + } + + public async overwriteDownlink(dto: CreateChirpstackDeviceQueueItemDto): Promise { + await this.deleteDownlinkQueue(dto.deviceQueueItem.devEUI); + try { + const req = new EnqueueDeviceQueueItemRequest(); + const queueItem = new DeviceQueueItemChirpstack(); + queueItem.setConfirmed(dto.deviceQueueItem.confirmed); + queueItem.setData(dto.deviceQueueItem.data); + queueItem.setDevEui(dto.deviceQueueItem.devEUI); + queueItem.setFPort(dto.deviceQueueItem.fPort); + req.setQueueItem(queueItem); + + const res = await this.postDownlink(req); + return res; + } catch (err) { + const fcntError = "enqueue downlink payload error: get next downlink fcnt for deveui error"; + if (err?.response?.data?.error?.startsWith(fcntError)) { + throw new BadRequestException(ErrorCodes.DeviceIsNotActivatedInChirpstack); + } + + throw err; } - - private readonly logger = new Logger(ChirpstackDeviceService.name); - - DEVICE_NAME_PREFIX = "OS2IOT-"; - DEFAULT_DESCRIPTION = "Created by OS2IoT"; - private readonly deviceStatsIntervalInDays: number; - - public makeCreateChirpstackDeviceDto(dto: CreateLoRaWANSettingsDto, name: string): CreateChirpstackDeviceDto { - const csDto = new ChirpstackDeviceContentsDto(); - csDto.name = `${this.DEVICE_NAME_PREFIX}${name}`.toLowerCase(); - csDto.description = this.DEFAULT_DESCRIPTION; - csDto.devEUI = dto.devEUI; - csDto.deviceProfileID = dto.deviceProfileID; - - csDto.isDisabled = dto.isDisabled; - csDto.skipFCntCheck = dto.skipFCntCheck; - - return { device: csDto }; - } - - public async overwriteDownlink(dto: CreateChirpstackDeviceQueueItemDto): Promise { - await this.deleteDownlinkQueue(dto.deviceQueueItem.devEUI); - try { - const req = new EnqueueDeviceQueueItemRequest(); - const queueItem = new DeviceQueueItemChirpstack(); - queueItem.setConfirmed(dto.deviceQueueItem.confirmed); - queueItem.setData(dto.deviceQueueItem.data); - queueItem.setDevEui(dto.deviceQueueItem.devEUI); - queueItem.setFPort(dto.deviceQueueItem.fPort); - req.setQueueItem(queueItem); - - const res = await this.postDownlink(req); - return res; - } catch (err) { - const fcntError = "enqueue downlink payload error: get next downlink fcnt for deveui error"; - if (err?.response?.data?.error?.startsWith(fcntError)) { - throw new BadRequestException(ErrorCodes.DeviceIsNotActivatedInChirpstack); - } - - throw err; - } - } - - public async deleteDevice(deviceEUI: string): Promise { - try { - const req = new DeleteDeviceRequest(); - req.setDevEui(deviceEUI); - await this.delete(`devices`, this.deviceServiceClient, req); - } catch (err) { - throw err; - } - } - - public async getDownlinkQueue(deviceEUI: string): Promise { - const req = new GetDeviceQueueItemsRequest(); - req.setDevEui(deviceEUI); - const res = await this.getQueue(req); - - const queueItems: DeviceQueueItem[] = res.getResultList().map(queueItem => { - return { - confirmed: queueItem.getConfirmed(), - devEUI: queueItem.getDevEui(), - fCnt: queueItem.getFCntDown(), - fPort: queueItem.getFPort(), - data: queueItem.getData_asB64(), - }; - }); - - const responseDto: DeviceDownlinkQueueResponseDto = { - totalCount: res.getTotalCount(), - deviceQueueItems: queueItems, - }; - return responseDto; - } - - private async deleteDownlinkQueue(deviceEUI: string): Promise { - const req = new FlushDeviceQueueRequest(); - req.setDevEui(deviceEUI); - await this.deleteQueue(req); + } + + public async deleteDevice(deviceEUI: string): Promise { + try { + const req = new DeleteDeviceRequest(); + req.setDevEui(deviceEUI); + await this.delete(`devices`, this.deviceServiceClient, req); + } catch (err) { + throw err; } - - public async activateDeviceWithABP( - devEUI: string, - devAddr: string, - fCntUp: number, - nFCntDown: number, - networkSessionKey: string, - applicationSessionKey: string - ): Promise { - const res = await this.createOrUpdateABPActivation( - devAddr, - networkSessionKey, - applicationSessionKey, - fCntUp, - nFCntDown, - devEUI - ); - if (!res) { - this.logger.warn(`Could not ABP activate Chirpstack Device using DEVEUI: ${devEUI}}`); - } - } - - private async createOrUpdateABPActivation( - devAddr: string, - networkSessionKey: string, - applicationSessionKey: string, - fCntUp: number, - nFCntDown: number, - devEUI: string - ) { - const req = new ActivateDeviceRequest(); - const deviceActivation = this.mapActivationToChirpstack( - devAddr, - networkSessionKey, - applicationSessionKey, - fCntUp, - nFCntDown, - devEUI - ); - req.setDeviceActivation(deviceActivation); - try { - await this.postActivation(req); - } catch (e) { - return false; - } - - return true; - } - private mapActivationToChirpstack( - devAddr: string, - networkSessionKey: string, - applicationSessionKey: string, - fCntUp: number, - nFCntDown: number, - devEUI: string - ) { - const deviceActivation = new DeviceActivation(); - deviceActivation.setDevAddr(devAddr); - deviceActivation.setNwkSEncKey(networkSessionKey); - deviceActivation.setAppSKey(applicationSessionKey); - deviceActivation.setFCntUp(fCntUp); - deviceActivation.setNFCntDown(nFCntDown); - deviceActivation.setDevEui(devEUI); - deviceActivation.setFNwkSIntKey(networkSessionKey); - deviceActivation.setSNwkSIntKey(networkSessionKey); - return deviceActivation; - } - - public async activateDeviceWithOTAA(deviceEUI: string, nwkKey: string, isUpdate: boolean): Promise { - try { - if (isUpdate) { - const req = new UpdateDeviceKeysRequest(); - const deviceKeys = this.mapDeviceKeysToChirpstack(deviceEUI, nwkKey); - req.setDeviceKeys(deviceKeys); - await this.putKeys(req); - } else { - const req = new CreateDeviceKeysRequest(); - const deviceKeys = this.mapDeviceKeysToChirpstack(deviceEUI, nwkKey); - req.setDeviceKeys(deviceKeys); - await this.postKeys(req); - } - } catch (e) { - return false; - } - - return true; + } + + public async getDownlinkQueue(deviceEUI: string): Promise { + const req = new GetDeviceQueueItemsRequest(); + req.setDevEui(deviceEUI); + const res = await this.getQueue(req); + + const queueItems: DeviceQueueItem[] = res.getResultList().map(queueItem => { + return { + confirmed: queueItem.getConfirmed(), + devEUI: queueItem.getDevEui(), + fCnt: queueItem.getFCntDown(), + fPort: queueItem.getFPort(), + data: queueItem.getData_asB64(), + }; + }); + + const responseDto: DeviceDownlinkQueueResponseDto = { + totalCount: res.getTotalCount(), + deviceQueueItems: queueItems, + }; + return responseDto; + } + + private async deleteDownlinkQueue(deviceEUI: string): Promise { + const req = new FlushDeviceQueueRequest(); + req.setDevEui(deviceEUI); + await this.deleteQueue(req); + } + + public async activateDeviceWithABP( + devEUI: string, + devAddr: string, + fCntUp: number, + nFCntDown: number, + networkSessionKey: string, + applicationSessionKey: string + ): Promise { + const res = await this.createOrUpdateABPActivation( + devAddr, + networkSessionKey, + applicationSessionKey, + fCntUp, + nFCntDown, + devEUI + ); + if (!res) { + this.logger.warn(`Could not ABP activate Chirpstack Device using DEVEUI: ${devEUI}}`); } - - private mapDeviceKeysToChirpstack(deviceEUI: string, nwkKey: string) { - const deviceKeys = new DeviceKeys(); - deviceKeys.setDevEui(deviceEUI); - deviceKeys.setNwkKey(nwkKey); - return deviceKeys; + } + + private async createOrUpdateABPActivation( + devAddr: string, + networkSessionKey: string, + applicationSessionKey: string, + fCntUp: number, + nFCntDown: number, + devEUI: string + ) { + const req = new ActivateDeviceRequest(); + const deviceActivation = this.mapActivationToChirpstack( + devAddr, + networkSessionKey, + applicationSessionKey, + fCntUp, + nFCntDown, + devEUI + ); + req.setDeviceActivation(deviceActivation); + try { + await this.postActivation(req); + } catch (e) { + return false; } - public async createOrUpdateDevice( - dto: CreateChirpstackDeviceDto, - lorawanDevices: ChirpstackDeviceId[] = null - ): Promise { - try { - if (await this.isDeviceAlreadyCreated(dto.device.devEUI, lorawanDevices)) { - const req = new UpdateDeviceRequest(); - const device = this.mapDeviceToChirpstack(dto); - req.setDevice(device); - await this.put(`devices`, this.deviceServiceClient, req); - } else { - const req = new CreateDeviceRequest(); - const device = this.mapDeviceToChirpstack(dto); - req.setDevice(device); - await this.post(`devices`, this.deviceServiceClient, req); - } - } catch (e) { - this.logger.error(`Update or Post device got error: ${e}`); - return false; - } - return true; + return true; + } + private mapActivationToChirpstack( + devAddr: string, + networkSessionKey: string, + applicationSessionKey: string, + fCntUp: number, + nFCntDown: number, + devEUI: string + ) { + const deviceActivation = new DeviceActivation(); + deviceActivation.setDevAddr(devAddr); + deviceActivation.setNwkSEncKey(networkSessionKey); + deviceActivation.setAppSKey(applicationSessionKey); + deviceActivation.setFCntUp(fCntUp); + deviceActivation.setNFCntDown(nFCntDown); + deviceActivation.setDevEui(devEUI); + deviceActivation.setFNwkSIntKey(networkSessionKey); + deviceActivation.setSNwkSIntKey(networkSessionKey); + return deviceActivation; + } + + public async activateDeviceWithOTAA(deviceEUI: string, nwkKey: string, isUpdate: boolean): Promise { + try { + if (isUpdate) { + const req = new UpdateDeviceKeysRequest(); + const deviceKeys = this.mapDeviceKeysToChirpstack(deviceEUI, nwkKey); + req.setDeviceKeys(deviceKeys); + await this.putKeys(req); + } else { + const req = new CreateDeviceKeysRequest(); + const deviceKeys = this.mapDeviceKeysToChirpstack(deviceEUI, nwkKey); + req.setDeviceKeys(deviceKeys); + await this.postKeys(req); + } + } catch (e) { + return false; } - private mapDeviceToChirpstack(dto: CreateChirpstackDeviceDto): Device { - const device = new Device(); - device.setApplicationId(dto.device.applicationID); - device.setDescription(dto.device.description); - device.setDevEui(dto.device.devEUI); - device.setDeviceProfileId(dto.device.deviceProfileID); - device.setIsDisabled(dto.device.isDisabled); - device.setName(dto.device.name); - device.setSkipFcntCheck(dto.device.skipFCntCheck); - return device; + return true; + } + + private mapDeviceKeysToChirpstack(deviceEUI: string, nwkKey: string) { + const deviceKeys = new DeviceKeys(); + deviceKeys.setDevEui(deviceEUI); + deviceKeys.setNwkKey(nwkKey); + return deviceKeys; + } + + public async createOrUpdateDevice( + dto: CreateChirpstackDeviceDto, + lorawanDevices: ChirpstackDeviceId[] = null + ): Promise { + try { + if (await this.isDeviceAlreadyCreated(dto.device.devEUI, lorawanDevices)) { + const req = new UpdateDeviceRequest(); + const device = this.mapDeviceToChirpstack(dto); + req.setDevice(device); + await this.put(`devices`, this.deviceServiceClient, req); + } else { + const req = new CreateDeviceRequest(); + const device = this.mapDeviceToChirpstack(dto); + req.setDevice(device); + await this.post(`devices`, this.deviceServiceClient, req); + } + } catch (e) { + this.logger.error(`Update or Post device got error: ${e}`); + return false; } - - public async getChirpstackDevice(id: string): Promise { - try { - const req = new GetDeviceRequest(); - req.setDevEui(id); - - const res = await this.get(`devices/${id}`, this.deviceServiceClient, req); - const device = res.getDevice(); - - const deviceDto: ChirpstackDeviceContentsDto = { - deviceStatusBattery: res.getDeviceStatus()?.getBatteryLevel(), - deviceStatusMargin: res.getDeviceStatus()?.getMargin(), - devEUI: device.getDevEui(), - deviceProfileID: device.getDeviceProfileId(), - applicationID: device.getApplicationId(), - description: device.getDescription(), - isDisabled: device.getIsDisabled(), - name: device.getName(), - skipFCntCheck: device.getSkipFcntCheck(), - tags: device.getTagsMap().toObject(), - variables: device.getVariablesMap().toObject(), - }; - - return deviceDto; - } catch (err) { - throw new BadRequestException(ErrorCodes.CouldntGetApplications); - } + return true; + } + + private mapDeviceToChirpstack(dto: CreateChirpstackDeviceDto): Device { + const device = new Device(); + device.setApplicationId(dto.device.applicationID); + device.setDescription(dto.device.description); + device.setDevEui(dto.device.devEUI); + device.setDeviceProfileId(dto.device.deviceProfileID); + device.setIsDisabled(dto.device.isDisabled); + device.setName(dto.device.name); + device.setSkipFcntCheck(dto.device.skipFCntCheck); + return device; + } + + public async getChirpstackDevice(id: string): Promise { + try { + const req = new GetDeviceRequest(); + req.setDevEui(id); + + const res = await this.get(`devices/${id}`, this.deviceServiceClient, req); + const device = res.getDevice(); + + const deviceDto: ChirpstackDeviceContentsDto = { + deviceStatusBattery: res.getDeviceStatus()?.getBatteryLevel(), + deviceStatusMargin: res.getDeviceStatus()?.getMargin(), + devEUI: device.getDevEui(), + deviceProfileID: device.getDeviceProfileId(), + applicationID: device.getApplicationId(), + description: device.getDescription(), + isDisabled: device.getIsDisabled(), + name: device.getName(), + skipFCntCheck: device.getSkipFcntCheck(), + tags: device.getTagsMap().toObject(), + variables: device.getVariablesMap().toObject(), + }; + + return deviceDto; + } catch (err) { + throw new BadRequestException(ErrorCodes.CouldntGetApplications); } - - private async getDeviceKeys(deviceId: string): Promise { - try { - const req = new GetDeviceKeysRequest(); - req.setDevEui(deviceId); - - const res = (await this.getKeys(req)).getDeviceKeys(); - - const keysDto: ChirpstackDeviceKeysContentDto = { - appKey: res.getAppKey(), - devEUI: res.getDevEui(), - nwkKey: res.getNwkKey(), - }; - - return keysDto; - } catch (err) { - // Chirpstack returns 404 if keys are not saved .. - // It seems like that the current logic is using this catch to see if the device is an ABP or OTAA device. - return new ChirpstackDeviceKeysContentDto(); - } + } + + private async getDeviceKeys(deviceId: string): Promise { + try { + const req = new GetDeviceKeysRequest(); + req.setDevEui(deviceId); + + const res = (await this.getKeys(req)).getDeviceKeys(); + + const keysDto: ChirpstackDeviceKeysContentDto = { + appKey: res.getAppKey(), + devEUI: res.getDevEui(), + nwkKey: res.getNwkKey(), + }; + + return keysDto; + } catch (err) { + // Chirpstack returns 404 if keys are not saved .. + // It seems like that the current logic is using this catch to see if the device is an ABP or OTAA device. + return new ChirpstackDeviceKeysContentDto(); } - - private async getDeviceActivation(deviceId: string): Promise { - try { - const req = new GetDeviceActivationRequest(); - req.setDevEui(deviceId); - - const res = (await this.getActivation(req)).getDeviceActivation(); - - const activationDto: ChirpstackDeviceActivationContentsDto = { - aFCntDown: res.getAFCntDown(), - devEUI: res.getDevEui(), - appSKey: res.getAppSKey(), - devAddr: res.getDevAddr(), - fCntUp: res.getFCntUp(), - fNwkSIntKey: res.getFNwkSIntKey(), - nFCntDown: res.getAFCntDown(), - nwkSEncKey: res.getNwkSEncKey(), - sNwkSIntKey: res.getSNwkSIntKey(), - }; - return activationDto; - } catch (err) { - return new ChirpstackDeviceActivationContentsDto(); - } + } + + private async getDeviceActivation(deviceId: string): Promise { + try { + const req = new GetDeviceActivationRequest(); + req.setDevEui(deviceId); + + const res = (await this.getActivation(req)).getDeviceActivation(); + + const activationDto: ChirpstackDeviceActivationContentsDto = { + aFCntDown: res.getAFCntDown(), + devEUI: res.getDevEui(), + appSKey: res.getAppSKey(), + devAddr: res.getDevAddr(), + fCntUp: res.getFCntUp(), + fNwkSIntKey: res.getFNwkSIntKey(), + nFCntDown: res.getAFCntDown(), + nwkSEncKey: res.getNwkSEncKey(), + sNwkSIntKey: res.getSNwkSIntKey(), + }; + return activationDto; + } catch (err) { + return new ChirpstackDeviceActivationContentsDto(); } - - /** - * Fetch and set LoRaWAN settings on the given device. This is not immutable. - * @param iotDevice - * @param applications - * @returns The mutated device - */ - public async enrichLoRaWANDevice(iotDevice: IoTDevice): Promise { - const loraDevice = iotDevice as LoRaWANDeviceWithChirpstackDataDto; - loraDevice.lorawanSettings = new CreateLoRaWANSettingsDto(); - await this.mapActivationAndKeys(loraDevice); - const csData = await this.getChirpstackDevice(loraDevice.deviceEUI); - loraDevice.lorawanSettings.devEUI = csData.devEUI; - loraDevice.lorawanSettings.deviceProfileID = csData.deviceProfileID; - loraDevice.lorawanSettings.skipFCntCheck = csData.skipFCntCheck; - loraDevice.lorawanSettings.isDisabled = csData.isDisabled; - loraDevice.lorawanSettings.deviceStatusBattery = csData.deviceStatusBattery; - loraDevice.lorawanSettings.deviceStatusMargin = csData.deviceStatusMargin; - - const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById(csData.deviceProfileID); - loraDevice.deviceProfileName = deviceProfile.deviceProfile.name; - - return loraDevice; + } + + /** + * Fetch and set LoRaWAN settings on the given device. This is not immutable. + * @param iotDevice + * @param applications + * @returns The mutated device + */ + public async enrichLoRaWANDevice(iotDevice: IoTDevice): Promise { + const loraDevice = iotDevice as LoRaWANDeviceWithChirpstackDataDto; + loraDevice.lorawanSettings = new CreateLoRaWANSettingsDto(); + await this.mapActivationAndKeys(loraDevice); + const csData = await this.getChirpstackDevice(loraDevice.deviceEUI); + loraDevice.lorawanSettings.devEUI = csData.devEUI; + loraDevice.lorawanSettings.deviceProfileID = csData.deviceProfileID; + loraDevice.lorawanSettings.skipFCntCheck = csData.skipFCntCheck; + loraDevice.lorawanSettings.isDisabled = csData.isDisabled; + loraDevice.lorawanSettings.deviceStatusBattery = csData.deviceStatusBattery; + loraDevice.lorawanSettings.deviceStatusMargin = csData.deviceStatusMargin; + + const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById(csData.deviceProfileID); + loraDevice.deviceProfileName = deviceProfile.deviceProfile.name; + + return loraDevice; + } + + private async mapActivationAndKeys(loraDevice: LoRaWANDeviceWithChirpstackDataDto) { + const keys = await this.getDeviceKeys(loraDevice.deviceEUI); + if (keys.nwkKey) { + // OTAA + loraDevice.lorawanSettings.activationType = ActivationType.OTAA; + loraDevice.lorawanSettings.OTAAapplicationKey = keys.nwkKey; + loraDevice.OTAAapplicationKey = keys.nwkKey; + } else { + const activation = await this.getDeviceActivation(loraDevice.deviceEUI); + if (activation.devAddr != null) { + // ABP + loraDevice.lorawanSettings.activationType = ActivationType.ABP; + loraDevice.lorawanSettings.devAddr = activation.devAddr; + loraDevice.lorawanSettings.fCntUp = activation.fCntUp; + loraDevice.lorawanSettings.nFCntDown = activation.nFCntDown; + loraDevice.lorawanSettings.networkSessionKey = activation.nwkSEncKey; + loraDevice.lorawanSettings.applicationSessionKey = activation.appSKey; + } else { + loraDevice.lorawanSettings.activationType = ActivationType.NONE; + } } - - private async mapActivationAndKeys(loraDevice: LoRaWANDeviceWithChirpstackDataDto) { - const keys = await this.getDeviceKeys(loraDevice.deviceEUI); - if (keys.nwkKey) { - // OTAA - loraDevice.lorawanSettings.activationType = ActivationType.OTAA; - loraDevice.lorawanSettings.OTAAapplicationKey = keys.nwkKey; - loraDevice.OTAAapplicationKey = keys.nwkKey; + } + + public async isDeviceAlreadyCreated(deviceEUI: string, chirpstackIds: ChirpstackDeviceId[] = null): Promise { + const alreadyExists = chirpstackIds.some(x => x.devEUI.toLowerCase() === deviceEUI.toLowerCase()); + return alreadyExists; + } + + public async getStats(deviceEUI: string): Promise { + const now = new Date(); + const to_time = dateToTimestamp(now); + const from_time = new Date(new Date().setDate(now.getDate() - this.deviceStatsIntervalInDays)); + const from_time_timestamp: Timestamp = dateToTimestamp(from_time); + + const req = new GetDeviceLinkMetricsRequest(); + req.setDevEui(deviceEUI); + req.setStart(from_time_timestamp); + req.setEnd(to_time); + req.setAggregation(Aggregation.DAY); + const metaData = this.makeMetadataHeader(); + + const getDeviceMetricsPromise = new Promise((resolve, reject) => { + this.deviceServiceClient.getLinkMetrics(req, metaData, (err, resp) => { + if (err) { + reject(err); } else { - const activation = await this.getDeviceActivation(loraDevice.deviceEUI); - if (activation.devAddr != null) { - // ABP - loraDevice.lorawanSettings.activationType = ActivationType.ABP; - loraDevice.lorawanSettings.devAddr = activation.devAddr; - loraDevice.lorawanSettings.fCntUp = activation.fCntUp; - loraDevice.lorawanSettings.nFCntDown = activation.nFCntDown; - loraDevice.lorawanSettings.networkSessionKey = activation.nwkSEncKey; - loraDevice.lorawanSettings.applicationSessionKey = activation.appSKey; - } else { - loraDevice.lorawanSettings.activationType = ActivationType.NONE; - } + resolve(resp); } + }); + }); + try { + const metrics = await getDeviceMetricsPromise; + return this.mapMetrics(metrics); + } catch (err) { + throw new BadRequestException(err); } - - public async isDeviceAlreadyCreated( - deviceEUI: string, - chirpstackIds: ChirpstackDeviceId[] = null - ): Promise { - const alreadyExists = chirpstackIds.some(x => x.devEUI.toLowerCase() === deviceEUI.toLowerCase()); - return alreadyExists; - } - - public async getStats(deviceEUI: string): Promise { - const now = new Date(); - const to_time = dateToTimestamp(now); - const from_time = new Date(new Date().setDate(now.getDate() - this.deviceStatsIntervalInDays)); - const from_time_timestamp: Timestamp = dateToTimestamp(from_time); - - const req = new GetDeviceLinkMetricsRequest(); - req.setDevEui(deviceEUI); - req.setStart(from_time_timestamp); - req.setEnd(to_time); - req.setAggregation(Aggregation.DAY); - const metaData = this.makeMetadataHeader(); - - const getDeviceMetricsPromise = new Promise((resolve, reject) => { - this.deviceServiceClient.getLinkMetrics(req, metaData, (err, resp) => { - if (err) { - reject(err); - } else { - resolve(resp); - } - }); - }); - try { - const metrics = await getDeviceMetricsPromise; - return this.mapMetrics(metrics); - } catch (err) { - throw new BadRequestException(err); - } - } - private mapMetrics(metrics: GetDeviceLinkMetricsResponse): LoRaWANStatsResponseDto { - const statsElementDto: LoRaWANStatsElementDto[] = []; - const deviceMetrics: DeviceMetricsDto = {}; - - const rssiTimestamp = metrics.getGwRssi().getTimestampsList(); - const rssis = metrics - .getGwRssi() - .getDatasetsList() - .find(e => e.getLabel() === "rssi") - .getDataList(); - - this.processPackets(rssiTimestamp, rssis, MetricProperties.rssi, deviceMetrics); - - const snrTimestamp = metrics.getGwSnr().getTimestampsList(); - const snr = metrics - .getGwSnr() - .getDatasetsList() - .find(e => e.getLabel() === "snr") - .getDataList(); - - this.processPackets(snrTimestamp, snr, MetricProperties.snr, deviceMetrics); - - const drTimestamp = metrics.getRxPacketsPerDr().getTimestampsList(); - const drDatasets = metrics.getRxPacketsPerDr().getDatasetsList(); - - drDatasets.forEach(drDataset => { - const drLabel = drDataset.getLabel(); - const drData = drDataset.getDataList(); - this.processPackets(drTimestamp, drData, MetricProperties.dr, deviceMetrics, drLabel); - }); - - Object.keys(deviceMetrics).forEach(timestamp => { - const packetCount = deviceMetrics[timestamp]; - const dto: LoRaWANStatsElementDto = { - timestamp, - gwRssi: packetCount.rssi, - gwSnr: packetCount.snr, - rxPacketsPerDr: packetCount.rxPacketsPerDr, - }; - statsElementDto.push(dto); - }); - return { result: statsElementDto }; - } - private processPackets = ( - timestamps: Array, - packets: number[], - key: string, - packetCounts: DeviceMetricsDto, - drLabel?: string - ) => { - timestamps.forEach((timestamp, index) => { - const isoTimestamp = timestamp.toDate().toISOString(); - packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { - rssi: 0, - snr: 0, - rxPacketsPerDr: {}, - }; - - if (drLabel) { - packetCounts[isoTimestamp].rxPacketsPerDr[drLabel as any] = packets[index]; - } else { - (packetCounts[isoTimestamp] as any)[key] = packets[index]; - } - }); - }; - - private async getKeys(request: GetDeviceKeysRequest): Promise { - return await this.makeRequest( - request, - this.deviceServiceClient.getKeys, - "GET KEYS success", - "GET KEYS failed and got error: " - ); - } - - private async postKeys(request: CreateDeviceKeysRequest): Promise { - await this.makeRequest( - request, - this.deviceServiceClient.createKeys, - "POST KEYS success", - "POST KEYS failed and got error: " - ); - } - - private async putKeys(request: UpdateDeviceKeysRequest): Promise { - await this.makeRequest( - request, - this.deviceServiceClient.updateKeys, - "UPDATE KEYS success", - "UPDATE KEYS failed and got error: " - ); - } - - private async getQueue(request: GetDeviceQueueItemsRequest): Promise { - return await this.makeRequest( - request, - this.deviceServiceClient.getQueue, - "GET QUEUE success", - "GET QUEUE failed and got error: " - ); - } - - private async deleteQueue(request: FlushDeviceQueueRequest): Promise { - await this.makeRequest( - request, - this.deviceServiceClient.flushQueue, - "DELETE QUEUE success", - "DELETE QUEUE failed and got error: " - ); - } - - private async postDownlink(request: EnqueueDeviceQueueItemRequest): Promise { - return await this.makeRequest( - request, - this.deviceServiceClient.enqueue, - "POST DOWNLINK success", - "POST DOWNLINK failed and got error :" - ); - } - - private async getActivation(request: GetDeviceActivationRequest): Promise { - return await this.makeRequest( - request, - this.deviceServiceClient.getActivation, - "GET ACTIVATION success", - "GET ACTIVATION failed and got error: " - ); - } - - private async postActivation(request?: ActivateDeviceRequest): Promise { - await this.makeRequest( - request, - this.deviceServiceClient.activate, - "post ACTIVATION success", - "GET activation failed and got error: " - ); - } - private async makeRequest( - request: any, - method: (request: any, metaData: any, callback: (err: ServiceError, resp: any) => void) => void, - successMessage: string, - errorMessage: string - ): Promise { - const metaData = this.makeMetadataHeader(); - const promise = new Promise((resolve, reject) => { - method.call(this.deviceServiceClient, request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.logger.debug(successMessage); - resolve(resp); - } - }); - }); - try { - return await promise; - } catch (err) { - this.logger.error(JSON.stringify(errorMessage)); - throw new InternalServerErrorException(); + } + private mapMetrics(metrics: GetDeviceLinkMetricsResponse): LoRaWANStatsResponseDto { + const statsElementDto: LoRaWANStatsElementDto[] = []; + const deviceMetrics: DeviceMetricsDto = {}; + + const rssiTimestamp = metrics.getGwRssi().getTimestampsList(); + const rssis = metrics + .getGwRssi() + .getDatasetsList() + .find(e => e.getLabel() === "rssi") + .getDataList(); + + this.processPackets(rssiTimestamp, rssis, MetricProperties.rssi, deviceMetrics); + + const snrTimestamp = metrics.getGwSnr().getTimestampsList(); + const snr = metrics + .getGwSnr() + .getDatasetsList() + .find(e => e.getLabel() === "snr") + .getDataList(); + + this.processPackets(snrTimestamp, snr, MetricProperties.snr, deviceMetrics); + + const drTimestamp = metrics.getRxPacketsPerDr().getTimestampsList(); + const drDatasets = metrics.getRxPacketsPerDr().getDatasetsList(); + + drDatasets.forEach(drDataset => { + const drLabel = drDataset.getLabel(); + const drData = drDataset.getDataList(); + this.processPackets(drTimestamp, drData, MetricProperties.dr, deviceMetrics, drLabel); + }); + + Object.keys(deviceMetrics).forEach(timestamp => { + const packetCount = deviceMetrics[timestamp]; + const dto: LoRaWANStatsElementDto = { + timestamp, + gwRssi: packetCount.rssi, + gwSnr: packetCount.snr, + rxPacketsPerDr: packetCount.rxPacketsPerDr, + }; + statsElementDto.push(dto); + }); + return { result: statsElementDto }; + } + private processPackets = ( + timestamps: Array, + packets: number[], + key: string, + packetCounts: DeviceMetricsDto, + drLabel?: string + ) => { + timestamps.forEach((timestamp, index) => { + const isoTimestamp = timestamp.toDate().toISOString(); + packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { + rssi: 0, + snr: 0, + rxPacketsPerDr: {}, + }; + + if (drLabel) { + packetCounts[isoTimestamp].rxPacketsPerDr[drLabel as any] = packets[index]; + } else { + (packetCounts[isoTimestamp] as any)[key] = packets[index]; + } + }); + }; + + private async getKeys(request: GetDeviceKeysRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.getKeys, + "GET KEYS success", + "GET KEYS failed and got error: " + ); + } + + private async postKeys(request: CreateDeviceKeysRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.createKeys, + "POST KEYS success", + "POST KEYS failed and got error: " + ); + } + + private async putKeys(request: UpdateDeviceKeysRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.updateKeys, + "UPDATE KEYS success", + "UPDATE KEYS failed and got error: " + ); + } + + private async getQueue(request: GetDeviceQueueItemsRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.getQueue, + "GET QUEUE success", + "GET QUEUE failed and got error: " + ); + } + + private async deleteQueue(request: FlushDeviceQueueRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.flushQueue, + "DELETE QUEUE success", + "DELETE QUEUE failed and got error: " + ); + } + + private async postDownlink(request: EnqueueDeviceQueueItemRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.enqueue, + "POST DOWNLINK success", + "POST DOWNLINK failed and got error :" + ); + } + + private async getActivation(request: GetDeviceActivationRequest): Promise { + return await this.makeRequest( + request, + this.deviceServiceClient.getActivation, + "GET ACTIVATION success", + "GET ACTIVATION failed and got error: " + ); + } + + private async postActivation(request?: ActivateDeviceRequest): Promise { + await this.makeRequest( + request, + this.deviceServiceClient.activate, + "post ACTIVATION success", + "GET activation failed and got error: " + ); + } + private async makeRequest( + request: any, + method: (request: any, metaData: any, callback: (err: ServiceError, resp: any) => void) => void, + successMessage: string, + errorMessage: string + ): Promise { + const metaData = this.makeMetadataHeader(); + const promise = new Promise((resolve, reject) => { + method.call(this.deviceServiceClient, request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.logger.debug(successMessage); + resolve(resp); } + }); + }); + try { + return await promise; + } catch (err) { + this.logger.error(JSON.stringify(errorMessage)); + throw new InternalServerErrorException(); } + } } diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index b7baad2d..32da227f 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -1,9 +1,9 @@ import { - BadRequestException, - Injectable, - InternalServerErrorException, - Logger, - NotFoundException, + BadRequestException, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, } from "@nestjs/common"; import { ChirpstackErrorResponseDto } from "@dto/chirpstack/chirpstack-error-response.dto"; import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; @@ -22,15 +22,15 @@ import { Repository } from "typeorm"; import { OrganizationService } from "@services/user-management/organization.service"; import { CommonLocationDto } from "@dto/chirpstack/common-location.dto"; import { - CreateGatewayRequest, - DeleteGatewayRequest, - Gateway as ChirpstackGateway, - GetGatewayMetricsRequest, - GetGatewayMetricsResponse, - GetGatewayResponse, - ListGatewaysRequest, - ListGatewaysResponse, - UpdateGatewayRequest, + CreateGatewayRequest, + DeleteGatewayRequest, + Gateway as ChirpstackGateway, + GetGatewayMetricsRequest, + GetGatewayMetricsResponse, + GetGatewayResponse, + ListGatewaysRequest, + ListGatewaysResponse, + UpdateGatewayRequest, } from "@chirpstack/chirpstack-api/api/gateway_pb"; import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; import { Aggregation, Location } from "@chirpstack/chirpstack-api/common/common_pb"; @@ -38,484 +38,474 @@ import { dateToTimestamp, timestampToDate } from "@helpers/date.helper"; import { ChirpstackGatewayResponseDto, GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { - ListAllChirpstackGatewaysResponseDto, - ListAllGatewaysResponseDto, + ListAllChirpstackGatewaysResponseDto, + ListAllGatewaysResponseDto, } from "@dto/chirpstack/list-all-gateways-response.dto"; @Injectable() export class ChirpstackGatewayService extends GenericChirpstackConfigurationService { - constructor( - @InjectRepository(DbGateway) - private gatewayRepository: Repository, - private organizationService: OrganizationService - ) { - super(); - } - GATEWAY_STATS_INTERVAL_IN_DAYS = 29; - private readonly logger = new Logger(ChirpstackGatewayService.name, { - timestamp: true, + constructor( + @InjectRepository(DbGateway) + private gatewayRepository: Repository, + private organizationService: OrganizationService + ) { + super(); + } + GATEWAY_STATS_INTERVAL_IN_DAYS = 29; + private readonly logger = new Logger(ChirpstackGatewayService.name, { + timestamp: true, + }); + + async createNewGateway(dto: CreateGatewayDto, userId: number): Promise { + dto.gateway = await this.updateDtoContents(dto.gateway); + + dto.gateway.tags = this.addOrganizationToTags(dto); + dto.gateway.tags = this.addUserToTags(dto, userId); + + const gateway = this.mapContentsDtoToGateway(dto.gateway); + gateway.createdBy = userId; + gateway.updatedBy = userId; + gateway.rxPacketsReceived = 0; + gateway.txPacketsEmitted = 0; + + gateway.organization = await this.organizationService.findById(dto.organizationId); + + const req = new CreateGatewayRequest(); + const chirpstackLocation = this.mapToChirpstackLocation(dto); + + const gatewayChirpstack = await this.mapToChirpstackGateway(dto, chirpstackLocation); + Object.entries(dto.gateway.tags).forEach(([key, value]) => { + gatewayChirpstack.getTagsMap().set(key, value); }); - async createNewGateway(dto: CreateGatewayDto, userId: number): Promise { - dto.gateway = await this.updateDtoContents(dto.gateway); - - dto.gateway.tags = this.addOrganizationToTags(dto); - dto.gateway.tags = this.addUserToTags(dto, userId); - - const gateway = this.mapContentsDtoToGateway(dto.gateway); - gateway.createdBy = userId; - gateway.updatedBy = userId; - gateway.rxPacketsReceived = 0; - gateway.txPacketsEmitted = 0; - - gateway.organization = await this.organizationService.findById(dto.organizationId); - - const req = new CreateGatewayRequest(); - const chirpstackLocation = this.mapToChirpstackLocation(dto); - - const gatewayChirpstack = await this.mapToChirpstackGateway(dto, chirpstackLocation); - Object.entries(dto.gateway.tags).forEach(([key, value]) => { - gatewayChirpstack.getTagsMap().set(key, value); - }); - - req.setGateway(gatewayChirpstack); - try { - await this.post("gateways", this.gatewayClient, req); - await this.gatewayRepository.save(gateway); - return { success: true }; - } catch (e) { - this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(e)}`); - throw new BadRequestException({ - success: false, - error: e, - }); - } - } - - async mapToChirpstackGateway(dto: CreateGatewayDto | UpdateGatewayDto, location: Location, gatewayId?: string) { - const gateway = new ChirpstackGateway(); - gateway.setGatewayId(gatewayId ? gatewayId : dto.gateway.gatewayId); - gateway.setDescription(dto.gateway.description); - gateway.setName(dto.gateway.name); - gateway.setLocation(location); - gateway.setStatsInterval(30); - gateway.setTenantId(dto.gateway.tenantId ? dto.gateway.tenantId : await this.getDefaultOrganizationId()); - - return gateway; - } - mapToChirpstackLocation(dto: CreateGatewayDto | UpdateGatewayDto) { - const location = new Location(); - location.setAccuracy(dto.gateway.location.accuracy); - location.setAltitude(dto.gateway.location.altitude); - location.setLatitude(dto.gateway.location.latitude); - location.setLongitude(dto.gateway.location.longitude); - location.setSource(dto.gateway.location.source); - - return location; - } - - addUserToTags(dto: CreateGatewayDto, userId: number): { [id: string]: string } { - const tags = dto.gateway.tags; - tags[this.CREATED_BY_KEY] = `${userId}`; - tags[this.UPDATED_BY_KEY] = `${userId}`; - return tags; + req.setGateway(gatewayChirpstack); + try { + await this.post("gateways", this.gatewayClient, req); + await this.gatewayRepository.save(gateway); + return { success: true }; + } catch (e) { + this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(e)}`); + throw new BadRequestException({ + success: false, + error: e, + }); } - - updateUpdatedByTag(dto: UpdateGatewayDto, userId: number): { [id: string]: string } { - const tags = dto.gateway.tags; - tags[this.UPDATED_BY_KEY] = `${userId}`; - return tags; + } + + async mapToChirpstackGateway(dto: CreateGatewayDto | UpdateGatewayDto, location: Location, gatewayId?: string) { + const gateway = new ChirpstackGateway(); + gateway.setGatewayId(gatewayId ? gatewayId : dto.gateway.gatewayId); + gateway.setDescription(dto.gateway.description); + gateway.setName(dto.gateway.name); + gateway.setLocation(location); + gateway.setStatsInterval(30); + gateway.setTenantId(dto.gateway.tenantId ? dto.gateway.tenantId : await this.getDefaultOrganizationId()); + + return gateway; + } + mapToChirpstackLocation(dto: CreateGatewayDto | UpdateGatewayDto) { + const location = new Location(); + location.setAccuracy(dto.gateway.location.accuracy); + location.setAltitude(dto.gateway.location.altitude); + location.setLatitude(dto.gateway.location.latitude); + location.setLongitude(dto.gateway.location.longitude); + location.setSource(dto.gateway.location.source); + + return location; + } + + addUserToTags(dto: CreateGatewayDto, userId: number): { [id: string]: string } { + const tags = dto.gateway.tags; + tags[this.CREATED_BY_KEY] = `${userId}`; + tags[this.UPDATED_BY_KEY] = `${userId}`; + return tags; + } + + updateUpdatedByTag(dto: UpdateGatewayDto, userId: number): { [id: string]: string } { + const tags = dto.gateway.tags; + tags[this.UPDATED_BY_KEY] = `${userId}`; + return tags; + } + + addOrganizationToTags(dto: CreateGatewayDto): { [id: string]: string } { + const tags = dto.gateway.tags; + tags[this.ORG_ID_KEY] = `${dto.organizationId}`; + return tags; + } + + async getAll(organizationId?: number): Promise { + let query = this.gatewayRepository + .createQueryBuilder("gateway") + .innerJoinAndSelect("gateway.organization", "organization"); + + if (organizationId) { + query = query.where('"organizationId" = :organizationId', { organizationId }); } - addOrganizationToTags(dto: CreateGatewayDto): { [id: string]: string } { - const tags = dto.gateway.tags; - tags[this.ORG_ID_KEY] = `${dto.organizationId}`; - return tags; + const gateways = await query.getMany(); + + return { + resultList: gateways.map(this.mapGatewayToResponseDto), + totalCount: gateways.length, + }; + } + + public async getWithPaginationAndSorting( + queryParams?: ListAllEntitiesDto, + organizationId?: number + ): Promise { + const orderByColumn = this.getSortingForGateways(queryParams); + const direction = queryParams?.sort?.toUpperCase() === "DESC" ? "DESC" : "ASC"; + const nullsOrder = queryParams?.sort?.toUpperCase() === "DESC" ? "NULLS LAST" : "NULLS FIRST"; + + let query = this.gatewayRepository + .createQueryBuilder("gateway") + .innerJoinAndSelect("gateway.organization", "organization") + .skip(queryParams?.offset ? +queryParams.offset : 0) + .take(queryParams.limit ? +queryParams.limit : 100) + .orderBy(orderByColumn, direction, nullsOrder); + + if (organizationId) { + query = query.where('"organizationId" = :organizationId', { organizationId }); } - async getAll(organizationId?: number): Promise { - let query = this.gatewayRepository - .createQueryBuilder("gateway") - .innerJoinAndSelect("gateway.organization", "organization"); + const [gateways, count] = await query.getManyAndCount(); - if (organizationId) { - query = query.where('"organizationId" = :organizationId', { organizationId }); - } - - const gateways = await query.getMany(); + return { + resultList: gateways.map(this.mapGatewayToResponseDto), + totalCount: count, + }; + } - return { - resultList: gateways.map(this.mapGatewayToResponseDto), - totalCount: gateways.length, - }; + async getOne(gatewayId: string): Promise { + if (gatewayId?.length != 16) { + throw new BadRequestException("Invalid gateway id"); } - - public async getWithPaginationAndSorting( - queryParams?: ListAllEntitiesDto, - organizationId?: number - ): Promise { - const orderByColumn = this.getSortingForGateways(queryParams); - const direction = queryParams?.sort?.toUpperCase() === "DESC" ? "DESC" : "ASC"; - const nullsOrder = queryParams?.sort?.toUpperCase() === "DESC" ? "NULLS LAST" : "NULLS FIRST"; - - let query = this.gatewayRepository - .createQueryBuilder("gateway") - .innerJoinAndSelect("gateway.organization", "organization") - .skip(queryParams?.offset ? +queryParams.offset : 0) - .take(queryParams.limit ? +queryParams.limit : 100) - .orderBy(orderByColumn, direction, nullsOrder); - - if (organizationId) { - query = query.where('"organizationId" = :organizationId', { organizationId }); - } - - const [gateways, count] = await query.getManyAndCount(); - - return { - resultList: gateways.map(this.mapGatewayToResponseDto), - totalCount: count, - }; + try { + const result = new SingleGatewayResponseDto(); + const gateway = await this.gatewayRepository.findOne({ + where: { gatewayId }, + relations: { organization: true }, + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + const now = new Date(); + const statsFrom = new Date(new Date().setDate(now.getDate() - this.GATEWAY_STATS_INTERVAL_IN_DAYS)); + + result.stats = await this.getGatewayStats(gatewayId, statsFrom, now); + result.gateway = this.mapGatewayToResponseDto(gateway); + + return result; + } catch (err) { + this.logger.error(`Tried to find gateway with id: '${gatewayId}', got an error: ${JSON.stringify(err)}`); + if (err?.message == "object does not exist") { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + throw new InternalServerErrorException(err?.response?.data); } + } - async getOne(gatewayId: string): Promise { - if (gatewayId?.length != 16) { - throw new BadRequestException("Invalid gateway id"); - } - try { - const result = new SingleGatewayResponseDto(); - const gateway = await this.gatewayRepository.findOne({ - where: { gatewayId }, - relations: { organization: true }, - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - const now = new Date(); - const statsFrom = new Date(new Date().setDate(now.getDate() - this.GATEWAY_STATS_INTERVAL_IN_DAYS)); - - result.stats = await this.getGatewayStats(gatewayId, statsFrom, now); - result.gateway = this.mapGatewayToResponseDto(gateway); - - return result; - } catch (err) { - this.logger.error(`Tried to find gateway with id: '${gatewayId}', got an error: ${JSON.stringify(err)}`); - if (err?.message == "object does not exist") { - throw new NotFoundException(ErrorCodes.IdDoesNotExists); - } - throw new InternalServerErrorException(err?.response?.data); - } - } + async getGatewayStats(gatewayId: string, from: Date, to: Date): Promise { + const to_time = dateToTimestamp(to); + const from_time_timestamp: Timestamp = dateToTimestamp(from); - async getGatewayStats(gatewayId: string, from: Date, to: Date): Promise { - const to_time = dateToTimestamp(to); - const from_time_timestamp: Timestamp = dateToTimestamp(from); - - const request = new GetGatewayMetricsRequest(); - request.setGatewayId(gatewayId); - request.setStart(from_time_timestamp); - request.setEnd(to_time); - request.setAggregation(Aggregation.DAY); - - const metaData = this.makeMetadataHeader(); - - const getGatewayMetricsPromise = new Promise((resolve, reject) => { - this.gatewayClient.getMetrics(request, metaData, (err, resp) => { - if (err) { - reject(err); - } else { - resolve(resp); - } - }); - }); - try { - const metrics = await getGatewayMetricsPromise; - return this.mapPackets(metrics); - } catch (err) { - throw new BadRequestException(err); - } - } + const request = new GetGatewayMetricsRequest(); + request.setGatewayId(gatewayId); + request.setStart(from_time_timestamp); + request.setEnd(to_time); + request.setAggregation(Aggregation.DAY); - //TODO: This could be moved to a helper function in the future, since it has a lot of similarities with metrics from chirpstack devices. - private mapPackets(metrics: GetGatewayMetricsResponse) { - const gatewayResponseDto: GatewayStatsElementDto[] = []; - const packetCounts: { [timestamp: string]: { rx: number; tx: number } } = {}; - - const rxTimestamps = metrics.getRxPackets().getTimestampsList(); - const rxPackets = metrics - .getRxPackets() - .getDatasetsList() - .find(e => e.getLabel() === "rx_count") - .getDataList(); - - this.processPackets(rxTimestamps, rxPackets, "rx", packetCounts); - - const txTimestamps = metrics.getTxPackets().getTimestampsList(); - const txPackets = metrics - .getTxPackets() - .getDatasetsList() - .find(e => e.getLabel() === "tx_count") - .getDataList(); - - this.processPackets(txTimestamps, txPackets, "tx", packetCounts); - - Object.keys(packetCounts).forEach(timestamp => { - const packetCount = packetCounts[timestamp]; - const dto: GatewayStatsElementDto = { - timestamp, - rxPacketsReceived: packetCount.rx, - txPacketsEmitted: packetCount.tx, - }; - gatewayResponseDto.push(dto); - }); - return gatewayResponseDto; - } - - private processPackets( - timestamps: Array, - packets: number[], - key: string, - packetCounts: { [timestamp: string]: { rx: number; tx: number } } - ) { - timestamps.forEach((timestamp, index) => { - const isoTimestamp = timestamp.toDate().toISOString(); - packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { rx: 0, tx: 0 }; - (packetCounts[isoTimestamp] as any)[key] = packets[index]; - }); - } + const metaData = this.makeMetadataHeader(); - async modifyGateway( - gatewayId: string, - dto: UpdateGatewayDto, - req: AuthenticatedRequest - ): Promise { - dto.gateway = await this.updateDtoContents(dto.gateway); - dto.gateway.tags = await this.ensureOrganizationIdIsSet(gatewayId, dto, req); - dto.gateway.tags = this.updateUpdatedByTag(dto, +req.user.userId); - - const gateway = this.mapContentsDtoToGateway(dto.gateway); - gateway.gatewayId = gatewayId; - gateway.updatedBy = req.user.userId; - - const request = new UpdateGatewayRequest(); - const location = this.mapToChirpstackLocation(dto); - - const gatewayCs = await this.mapToChirpstackGateway(dto, location, gatewayId); - - Object.entries(dto.gateway.tags).forEach(([key, value]) => { - gatewayCs.getTagsMap().set(key, value); - }); - - request.setGateway(gatewayCs); - try { - await this.put("gateways", this.gatewayClient, request); - await this.gatewayRepository.update({ gatewayId }, gateway); - return { success: true }; - } catch (e) { - this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(e)}`); - throw new BadRequestException({ - success: false, - error: e, - }); + const getGatewayMetricsPromise = new Promise((resolve, reject) => { + this.gatewayClient.getMetrics(request, metaData, (err, resp) => { + if (err) { + reject(err); + } else { + resolve(resp); } + }); + }); + try { + const metrics = await getGatewayMetricsPromise; + return this.mapPackets(metrics); + } catch (err) { + throw new BadRequestException(err); } + } + + //TODO: This could be moved to a helper function in the future, since it has a lot of similarities with metrics from chirpstack devices. + private mapPackets(metrics: GetGatewayMetricsResponse) { + const gatewayResponseDto: GatewayStatsElementDto[] = []; + const packetCounts: { [timestamp: string]: { rx: number; tx: number } } = {}; + + const rxTimestamps = metrics.getRxPackets().getTimestampsList(); + const rxPackets = metrics + .getRxPackets() + .getDatasetsList() + .find(e => e.getLabel() === "rx_count") + .getDataList(); + + this.processPackets(rxTimestamps, rxPackets, "rx", packetCounts); + + const txTimestamps = metrics.getTxPackets().getTimestampsList(); + const txPackets = metrics + .getTxPackets() + .getDatasetsList() + .find(e => e.getLabel() === "tx_count") + .getDataList(); + + this.processPackets(txTimestamps, txPackets, "tx", packetCounts); + + Object.keys(packetCounts).forEach(timestamp => { + const packetCount = packetCounts[timestamp]; + const dto: GatewayStatsElementDto = { + timestamp, + rxPacketsReceived: packetCount.rx, + txPacketsEmitted: packetCount.tx, + }; + gatewayResponseDto.push(dto); + }); + return gatewayResponseDto; + } + + private processPackets( + timestamps: Array, + packets: number[], + key: string, + packetCounts: { [timestamp: string]: { rx: number; tx: number } } + ) { + timestamps.forEach((timestamp, index) => { + const isoTimestamp = timestamp.toDate().toISOString(); + packetCounts[isoTimestamp] = packetCounts[isoTimestamp] || { rx: 0, tx: 0 }; + (packetCounts[isoTimestamp] as any)[key] = packets[index]; + }); + } - public async updateGatewayStats( - gatewayId: string, - rxPacketsReceived: number, - txPacketsEmitted: number, - updatedAt: Date, - lastSeenAt: Date | undefined - ) { - await this.gatewayRepository.update( - { gatewayId }, - { rxPacketsReceived, txPacketsEmitted, lastSeenAt, updatedAt } - ); - } - - async ensureOrganizationIdIsSet( - gatewayId: string, - dto: UpdateGatewayDto, - req: AuthenticatedRequest - ): Promise<{ [id: string]: string }> { - const existing = await this.getOne(gatewayId); - const tags = dto.gateway.tags; - tags[this.ORG_ID_KEY] = `${existing.gateway.organizationId}`; - // TODO: Interpolated string will never be null? - if (tags[this.ORG_ID_KEY] != null) { - checkIfUserHasAccessToOrganization(req, +tags[this.ORG_ID_KEY], OrganizationAccessScope.GatewayWrite); - } - return tags; - } + async modifyGateway( + gatewayId: string, + dto: UpdateGatewayDto, + req: AuthenticatedRequest + ): Promise { + dto.gateway = await this.updateDtoContents(dto.gateway); + dto.gateway.tags = await this.ensureOrganizationIdIsSet(gatewayId, dto, req); + dto.gateway.tags = this.updateUpdatedByTag(dto, +req.user.userId); - async deleteGateway(gatewayId: string): Promise { - const req = new DeleteGatewayRequest(); - req.setGatewayId(gatewayId); - try { - await this.delete("gateways", this.gatewayClient, req); - await this.gatewayRepository.delete({ gatewayId }); - return { - success: true, - }; - } catch (err) { - this.logger.error(`Got error from Chirpstack: ${JSON.stringify(err?.response?.data)}`); - return { - success: false, - chirpstackError: err?.response?.data as ChirpstackErrorResponseDto, - }; - } - } + const gateway = this.mapContentsDtoToGateway(dto.gateway); + gateway.gatewayId = gatewayId; + gateway.updatedBy = req.user.userId; - private async updateDtoContents( - contentsDto: GatewayContentsDto | UpdateGatewayContentsDto - ): Promise { - if (contentsDto?.tagsString) { - contentsDto.tags = JSON.parse(contentsDto.tagsString); - } else { - contentsDto.tags = {}; - } + const request = new UpdateGatewayRequest(); + const location = this.mapToChirpstackLocation(dto); - contentsDto.id = contentsDto.gatewayId; + const gatewayCs = await this.mapToChirpstackGateway(dto, location, gatewayId); - return contentsDto; - } + Object.entries(dto.gateway.tags).forEach(([key, value]) => { + gatewayCs.getTagsMap().set(key, value); + }); - public mapContentsDtoToGateway(dto: GatewayContentsDto) { - const gateway = new DbGateway(); - gateway.name = dto.name; - gateway.gatewayId = dto.gatewayId; - gateway.description = dto.description; - gateway.altitude = dto.location.altitude; - gateway.location = { - type: "Point", - coordinates: [dto.location.longitude, dto.location.latitude], - }; - - gateway.placement = dto.placement; - gateway.modelName = dto.modelName; - gateway.antennaType = dto.antennaType; - gateway.status = dto.status; - gateway.gatewayResponsibleName = dto.gatewayResponsibleName; - gateway.gatewayResponsibleEmail = dto.gatewayResponsibleEmail; - gateway.gatewayResponsiblePhoneNumber = dto.gatewayResponsiblePhoneNumber; - gateway.operationalResponsibleName = dto.operationalResponsibleName; - gateway.operationalResponsibleEmail = dto.operationalResponsibleEmail; - - const tempTags = { ...dto.tags }; - tempTags[this.ORG_ID_KEY] = undefined; - tempTags[this.CREATED_BY_KEY] = undefined; - tempTags[this.UPDATED_BY_KEY] = undefined; - gateway.tags = JSON.stringify(tempTags); - - return gateway; + request.setGateway(gatewayCs); + try { + await this.put("gateways", this.gatewayClient, request); + await this.gatewayRepository.update({ gatewayId }, gateway); + return { success: true }; + } catch (e) { + this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', got response: ${JSON.stringify(e)}`); + throw new BadRequestException({ + success: false, + error: e, + }); } - - public mapChirpstackGatewayToDatabaseGateway(chirpstackGateway: ChirpstackGateway, gwResponse: GetGatewayResponse) { - const gateway = new DbGateway(); - gateway.name = chirpstackGateway.getName(); - gateway.gatewayId = chirpstackGateway.getGatewayId(); - gateway.description = chirpstackGateway.getDescription(); - gateway.altitude = chirpstackGateway.getLocation().getAltitude(); - gateway.location = { - type: "Point", - coordinates: [ - chirpstackGateway.getLocation().getLongitude(), - chirpstackGateway.getLocation().getLatitude(), - ], - }; - const jsonRepresentation: Record = chirpstackGateway - .getTagsMap() - .toArray() - .reduce((obj: Record, [key, value]) => { - obj[key] = value; - return obj; - }, {}); - jsonRepresentation["internalOrganizationId"] = undefined; - jsonRepresentation["os2iot-updated-by"] = undefined; - jsonRepresentation["os2iot-created-by"] = undefined; - gateway.tags = JSON.stringify(jsonRepresentation); - gateway.lastSeenAt = gwResponse.getLastSeenAt() - ? timestampToDate(gwResponse.getLastSeenAt().toObject()) - : undefined; - gateway.createdAt = gwResponse.getCreatedAt() - ? timestampToDate(gwResponse.getCreatedAt().toObject()) - : undefined; - gateway.updatedAt = gwResponse.getUpdatedAt() - ? timestampToDate(gwResponse.getUpdatedAt().toObject()) - : undefined; - gateway.rxPacketsReceived = 0; - gateway.txPacketsEmitted = 0; - gateway.createdBy = - chirpstackGateway.getTagsMap().get("os2iot-created-by") !== undefined - ? Number(chirpstackGateway.getTagsMap().get("os2iot-created-by")) - : undefined; - gateway.updatedBy = - chirpstackGateway.getTagsMap().get("os2iot-updated-by") !== undefined - ? Number(chirpstackGateway.getTagsMap().get("os2iot-updated-by")) - : undefined; - - return gateway; + } + + public async updateGatewayStats( + gatewayId: string, + rxPacketsReceived: number, + txPacketsEmitted: number, + updatedAt: Date, + lastSeenAt: Date | undefined + ) { + await this.gatewayRepository.update({ gatewayId }, { rxPacketsReceived, txPacketsEmitted, lastSeenAt, updatedAt }); + } + + async ensureOrganizationIdIsSet( + gatewayId: string, + dto: UpdateGatewayDto, + req: AuthenticatedRequest + ): Promise<{ [id: string]: string }> { + const existing = await this.getOne(gatewayId); + const tags = dto.gateway.tags; + tags[this.ORG_ID_KEY] = `${existing.gateway.organizationId}`; + // TODO: Interpolated string will never be null? + if (tags[this.ORG_ID_KEY] != null) { + checkIfUserHasAccessToOrganization(req, +tags[this.ORG_ID_KEY], OrganizationAccessScope.GatewayWrite); } - private mapGatewayToResponseDto(gateway: DbGateway): GatewayResponseDto { - const responseDto = gateway as unknown as GatewayResponseDto; - responseDto.organizationId = gateway.organization.id; - responseDto.organizationName = gateway.organization.name; - - const commonLocation = new CommonLocationDto(); - commonLocation.latitude = gateway.location.coordinates[1]; - commonLocation.longitude = gateway.location.coordinates[0]; - commonLocation.altitude = gateway.altitude; - responseDto.tags = JSON.parse(gateway.tags); - responseDto.location = commonLocation; - - return responseDto; + return tags; + } + + async deleteGateway(gatewayId: string): Promise { + const req = new DeleteGatewayRequest(); + req.setGatewayId(gatewayId); + try { + await this.delete("gateways", this.gatewayClient, req); + await this.gatewayRepository.delete({ gatewayId }); + return { + success: true, + }; + } catch (err) { + this.logger.error(`Got error from Chirpstack: ${JSON.stringify(err?.response?.data)}`); + return { + success: false, + chirpstackError: err?.response?.data as ChirpstackErrorResponseDto, + }; } - async getAllGatewaysFromChirpstack(): Promise { - const limit = 1000; - const listReq = new ListGatewaysRequest(); - // Get all chirpstack gateways - const chirpStackGateways = await this.getAllWithPagination( - "gateways", - this.gatewayClient, - listReq, - limit, - 0 - ); - - const responseItem: ChirpstackGatewayResponseDto[] = []; - chirpStackGateways.resultList.map(e => { - const resultItem: ChirpstackGatewayResponseDto = { - gatewayId: e.gatewayId, - name: e.name, - location: e.location, - description: e.description, - createdAt: e.createdAt ?? undefined, - updatedAt: e.updatedAt ?? undefined, - lastSeenAt: e.lastSeenAt ?? undefined, - }; - responseItem.push(resultItem); - }); - const responseList: ListAllChirpstackGatewaysResponseDto = { - totalCount: chirpStackGateways.totalCount, - resultList: responseItem, - }; - return responseList; + } + + private async updateDtoContents( + contentsDto: GatewayContentsDto | UpdateGatewayContentsDto + ): Promise { + if (contentsDto?.tagsString) { + contentsDto.tags = JSON.parse(contentsDto.tagsString); + } else { + contentsDto.tags = {}; } - private getSortingForGateways(query: ListAllEntitiesDto) { - let orderBy = "gateway.id"; - - if (!query.orderOn) { - return orderBy; - } - - if (query.orderOn === "organizationName") { - orderBy = "organization.name"; - } else if (query.orderOn === "status") { - orderBy = "gateway.lastSeenAt"; - } else { - orderBy = `gateway.${query.orderOn}`; - } + contentsDto.id = contentsDto.gatewayId; + + return contentsDto; + } + + public mapContentsDtoToGateway(dto: GatewayContentsDto) { + const gateway = new DbGateway(); + gateway.name = dto.name; + gateway.gatewayId = dto.gatewayId; + gateway.description = dto.description; + gateway.altitude = dto.location.altitude; + gateway.location = { + type: "Point", + coordinates: [dto.location.longitude, dto.location.latitude], + }; + + gateway.placement = dto.placement; + gateway.modelName = dto.modelName; + gateway.antennaType = dto.antennaType; + gateway.status = dto.status; + gateway.gatewayResponsibleName = dto.gatewayResponsibleName; + gateway.gatewayResponsibleEmail = dto.gatewayResponsibleEmail; + gateway.gatewayResponsiblePhoneNumber = dto.gatewayResponsiblePhoneNumber; + gateway.operationalResponsibleName = dto.operationalResponsibleName; + gateway.operationalResponsibleEmail = dto.operationalResponsibleEmail; + + const tempTags = { ...dto.tags }; + tempTags[this.ORG_ID_KEY] = undefined; + tempTags[this.CREATED_BY_KEY] = undefined; + tempTags[this.UPDATED_BY_KEY] = undefined; + gateway.tags = JSON.stringify(tempTags); + + return gateway; + } + + public mapChirpstackGatewayToDatabaseGateway(chirpstackGateway: ChirpstackGateway, gwResponse: GetGatewayResponse) { + const gateway = new DbGateway(); + gateway.name = chirpstackGateway.getName(); + gateway.gatewayId = chirpstackGateway.getGatewayId(); + gateway.description = chirpstackGateway.getDescription(); + gateway.altitude = chirpstackGateway.getLocation().getAltitude(); + gateway.location = { + type: "Point", + coordinates: [chirpstackGateway.getLocation().getLongitude(), chirpstackGateway.getLocation().getLatitude()], + }; + const jsonRepresentation: Record = chirpstackGateway + .getTagsMap() + .toArray() + .reduce((obj: Record, [key, value]) => { + obj[key] = value; + return obj; + }, {}); + jsonRepresentation["internalOrganizationId"] = undefined; + jsonRepresentation["os2iot-updated-by"] = undefined; + jsonRepresentation["os2iot-created-by"] = undefined; + gateway.tags = JSON.stringify(jsonRepresentation); + gateway.lastSeenAt = gwResponse.getLastSeenAt() + ? timestampToDate(gwResponse.getLastSeenAt().toObject()) + : undefined; + gateway.createdAt = gwResponse.getCreatedAt() ? timestampToDate(gwResponse.getCreatedAt().toObject()) : undefined; + gateway.updatedAt = gwResponse.getUpdatedAt() ? timestampToDate(gwResponse.getUpdatedAt().toObject()) : undefined; + gateway.rxPacketsReceived = 0; + gateway.txPacketsEmitted = 0; + gateway.createdBy = + chirpstackGateway.getTagsMap().get("os2iot-created-by") !== undefined + ? Number(chirpstackGateway.getTagsMap().get("os2iot-created-by")) + : undefined; + gateway.updatedBy = + chirpstackGateway.getTagsMap().get("os2iot-updated-by") !== undefined + ? Number(chirpstackGateway.getTagsMap().get("os2iot-updated-by")) + : undefined; + + return gateway; + } + private mapGatewayToResponseDto(gateway: DbGateway): GatewayResponseDto { + const responseDto = gateway as unknown as GatewayResponseDto; + responseDto.organizationId = gateway.organization.id; + responseDto.organizationName = gateway.organization.name; + + const commonLocation = new CommonLocationDto(); + commonLocation.latitude = gateway.location.coordinates[1]; + commonLocation.longitude = gateway.location.coordinates[0]; + commonLocation.altitude = gateway.altitude; + responseDto.tags = JSON.parse(gateway.tags); + responseDto.location = commonLocation; + + return responseDto; + } + async getAllGatewaysFromChirpstack(): Promise { + const limit = 1000; + const listReq = new ListGatewaysRequest(); + // Get all chirpstack gateways + const chirpStackGateways = await this.getAllWithPagination( + "gateways", + this.gatewayClient, + listReq, + limit, + 0 + ); + + const responseItem: ChirpstackGatewayResponseDto[] = []; + chirpStackGateways.resultList.map(e => { + const resultItem: ChirpstackGatewayResponseDto = { + gatewayId: e.gatewayId, + name: e.name, + location: e.location, + description: e.description, + createdAt: e.createdAt ?? undefined, + updatedAt: e.updatedAt ?? undefined, + lastSeenAt: e.lastSeenAt ?? undefined, + }; + responseItem.push(resultItem); + }); + const responseList: ListAllChirpstackGatewaysResponseDto = { + totalCount: chirpStackGateways.totalCount, + resultList: responseItem, + }; + return responseList; + } + + private getSortingForGateways(query: ListAllEntitiesDto) { + let orderBy = "gateway.id"; + + if (!query.orderOn) { + return orderBy; + } - return orderBy; + if (query.orderOn === "organizationName") { + orderBy = "organization.name"; + } else if (query.orderOn === "status") { + orderBy = "gateway.lastSeenAt"; + } else { + orderBy = `gateway.${query.orderOn}`; } + + return orderBy; + } } diff --git a/src/services/chirpstack/device-profile.service.ts b/src/services/chirpstack/device-profile.service.ts index 566efed3..43d1ef20 100644 --- a/src/services/chirpstack/device-profile.service.ts +++ b/src/services/chirpstack/device-profile.service.ts @@ -1,14 +1,14 @@ import { - BadRequestException, - ConflictException, - Injectable, - InternalServerErrorException, - NotFoundException, + BadRequestException, + ConflictException, + Injectable, + InternalServerErrorException, + NotFoundException, } from "@nestjs/common"; import { CreateDeviceProfileDto } from "@dto/chirpstack/create-device-profile.dto"; import { - DeviceProfileListDto, - ListAllDeviceProfilesResponseDto, + DeviceProfileListDto, + ListAllDeviceProfilesResponseDto, } from "@dto/chirpstack/list-all-device-profiles-response.dto"; import { GenericChirpstackConfigurationService } from "./generic-chirpstack-configuration.service"; import { UpdateDeviceProfileDto } from "@dto/chirpstack/update-device-profile.dto"; @@ -18,15 +18,15 @@ import { ErrorCodes } from "@enum/error-codes.enum"; import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@helpers/security-helper"; import { ServiceError } from "@grpc/grpc-js"; import { - CreateDeviceProfileRequest, - DeleteDeviceProfileRequest, - DeviceProfile, - GetDeviceProfileRequest, - GetDeviceProfileResponse, - ListDeviceProfileAdrAlgorithmsResponse, - ListDeviceProfilesRequest, - ListDeviceProfilesResponse, - UpdateDeviceProfileRequest, + CreateDeviceProfileRequest, + DeleteDeviceProfileRequest, + DeviceProfile, + GetDeviceProfileRequest, + GetDeviceProfileResponse, + ListDeviceProfileAdrAlgorithmsResponse, + ListDeviceProfilesRequest, + ListDeviceProfilesResponse, + UpdateDeviceProfileRequest, } from "@chirpstack/chirpstack-api/api/device_profile_pb"; import { timestampToDate } from "@helpers/date.helper"; import { ListAllAdrAlgorithmsResponseDto } from "@dto/chirpstack/list-all-adr-algorithms-response.dto"; @@ -40,308 +40,304 @@ import * as BluebirdPromise from "bluebird"; @Injectable() export class DeviceProfileService extends GenericChirpstackConfigurationService { - public async createDeviceProfile(dto: CreateDeviceProfileDto, userId: number): Promise { - if (await this.isNameInUse(dto.deviceProfile.name)) { - throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); - } - dto.deviceProfile = await this.updateDto(dto.deviceProfile); - dto.deviceProfile.tags = this.addOrganizationToTags(dto); - dto.deviceProfile.tags = this.addUserIdToTags(dto, userId); - - const req = new CreateDeviceProfileRequest(); - - const deviceProfile = this.mapToChirpstackDto(dto, true); - - Object.entries(dto.deviceProfile.tags).forEach(([key, value]) => { - deviceProfile.getTagsMap().set(key, value); - }); - - req.setDeviceProfile(deviceProfile); - const result: IdResponse = await this.post("device-profiles", this.deviceProfileClient, req); - return result; + public async createDeviceProfile(dto: CreateDeviceProfileDto, userId: number): Promise { + if (await this.isNameInUse(dto.deviceProfile.name)) { + throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); } - - private addOrganizationToTags(dto: CreateDeviceProfileDto): { [id: string]: string } { - const tags = dto.deviceProfile?.tags != null ? dto.deviceProfile.tags : {}; - tags[this.ORG_ID_KEY] = `${dto.internalOrganizationId}`; - return tags; + dto.deviceProfile = await this.updateDto(dto.deviceProfile); + dto.deviceProfile.tags = this.addOrganizationToTags(dto); + dto.deviceProfile.tags = this.addUserIdToTags(dto, userId); + + const req = new CreateDeviceProfileRequest(); + + const deviceProfile = this.mapToChirpstackDto(dto, true); + + Object.entries(dto.deviceProfile.tags).forEach(([key, value]) => { + deviceProfile.getTagsMap().set(key, value); + }); + + req.setDeviceProfile(deviceProfile); + const result: IdResponse = await this.post("device-profiles", this.deviceProfileClient, req); + return result; + } + + private addOrganizationToTags(dto: CreateDeviceProfileDto): { [id: string]: string } { + const tags = dto.deviceProfile?.tags != null ? dto.deviceProfile.tags : {}; + tags[this.ORG_ID_KEY] = `${dto.internalOrganizationId}`; + return tags; + } + + private addUserIdToTags(dto: CreateDeviceProfileDto, userId: number): { [id: string]: string } { + const tags = dto.deviceProfile?.tags != null ? dto.deviceProfile.tags : {}; + tags[this.CREATED_BY_KEY] = `${userId}`; + tags[this.UPDATED_BY_KEY] = `${userId}`; + return tags; + } + + public async updateDeviceProfile(data: UpdateDeviceProfileDto, id: string, req: AuthenticatedRequest): Promise { + if (await this.isNameInUse(data.deviceProfile.name, id)) { + throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); } - - private addUserIdToTags(dto: CreateDeviceProfileDto, userId: number): { [id: string]: string } { - const tags = dto.deviceProfile?.tags != null ? dto.deviceProfile.tags : {}; - tags[this.CREATED_BY_KEY] = `${userId}`; - tags[this.UPDATED_BY_KEY] = `${userId}`; - return tags; + const deviceProfile = this.mapToChirpstackDto(data); + const request = new UpdateDeviceProfileRequest(); + + data.deviceProfile = await this.updateDto(data.deviceProfile); + + //Have to set these everytime, otherwise they will be erased. + deviceProfile.getTagsMap().set(this.ORG_ID_KEY, data.deviceProfile.internalOrganizationId.toString()); + deviceProfile.getTagsMap().set(this.UPDATED_BY_KEY, data.deviceProfile.updatedBy.toString()); + deviceProfile.getTagsMap().set(this.CREATED_BY_KEY, data.deviceProfile.createdBy.toString()); + + checkIfUserHasAccessToOrganization( + req, + data.deviceProfile.internalOrganizationId, + OrganizationAccessScope.ApplicationWrite + ); + + request.setDeviceProfile(deviceProfile); + return await this.put("device-profiles", this.deviceProfileClient, request); + } + + private async isNameInUse(name: string, id?: string): Promise { + const deviceProfiles = await this.findAllDeviceProfiles(1000, 0); + return deviceProfiles.result + .filter(x => (id ? x.id != id : true)) + .some(x => x.name.toLocaleLowerCase() == name.toLocaleLowerCase()); + } + + public async deleteDeviceProfile(id: string, req: AuthenticatedRequest): Promise { + const getReq = new GetDeviceProfileRequest(); + const result = await this.getOneById( + "device-profiles", + id, + this.deviceProfileClient, + getReq + ); + const deviceProfileId = result.getDeviceProfile().getId(); + const listReq = new ListDevicesRequest(); + const listAppReq = new ListApplicationsRequest(); + listAppReq.setTenantId(await this.getDefaultOrganizationId()); + + const applications = await this.getAllWithPagination( + "devices", + this.applicationServiceClient, + listAppReq, + 1000, + 0 + ); + + let devices: DeviceListItem.AsObject[] = []; + for (let index = 0; index < applications.resultList.length; index++) { + listReq.setApplicationId(applications.resultList[index].id); + const devicesForApp = await this.getAllWithPagination( + "devices", + this.deviceServiceClient, + listReq, + 10000, + 0 + ); + devices = devices.concat(devicesForApp.resultList); } - public async updateDeviceProfile( - data: UpdateDeviceProfileDto, - id: string, - req: AuthenticatedRequest - ): Promise { - if (await this.isNameInUse(data.deviceProfile.name, id)) { - throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); - } - const deviceProfile = this.mapToChirpstackDto(data); - const request = new UpdateDeviceProfileRequest(); - - data.deviceProfile = await this.updateDto(data.deviceProfile); - - //Have to set these everytime, otherwise they will be erased. - deviceProfile.getTagsMap().set(this.ORG_ID_KEY, data.deviceProfile.internalOrganizationId.toString()); - deviceProfile.getTagsMap().set(this.UPDATED_BY_KEY, data.deviceProfile.updatedBy.toString()); - deviceProfile.getTagsMap().set(this.CREATED_BY_KEY, data.deviceProfile.createdBy.toString()); - - checkIfUserHasAccessToOrganization( - req, - data.deviceProfile.internalOrganizationId, - OrganizationAccessScope.ApplicationWrite - ); - - request.setDeviceProfile(deviceProfile); - return await this.put("device-profiles", this.deviceProfileClient, request); + const match = devices.find(e => e.deviceProfileId === deviceProfileId); + if (match) { + throw new ConflictException(ErrorCodes.DeleteNotAllowedHasLoRaWANDevices); } - private async isNameInUse(name: string, id?: string): Promise { - const deviceProfiles = await this.findAllDeviceProfiles(1000, 0); - return deviceProfiles.result - .filter(x => (id ? x.id != id : true)) - .some(x => x.name.toLocaleLowerCase() == name.toLocaleLowerCase()); + if (result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY) != null) { + checkIfUserHasAccessToOrganization( + req, + +result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY), + OrganizationAccessScope.ApplicationWrite + ); } - - public async deleteDeviceProfile(id: string, req: AuthenticatedRequest): Promise { - const getReq = new GetDeviceProfileRequest(); - const result = await this.getOneById( - "device-profiles", - id, - this.deviceProfileClient, - getReq - ); - const deviceProfileId = result.getDeviceProfile().getId(); - const listReq = new ListDevicesRequest(); - const listAppReq = new ListApplicationsRequest(); - listAppReq.setTenantId(await this.getDefaultOrganizationId()); - - const applications = await this.getAllWithPagination( - "devices", - this.applicationServiceClient, - listAppReq, - 1000, - 0 - ); - - let devices: DeviceListItem.AsObject[] = []; - for (let index = 0; index < applications.resultList.length; index++) { - listReq.setApplicationId(applications.resultList[index].id); - const devicesForApp = await this.getAllWithPagination( - "devices", - this.deviceServiceClient, - listReq, - 10000, - 0 - ); - devices = devices.concat(devicesForApp.resultList); - } - - const match = devices.find(e => e.deviceProfileId === deviceProfileId); - if (match) { - throw new ConflictException(ErrorCodes.DeleteNotAllowedHasLoRaWANDevices); - } - - if (result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY) != null) { - checkIfUserHasAccessToOrganization( - req, - +result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY), - OrganizationAccessScope.ApplicationWrite - ); - } - const deleteReq = new DeleteDeviceProfileRequest(); - deleteReq.setId(result.getDeviceProfile().getId()); - return await this.delete("device-profiles", this.deviceProfileClient, deleteReq); + const deleteReq = new DeleteDeviceProfileRequest(); + deleteReq.setId(result.getDeviceProfile().getId()); + return await this.delete("device-profiles", this.deviceProfileClient, deleteReq); + } + + public async findAllDeviceProfiles(limit?: number, offset?: number): Promise { + const req = new ListDeviceProfilesRequest(); + req.setTenantId(await this.getDefaultOrganizationId()); + + const result = await this.getAllWithPagination( + "device-profiles", + this.deviceProfileClient, + req, + limit, + offset + ); + + const deviceResultListDto: DeviceProfileListDto[] = result.resultList.map(e => { + return { + name: e.name, + createdAt: timestampToDate(e.createdAt), + updatedAt: timestampToDate(e.updatedAt), + id: e.id, + }; + }); + const deviceProfileList: ListAllDeviceProfilesResponseDto = { + totalCount: result.totalCount.toString(), + result: deviceResultListDto, + }; + + await BluebirdPromise.all( + BluebirdPromise.map( + deviceProfileList.result, + async x => { + const dp = await this.findOneDeviceProfileById(x.id); + x.internalOrganizationId = +dp.deviceProfile.internalOrganizationId; + x.createdBy = +dp.deviceProfile.createdBy; + x.updatedBy = +dp.deviceProfile.updatedBy; + }, + { concurrency: 20 } + ) + ); + + return deviceProfileList; + } + + public async findOneDeviceProfileById(id: string): Promise { + const req = new GetDeviceProfileRequest(); + req.setId(id); + try { + const result = await this.getOneById( + "device-profiles", + id, + this.deviceProfileClient, + req + ); + const deviceProfileObject = this.mapSingleDeviceProfileResponse(result); + + return deviceProfileObject; + } catch (err) { + throw new InternalServerErrorException("Could not get device profile"); } - - public async findAllDeviceProfiles(limit?: number, offset?: number): Promise { - const req = new ListDeviceProfilesRequest(); - req.setTenantId(await this.getDefaultOrganizationId()); - - const result = await this.getAllWithPagination( - "device-profiles", - this.deviceProfileClient, - req, - limit, - offset - ); - - const deviceResultListDto: DeviceProfileListDto[] = result.resultList.map(e => { - return { - name: e.name, - createdAt: timestampToDate(e.createdAt), - updatedAt: timestampToDate(e.updatedAt), - id: e.id, - }; - }); - const deviceProfileList: ListAllDeviceProfilesResponseDto = { - totalCount: result.totalCount.toString(), - result: deviceResultListDto, - }; - - await BluebirdPromise.all( - BluebirdPromise.map( - deviceProfileList.result, - async x => { - const dp = await this.findOneDeviceProfileById(x.id); - x.internalOrganizationId = +dp.deviceProfile.internalOrganizationId; - x.createdBy = +dp.deviceProfile.createdBy; - x.updatedBy = +dp.deviceProfile.updatedBy; - }, - { concurrency: 20 } - ) - ); - - return deviceProfileList; - } - - public async findOneDeviceProfileById(id: string): Promise { - const req = new GetDeviceProfileRequest(); - req.setId(id); - try { - const result = await this.getOneById( - "device-profiles", - id, - this.deviceProfileClient, - req - ); - const deviceProfileObject = this.mapSingleDeviceProfileResponse(result); - - return deviceProfileObject; - } catch (err) { - throw new InternalServerErrorException("Could not get device profile"); - } - } - - public async updateDto(dto: DeviceProfileDto): Promise { - dto.organizationID = await this.getDefaultOrganizationId(); - - return dto; - } - - private mapSingleDeviceProfileResponse(result: GetDeviceProfileResponse): CreateDeviceProfileDto { - const responseObject = result.getDeviceProfile().toObject(); - const deviceProfileMapped = this.mapDeviceInfoContent(responseObject); - const deviceProfileResponseObject: CreateDeviceProfileDto = { - deviceProfile: deviceProfileMapped, - createdAt: result.getCreatedAt().toDate(), - updatedAt: result.getUpdatedAt().toDate(), - internalOrganizationId: +result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY), - }; - - deviceProfileResponseObject.deviceProfile.internalOrganizationId = +result - .getDeviceProfile() - .getTagsMap() - .get(this.ORG_ID_KEY); - deviceProfileResponseObject.deviceProfile.createdBy = +result - .getDeviceProfile() - .getTagsMap() - .get(this.CREATED_BY_KEY); - deviceProfileResponseObject.deviceProfile.updatedBy = +result - .getDeviceProfile() - .getTagsMap() - .get(this.UPDATED_BY_KEY); - - deviceProfileResponseObject.deviceProfile.tagsMap = deviceProfileResponseObject.deviceProfile.tagsMap.filter( - ([key]) => { - return key !== this.ORG_ID_KEY && key !== this.CREATED_BY_KEY && key !== this.UPDATED_BY_KEY; - } - ); - return deviceProfileResponseObject; - } - - private mapDeviceInfoContent(devProfile: DeviceProfile.AsObject) { - const deviceProfileMapped: DeviceProfileDto = { - name: devProfile.name, - id: devProfile.id, - adrAlgorithmID: devProfile.adrAlgorithmId, - macVersion: devProfile.macVersion, - regParamsRevision: devProfile.regParamsRevision, - classBTimeout: devProfile.classBTimeout, - classCTimeout: devProfile.classCTimeout, - pingSlotDR: devProfile.classBPingSlotDr, - pingSlotFreq: devProfile.classBPingSlotFreq, - pingSlotPeriod: devProfile.classBPingSlotNbK, - rfRegion: "EU868", - rxDROffset1: devProfile.abpRx1DrOffset, - rxDataRate2: devProfile.abpRx2Dr, - rxDelay1: devProfile.abpRx1Delay, - rxFreq2: devProfile.abpRx2Freq, - supportsClassB: devProfile.supportsClassB, - supportsClassC: devProfile.supportsClassC, - supportsJoin: devProfile.supportsOtaa, - tagsMap: devProfile.tagsMap, - devStatusReqFreq: devProfile.deviceStatusReqInterval, - }; - return deviceProfileMapped; - } - - mapToChirpstackDto(data: CreateDeviceProfileDto | UpdateDeviceProfileDto, isCreate?: boolean) { - const deviceProfile = new DeviceProfile(); - deviceProfile.setName(data.deviceProfile.name); - deviceProfile.setMacVersion(data.deviceProfile.macVersion); - deviceProfile.setRegParamsRevision(data.deviceProfile.regParamsRevision); - deviceProfile.setAdrAlgorithmId(data.deviceProfile.adrAlgorithmID); - deviceProfile.setClassBTimeout(data.deviceProfile.classBTimeout); - deviceProfile.setClassCTimeout(data.deviceProfile.classCTimeout); - deviceProfile.setId(data.deviceProfile.id); - deviceProfile.setClassBPingSlotDr(data.deviceProfile.pingSlotDR); - deviceProfile.setClassBPingSlotFreq(data.deviceProfile.pingSlotFreq); - deviceProfile.setClassBPingSlotNbK(data.deviceProfile.pingSlotPeriod); - //region 0 = EU868 - deviceProfile.setRegion(0); - deviceProfile.setAbpRx1DrOffset(data.deviceProfile.rxDROffset1); - deviceProfile.setAbpRx2Dr(data.deviceProfile.rxDataRate2); - deviceProfile.setAbpRx1Delay(data.deviceProfile.rxDelay1); - deviceProfile.setAbpRx2Freq(data.deviceProfile.rxFreq2); - deviceProfile.setSupportsClassB(data.deviceProfile.supportsClassB); - deviceProfile.setSupportsClassC(data.deviceProfile.supportsClassC); - deviceProfile.setSupportsOtaa(data.deviceProfile.supportsJoin); - deviceProfile.setDeviceStatusReqInterval( - data.deviceProfile.devStatusReqFreq === undefined ? 1 : data.deviceProfile.devStatusReqFreq - ); - - isCreate ? deviceProfile.setTenantId(data.deviceProfile.organizationID) : {}; - - return deviceProfile; - } - - public async getAdrAlgorithmsForChirpstack(): Promise { - const result = await this.getAdrAlgorithms(); - - const adrAlgoritmList: AdrAlgorithmDto[] = result.getResultList().map(e => { - return { - id: e.getId(), - name: e.getName(), - }; - }); - - return { - adrAlgorithms: adrAlgoritmList, - }; - } - - async getAdrAlgorithms(): Promise { - const metaData = this.makeMetadataHeader(); - const getPromise = new Promise((resolve, reject) => { - this.deviceProfileClient.listAdrAlgorithms(new Empty(), metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - resolve(resp); - } - }); - }); - try { - return await getPromise; - } catch (err) { - throw new NotFoundException(); + } + + public async updateDto(dto: DeviceProfileDto): Promise { + dto.organizationID = await this.getDefaultOrganizationId(); + + return dto; + } + + private mapSingleDeviceProfileResponse(result: GetDeviceProfileResponse): CreateDeviceProfileDto { + const responseObject = result.getDeviceProfile().toObject(); + const deviceProfileMapped = this.mapDeviceInfoContent(responseObject); + const deviceProfileResponseObject: CreateDeviceProfileDto = { + deviceProfile: deviceProfileMapped, + createdAt: result.getCreatedAt().toDate(), + updatedAt: result.getUpdatedAt().toDate(), + internalOrganizationId: +result.getDeviceProfile().getTagsMap().get(this.ORG_ID_KEY), + }; + + deviceProfileResponseObject.deviceProfile.internalOrganizationId = +result + .getDeviceProfile() + .getTagsMap() + .get(this.ORG_ID_KEY); + deviceProfileResponseObject.deviceProfile.createdBy = +result + .getDeviceProfile() + .getTagsMap() + .get(this.CREATED_BY_KEY); + deviceProfileResponseObject.deviceProfile.updatedBy = +result + .getDeviceProfile() + .getTagsMap() + .get(this.UPDATED_BY_KEY); + + deviceProfileResponseObject.deviceProfile.tagsMap = deviceProfileResponseObject.deviceProfile.tagsMap.filter( + ([key]) => { + return key !== this.ORG_ID_KEY && key !== this.CREATED_BY_KEY && key !== this.UPDATED_BY_KEY; + } + ); + return deviceProfileResponseObject; + } + + private mapDeviceInfoContent(devProfile: DeviceProfile.AsObject) { + const deviceProfileMapped: DeviceProfileDto = { + name: devProfile.name, + id: devProfile.id, + adrAlgorithmID: devProfile.adrAlgorithmId, + macVersion: devProfile.macVersion, + regParamsRevision: devProfile.regParamsRevision, + classBTimeout: devProfile.classBTimeout, + classCTimeout: devProfile.classCTimeout, + pingSlotDR: devProfile.classBPingSlotDr, + pingSlotFreq: devProfile.classBPingSlotFreq, + pingSlotPeriod: devProfile.classBPingSlotNbK, + rfRegion: "EU868", + rxDROffset1: devProfile.abpRx1DrOffset, + rxDataRate2: devProfile.abpRx2Dr, + rxDelay1: devProfile.abpRx1Delay, + rxFreq2: devProfile.abpRx2Freq, + supportsClassB: devProfile.supportsClassB, + supportsClassC: devProfile.supportsClassC, + supportsJoin: devProfile.supportsOtaa, + tagsMap: devProfile.tagsMap, + devStatusReqFreq: devProfile.deviceStatusReqInterval, + }; + return deviceProfileMapped; + } + + mapToChirpstackDto(data: CreateDeviceProfileDto | UpdateDeviceProfileDto, isCreate?: boolean) { + const deviceProfile = new DeviceProfile(); + deviceProfile.setName(data.deviceProfile.name); + deviceProfile.setMacVersion(data.deviceProfile.macVersion); + deviceProfile.setRegParamsRevision(data.deviceProfile.regParamsRevision); + deviceProfile.setAdrAlgorithmId(data.deviceProfile.adrAlgorithmID); + deviceProfile.setClassBTimeout(data.deviceProfile.classBTimeout); + deviceProfile.setClassCTimeout(data.deviceProfile.classCTimeout); + deviceProfile.setId(data.deviceProfile.id); + deviceProfile.setClassBPingSlotDr(data.deviceProfile.pingSlotDR); + deviceProfile.setClassBPingSlotFreq(data.deviceProfile.pingSlotFreq); + deviceProfile.setClassBPingSlotNbK(data.deviceProfile.pingSlotPeriod); + //region 0 = EU868 + deviceProfile.setRegion(0); + deviceProfile.setAbpRx1DrOffset(data.deviceProfile.rxDROffset1); + deviceProfile.setAbpRx2Dr(data.deviceProfile.rxDataRate2); + deviceProfile.setAbpRx1Delay(data.deviceProfile.rxDelay1); + deviceProfile.setAbpRx2Freq(data.deviceProfile.rxFreq2); + deviceProfile.setSupportsClassB(data.deviceProfile.supportsClassB); + deviceProfile.setSupportsClassC(data.deviceProfile.supportsClassC); + deviceProfile.setSupportsOtaa(data.deviceProfile.supportsJoin); + deviceProfile.setDeviceStatusReqInterval( + data.deviceProfile.devStatusReqFreq === undefined ? 1 : data.deviceProfile.devStatusReqFreq + ); + + isCreate ? deviceProfile.setTenantId(data.deviceProfile.organizationID) : {}; + + return deviceProfile; + } + + public async getAdrAlgorithmsForChirpstack(): Promise { + const result = await this.getAdrAlgorithms(); + + const adrAlgoritmList: AdrAlgorithmDto[] = result.getResultList().map(e => { + return { + id: e.getId(), + name: e.getName(), + }; + }); + + return { + adrAlgorithms: adrAlgoritmList, + }; + } + + async getAdrAlgorithms(): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + this.deviceProfileClient.listAdrAlgorithms(new Empty(), metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + resolve(resp); } + }); + }); + try { + return await getPromise; + } catch (err) { + throw new NotFoundException(); } + } } diff --git a/src/services/chirpstack/gateway-boostrapper.service.ts b/src/services/chirpstack/gateway-boostrapper.service.ts index 6457365b..c74baef7 100644 --- a/src/services/chirpstack/gateway-boostrapper.service.ts +++ b/src/services/chirpstack/gateway-boostrapper.service.ts @@ -9,55 +9,55 @@ import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways-re * Populate the database with information for each missing/new gateway. */ export class GatewayBootstrapperService implements OnApplicationBootstrap { - constructor( - @Inject(GatewayStatusHistoryService) - private statusHistoryService: GatewayStatusHistoryService, - @Inject(ChirpstackGatewayService) - private chirpstackGatewayService: ChirpstackGatewayService - ) {} - private readonly logger = new Logger(GatewayBootstrapperService.name); - - async onApplicationBootstrap(): Promise { - try { - const chirpstackGatewaysPromise = this.chirpstackGatewayService.getAll(); - const latestStatusHistories = await this.statusHistoryService.findLatestPerGateway(); - const gateways = await chirpstackGatewaysPromise; - await this.seedGatewayStatus(gateways, latestStatusHistories); - } catch (e) { - this.logger.error("Error in applicationBootstrap"); - throw new InternalServerErrorException(e); - } + constructor( + @Inject(GatewayStatusHistoryService) + private statusHistoryService: GatewayStatusHistoryService, + @Inject(ChirpstackGatewayService) + private chirpstackGatewayService: ChirpstackGatewayService + ) {} + private readonly logger = new Logger(GatewayBootstrapperService.name); + + async onApplicationBootstrap(): Promise { + try { + const chirpstackGatewaysPromise = this.chirpstackGatewayService.getAll(); + const latestStatusHistories = await this.statusHistoryService.findLatestPerGateway(); + const gateways = await chirpstackGatewaysPromise; + await this.seedGatewayStatus(gateways, latestStatusHistories); + } catch (e) { + this.logger.error("Error in applicationBootstrap"); + throw new InternalServerErrorException(e); } - - /** - * Populate the gateway status table with an entry for each new gateway. - * @param gateways All chirpstack gateways - * @param statusHistories Existing status histories to check against - */ - private async seedGatewayStatus(gateways: ListAllGatewaysResponseDto, statusHistories: GatewayStatusHistory[]) { - const now = new Date(); - const errorTime = new Date(); - errorTime.setSeconds(errorTime.getSeconds() - 150); - - // Don't overwrite ones which already have a status history - const newHistoriesForMissingGateways = gateways.resultList.reduce((res: GatewayStatusHistory[], gateway) => { - if (!statusHistories.some(history => history.mac === gateway.gatewayId) && gateway.lastSeenAt) { - const lastSeenDate = gateway.lastSeenAt; - - const wasOnline = errorTime.getTime() < lastSeenDate.getTime(); - - res.push({ - mac: gateway.gatewayId, - timestamp: now, - wasOnline, - } as GatewayStatusHistory); - } - - return res; - }, []); - - if (newHistoriesForMissingGateways.length) { - await this.statusHistoryService.createMany(newHistoriesForMissingGateways); - } + } + + /** + * Populate the gateway status table with an entry for each new gateway. + * @param gateways All chirpstack gateways + * @param statusHistories Existing status histories to check against + */ + private async seedGatewayStatus(gateways: ListAllGatewaysResponseDto, statusHistories: GatewayStatusHistory[]) { + const now = new Date(); + const errorTime = new Date(); + errorTime.setSeconds(errorTime.getSeconds() - 150); + + // Don't overwrite ones which already have a status history + const newHistoriesForMissingGateways = gateways.resultList.reduce((res: GatewayStatusHistory[], gateway) => { + if (!statusHistories.some(history => history.mac === gateway.gatewayId) && gateway.lastSeenAt) { + const lastSeenDate = gateway.lastSeenAt; + + const wasOnline = errorTime.getTime() < lastSeenDate.getTime(); + + res.push({ + mac: gateway.gatewayId, + timestamp: now, + wasOnline, + } as GatewayStatusHistory); + } + + return res; + }, []); + + if (newHistoriesForMissingGateways.length) { + await this.statusHistoryService.createMany(newHistoriesForMissingGateways); } + } } diff --git a/src/services/chirpstack/gateway-status-history.service.ts b/src/services/chirpstack/gateway-status-history.service.ts index 5638b016..22fca147 100644 --- a/src/services/chirpstack/gateway-status-history.service.ts +++ b/src/services/chirpstack/gateway-status-history.service.ts @@ -1,6 +1,6 @@ import { - GatewayGetAllStatusResponseDto, - ListAllGatewayStatusDto, + GatewayGetAllStatusResponseDto, + ListAllGatewayStatusDto, } from "@dto/chirpstack/backend/gateway-all-status.dto"; import { GatewayStatus } from "@dto/chirpstack/backend/gateway-status.dto"; import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; @@ -14,141 +14,141 @@ import { GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; @Injectable() export class GatewayStatusHistoryService { - constructor( - @InjectRepository(GatewayStatusHistory) - private gatewayStatusHistoryRepository: Repository, - private chirpstackGatewayService: ChirpstackGatewayService - ) {} - private readonly logger = new Logger(GatewayStatusHistoryService.name); - - public async findAllWithChirpstack(query: ListAllGatewayStatusDto): Promise { - // Very expensive operation. Since no gateway data is stored on the backend database, we need - // to get them from Chirpstack. There's no filter by tags support so we must fetch all gateways. - const gateways = await this.chirpstackGatewayService.getAll(query.organizationId); - const gatewayIds = gateways.resultList.map(gateway => gateway.gatewayId); - const fromDate = gatewayStatusIntervalToDate(query.timeInterval); - - if (!gatewayIds.length) { - return { count: 0, data: [] }; - } - - const statusHistoriesInPeriod = await this.gatewayStatusHistoryRepository.find({ - where: { - mac: In(gatewayIds), - timestamp: MoreThanOrEqual(fromDate), - }, - }); - // To know the status of each gateway up till the first status since the start date, - // we must fetch the previous status - const latestStatusHistoryPerGatewayBeforePeriod = await this.fetchLatestStatusBeforeDate(gatewayIds, fromDate); - - const statusHistories = this.mergeStatusHistories( - fromDate, - statusHistoriesInPeriod, - latestStatusHistoryPerGatewayBeforePeriod - ); - - const data: GatewayStatus[] = this.mapStatusHistoryToGateways(gateways.resultList, statusHistories); - - return { - data, - count: gateways.totalCount, - }; + constructor( + @InjectRepository(GatewayStatusHistory) + private gatewayStatusHistoryRepository: Repository, + private chirpstackGatewayService: ChirpstackGatewayService + ) {} + private readonly logger = new Logger(GatewayStatusHistoryService.name); + + public async findAllWithChirpstack(query: ListAllGatewayStatusDto): Promise { + // Very expensive operation. Since no gateway data is stored on the backend database, we need + // to get them from Chirpstack. There's no filter by tags support so we must fetch all gateways. + const gateways = await this.chirpstackGatewayService.getAll(query.organizationId); + const gatewayIds = gateways.resultList.map(gateway => gateway.gatewayId); + const fromDate = gatewayStatusIntervalToDate(query.timeInterval); + + if (!gatewayIds.length) { + return { count: 0, data: [] }; } - public async findOne(gateway: GatewayResponseDto, timeInterval: GatewayStatusInterval): Promise { - const fromDate = gatewayStatusIntervalToDate(timeInterval); - - const statusHistoriesInPeriod = await this.gatewayStatusHistoryRepository.find({ - where: { - mac: gateway.gatewayId, - timestamp: MoreThanOrEqual(fromDate), - }, + const statusHistoriesInPeriod = await this.gatewayStatusHistoryRepository.find({ + where: { + mac: In(gatewayIds), + timestamp: MoreThanOrEqual(fromDate), + }, + }); + // To know the status of each gateway up till the first status since the start date, + // we must fetch the previous status + const latestStatusHistoryPerGatewayBeforePeriod = await this.fetchLatestStatusBeforeDate(gatewayIds, fromDate); + + const statusHistories = this.mergeStatusHistories( + fromDate, + statusHistoriesInPeriod, + latestStatusHistoryPerGatewayBeforePeriod + ); + + const data: GatewayStatus[] = this.mapStatusHistoryToGateways(gateways.resultList, statusHistories); + + return { + data, + count: gateways.totalCount, + }; + } + + public async findOne(gateway: GatewayResponseDto, timeInterval: GatewayStatusInterval): Promise { + const fromDate = gatewayStatusIntervalToDate(timeInterval); + + const statusHistoriesInPeriod = await this.gatewayStatusHistoryRepository.find({ + where: { + mac: gateway.gatewayId, + timestamp: MoreThanOrEqual(fromDate), + }, + }); + + const latestStatusHistoryPerGatewayBeforePeriod = await this.fetchLatestStatusBeforeDate( + [gateway.gatewayId], + fromDate + ); + + const statusHistories = this.mergeStatusHistories( + fromDate, + statusHistoriesInPeriod, + latestStatusHistoryPerGatewayBeforePeriod + ); + + return this.mapStatusHistoryToGateway(gateway, statusHistories); + } + + public findLatestPerGateway(): Promise { + return this.gatewayStatusHistoryRepository + .createQueryBuilder("status_history") + .distinctOn([nameof("mac")]) + .orderBy({ + [nameof("mac")]: "ASC", + [nameof("timestamp")]: "DESC", + }) + .getMany(); + } + + public createMany(histories: GatewayStatusHistory[]): Promise { + return this.gatewayStatusHistoryRepository.save(histories); + } + + private fetchLatestStatusBeforeDate(gatewayIds: string[], date: Date) { + return this.gatewayStatusHistoryRepository + .createQueryBuilder("status_history") + .where("status_history.mac IN (:...gatewayIds)", { gatewayIds }) + .andWhere("status_history.timestamp < :date", { date }) + .distinctOn([nameof("mac")]) + .orderBy({ + [nameof("mac")]: "ASC", + [nameof("timestamp")]: "DESC", + }) + .getMany(); + } + + private mergeStatusHistories( + fromDate: Date, + statusHistoriesInPeriod: GatewayStatusHistory[], + latestStatusHistoryPerGateway: GatewayStatusHistory[] + ): GatewayStatusHistory[] { + const combinedHistories = statusHistoriesInPeriod.slice(); + + latestStatusHistoryPerGateway.forEach(latestHistory => { + // Ensure that the timestamp is within the time period + latestHistory.timestamp = fromDate; + combinedHistories.push(latestHistory); + }); + + return combinedHistories; + } + + private mapStatusHistoryToGateways( + gateways: GatewayResponseDto[], + statusHistories: GatewayStatusHistory[] + ): GatewayStatus[] { + return gateways.map(gateway => { + return this.mapStatusHistoryToGateway(gateway, statusHistories); + }); + } + + private mapStatusHistoryToGateway(gateway: GatewayResponseDto, statusHistories: GatewayStatusHistory[]) { + const statusTimestamps = statusHistories.reduce((res: GatewayStatus["statusTimestamps"], history) => { + if (history.mac === gateway.gatewayId) { + res.push({ + timestamp: history.timestamp, + wasOnline: history.wasOnline, }); + } - const latestStatusHistoryPerGatewayBeforePeriod = await this.fetchLatestStatusBeforeDate( - [gateway.gatewayId], - fromDate - ); - - const statusHistories = this.mergeStatusHistories( - fromDate, - statusHistoriesInPeriod, - latestStatusHistoryPerGatewayBeforePeriod - ); - - return this.mapStatusHistoryToGateway(gateway, statusHistories); - } - - public findLatestPerGateway(): Promise { - return this.gatewayStatusHistoryRepository - .createQueryBuilder("status_history") - .distinctOn([nameof("mac")]) - .orderBy({ - [nameof("mac")]: "ASC", - [nameof("timestamp")]: "DESC", - }) - .getMany(); - } + return res; + }, []); - public createMany(histories: GatewayStatusHistory[]): Promise { - return this.gatewayStatusHistoryRepository.save(histories); - } - - private fetchLatestStatusBeforeDate(gatewayIds: string[], date: Date) { - return this.gatewayStatusHistoryRepository - .createQueryBuilder("status_history") - .where("status_history.mac IN (:...gatewayIds)", { gatewayIds }) - .andWhere("status_history.timestamp < :date", { date }) - .distinctOn([nameof("mac")]) - .orderBy({ - [nameof("mac")]: "ASC", - [nameof("timestamp")]: "DESC", - }) - .getMany(); - } - - private mergeStatusHistories( - fromDate: Date, - statusHistoriesInPeriod: GatewayStatusHistory[], - latestStatusHistoryPerGateway: GatewayStatusHistory[] - ): GatewayStatusHistory[] { - const combinedHistories = statusHistoriesInPeriod.slice(); - - latestStatusHistoryPerGateway.forEach(latestHistory => { - // Ensure that the timestamp is within the time period - latestHistory.timestamp = fromDate; - combinedHistories.push(latestHistory); - }); - - return combinedHistories; - } - - private mapStatusHistoryToGateways( - gateways: GatewayResponseDto[], - statusHistories: GatewayStatusHistory[] - ): GatewayStatus[] { - return gateways.map(gateway => { - return this.mapStatusHistoryToGateway(gateway, statusHistories); - }); - } - - private mapStatusHistoryToGateway(gateway: GatewayResponseDto, statusHistories: GatewayStatusHistory[]) { - const statusTimestamps = statusHistories.reduce((res: GatewayStatus["statusTimestamps"], history) => { - if (history.mac === gateway.gatewayId) { - res.push({ - timestamp: history.timestamp, - wasOnline: history.wasOnline, - }); - } - - return res; - }, []); - - return { - id: gateway.gatewayId, - name: gateway.name, - statusTimestamps, - }; - } + return { + id: gateway.gatewayId, + name: gateway.name, + statusTimestamps, + }; + } } diff --git a/src/services/chirpstack/generic-chirpstack-configuration.service.ts b/src/services/chirpstack/generic-chirpstack-configuration.service.ts index f712e0d6..558d9ef6 100644 --- a/src/services/chirpstack/generic-chirpstack-configuration.service.ts +++ b/src/services/chirpstack/generic-chirpstack-configuration.service.ts @@ -1,9 +1,9 @@ import { - BadRequestException, - Injectable, - InternalServerErrorException, - Logger, - NotFoundException, + BadRequestException, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, } from "@nestjs/common"; import { ListAllChirpstackApplicationsResponseDto } from "@dto/chirpstack/list-all-applications-response.dto"; import { Metadata, ServiceError, credentials } from "@grpc/grpc-js"; @@ -21,214 +21,214 @@ import { MulticastGroupServiceClient } from "@chirpstack/chirpstack-api/api/mult @Injectable() export class GenericChirpstackConfigurationService { - baseUrlGRPC = `${configuration()["chirpstack"]["hostname"]}:${configuration()["chirpstack"]["port"]}`; + baseUrlGRPC = `${configuration()["chirpstack"]["hostname"]}:${configuration()["chirpstack"]["port"]}`; - private readonly innerLogger = new Logger(GenericChirpstackConfigurationService.name); - protected applicationServiceClient = new ApplicationServiceClient(this.baseUrlGRPC, credentials.createInsecure()); - protected deviceServiceClient = new DeviceServiceClient(this.baseUrlGRPC, credentials.createInsecure()); - protected gatewayClient = new GatewayServiceClient(this.baseUrlGRPC, credentials.createInsecure()); - protected deviceProfileClient = new DeviceProfileServiceClient(this.baseUrlGRPC, credentials.createInsecure()); - protected multicastServiceClient = new MulticastGroupServiceClient(this.baseUrlGRPC, credentials.createInsecure()); - protected readonly ORG_ID_KEY = "internalOrganizationId"; - protected readonly UPDATED_BY_KEY = "os2iot-updated-by"; - protected readonly CREATED_BY_KEY = "os2iot-created-by"; + private readonly innerLogger = new Logger(GenericChirpstackConfigurationService.name); + protected applicationServiceClient = new ApplicationServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected deviceServiceClient = new DeviceServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected gatewayClient = new GatewayServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected deviceProfileClient = new DeviceProfileServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected multicastServiceClient = new MulticastGroupServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + protected readonly ORG_ID_KEY = "internalOrganizationId"; + protected readonly UPDATED_BY_KEY = "os2iot-updated-by"; + protected readonly CREATED_BY_KEY = "os2iot-created-by"; - makeMetadataHeader(): Metadata { - const metadata = new Metadata(); - metadata.set("authorization", "Bearer " + configuration()["chirpstack"]["apikey"]); - return metadata; - } + makeMetadataHeader(): Metadata { + const metadata = new Metadata(); + metadata.set("authorization", "Bearer " + configuration()["chirpstack"]["apikey"]); + return metadata; + } - async post(logName: string, client: any, request: any): Promise { - const metaData = this.makeMetadataHeader(); - const createPromise = new Promise((resolve, reject) => { - client.create(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.innerLogger.debug(`post:${logName} success`); - resolve(resp.toObject()); - } - }); - }); - try { - return await createPromise; - } catch (err) { - this.innerLogger.error(`POST ${logName} got error: ${err}`); - throw new BadRequestException(); + async post(logName: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + const createPromise = new Promise((resolve, reject) => { + client.create(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`post:${logName} success`); + resolve(resp.toObject()); } + }); + }); + try { + return await createPromise; + } catch (err) { + this.innerLogger.error(`POST ${logName} got error: ${err}`); + throw new BadRequestException(); } + } - async put(logName: string, client: any, request: any): Promise { - const metaData = this.makeMetadataHeader(); - const updatePromise = new Promise((resolve, reject) => { - client.update(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.innerLogger.debug(`update :${logName} success`); - resolve(resp); - } - }); - }); - try { - await updatePromise; - return; - } catch (err) { - this.innerLogger.error(`UPDATE ${logName} got error: ${err}`); - throw new BadRequestException(); + async put(logName: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + const updatePromise = new Promise((resolve, reject) => { + client.update(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`update :${logName} success`); + resolve(resp); } + }); + }); + try { + await updatePromise; + return; + } catch (err) { + this.innerLogger.error(`UPDATE ${logName} got error: ${err}`); + throw new BadRequestException(); } + } - async getOneById(logName: string, id: string, client: any, request: any): Promise { - const metaData = this.makeMetadataHeader(); - request.setId(id); - const getPromise = new Promise((resolve, reject) => { - client.get(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.innerLogger.debug(`get from:${logName} success`); - resolve(resp); - } - }); - }); - try { - return await getPromise; - } catch (err) { - this.innerLogger.error(`GET ${logName} got error: ${err}`); - throw new NotFoundException(); + async getOneById(logName: string, id: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + request.setId(id); + const getPromise = new Promise((resolve, reject) => { + client.get(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`get from:${logName} success`); + resolve(resp); } + }); + }); + try { + return await getPromise; + } catch (err) { + this.innerLogger.error(`GET ${logName} got error: ${err}`); + throw new NotFoundException(); } + } - async delete(logName: string, client: any, request: any): Promise { - if (client) { - const metaData = this.makeMetadataHeader(); - const deletePromise = new Promise((resolve, reject) => { - client.delete(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.innerLogger.debug(`delete :${logName} success`); - resolve(resp); - } - }); - }); - try { - await deletePromise; - return; - } catch (err) { - this.innerLogger.error(`DELETE ${logName} got error: ${err}`); - throw new BadRequestException(); - } - } + async delete(logName: string, client: any, request: any): Promise { + if (client) { + const metaData = this.makeMetadataHeader(); + const deletePromise = new Promise((resolve, reject) => { + client.delete(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`delete :${logName} success`); + resolve(resp); + } + }); + }); + try { + await deletePromise; + return; + } catch (err) { + this.innerLogger.error(`DELETE ${logName} got error: ${err}`); + throw new BadRequestException(); + } } + } - async get(logName: string, client: any, request: any): Promise { - const metaData = this.makeMetadataHeader(); - const getPromise = new Promise((resolve, reject) => { - client.get(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.innerLogger.debug(`get from:${logName} success`); - resolve(resp); - } - }); - }); - try { - return await getPromise; - } catch (err) { - this.innerLogger.error(`GET ${logName} got error: ${err}`); - throw new NotFoundException(); + async get(logName: string, client: any, request: any): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + client.get(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.innerLogger.debug(`get from:${logName} success`); + resolve(resp); } + }); + }); + try { + return await getPromise; + } catch (err) { + this.innerLogger.error(`GET ${logName} got error: ${err}`); + throw new NotFoundException(); } + } - async getAllApplicationsWithPagination(tenantID: string): Promise { - const req = new ListApplicationsRequest(); - req.setTenantId(await this.getDefaultOrganizationId()); + async getAllApplicationsWithPagination(tenantID: string): Promise { + const req = new ListApplicationsRequest(); + req.setTenantId(await this.getDefaultOrganizationId()); - const result = await this.getAllWithPagination( - `applications?limit=100&organizationID=${tenantID}`, - this.applicationServiceClient, - req, - 100, - undefined - ); - const chirpstackApplicationResponseDto: ChirpstackApplicationResponseDto[] = []; - result.resultList.map(e => { - const resultItem: ChirpstackApplicationResponseDto = { - name: e.name, - description: e.description, - id: e.id, - tenantId: tenantID, - }; - chirpstackApplicationResponseDto.push(resultItem); - }); - return { - totalCount: result.totalCount, - resultList: chirpstackApplicationResponseDto, - }; - } + const result = await this.getAllWithPagination( + `applications?limit=100&organizationID=${tenantID}`, + this.applicationServiceClient, + req, + 100, + undefined + ); + const chirpstackApplicationResponseDto: ChirpstackApplicationResponseDto[] = []; + result.resultList.map(e => { + const resultItem: ChirpstackApplicationResponseDto = { + name: e.name, + description: e.description, + id: e.id, + tenantId: tenantID, + }; + chirpstackApplicationResponseDto.push(resultItem); + }); + return { + totalCount: result.totalCount, + resultList: chirpstackApplicationResponseDto, + }; + } - async getAllWithPagination( - logName: string, - client: any, - request: any, - limit?: number, - offset?: number - ): Promise { - const metaData = this.makeMetadataHeader(); - request.setLimit(limit); - request.setOffset(offset); + async getAllWithPagination( + logName: string, + client: any, + request: any, + limit?: number, + offset?: number + ): Promise { + const metaData = this.makeMetadataHeader(); + request.setLimit(limit); + request.setOffset(offset); - const getListPromise = new Promise((resolve, reject) => { - client.list(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - const result = resp.toObject(); - resolve(result); - this.innerLogger.debug(`get all from:${logName} success`); - } - }); - }); - try { - return await getListPromise; - } catch (err) { - this.innerLogger.error(`GET ALL ${logName} got error: ${JSON.stringify(err)}`); - throw new NotFoundException(); + const getListPromise = new Promise((resolve, reject) => { + client.list(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + const result = resp.toObject(); + resolve(result); + this.innerLogger.debug(`get all from:${logName} success`); } + }); + }); + try { + return await getListPromise; + } catch (err) { + this.innerLogger.error(`GET ALL ${logName} got error: ${JSON.stringify(err)}`); + throw new NotFoundException(); } + } - public async getTenants(limit?: number, offset?: number): Promise { - const tenantClient = new TenantServiceClient(this.baseUrlGRPC, credentials.createInsecure()); - const req = new ListTenantsRequest(); + public async getTenants(limit?: number, offset?: number): Promise { + const tenantClient = new TenantServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + const req = new ListTenantsRequest(); - const res = await this.getAllWithPagination( - "organizations", - tenantClient, - req, - limit, - offset - ); - return res; - } + const res = await this.getAllWithPagination( + "organizations", + tenantClient, + req, + limit, + offset + ); + return res; + } - public async getDefaultOrganizationId(): Promise { - let id = null; - await this.getTenants(1000, 0).then(response => { - response.resultList.forEach(element => { - if (element.name.toLowerCase() == "os2iot" || element.name.toLowerCase() == "chirpstack") { - id = element.id; - } - }); - }); - if (id) { - return id; + public async getDefaultOrganizationId(): Promise { + let id = null; + await this.getTenants(1000, 0).then(response => { + response.resultList.forEach(element => { + if (element.name.toLowerCase() == "os2iot" || element.name.toLowerCase() == "chirpstack") { + id = element.id; } - throw new InternalServerErrorException( - "Could not find any Organization in Chirpstack named: 'OS2iot' or 'chirpstack'" - ); + }); + }); + if (id) { + return id; } + throw new InternalServerErrorException( + "Could not find any Organization in Chirpstack named: 'OS2iot' or 'chirpstack'" + ); + } } diff --git a/src/services/chirpstack/multicast.service.ts b/src/services/chirpstack/multicast.service.ts index 4bd39cfc..f91d28c8 100644 --- a/src/services/chirpstack/multicast.service.ts +++ b/src/services/chirpstack/multicast.service.ts @@ -15,30 +15,30 @@ import { LorawanMulticastDefinition } from "@entities/lorawan-multicast.entity"; import { IoTDeviceType } from "@enum/device-type.enum"; import { LoRaWANDevice } from "@entities/lorawan-device.entity"; import { - MulticastDownlinkQueueResponseDto, - MulticastQueueItem, + MulticastDownlinkQueueResponseDto, + MulticastQueueItem, } from "@dto/chirpstack/chirpstack-multicast-downlink-queue-response.dto"; import { CreateMulticastDownlinkDto } from "@dto/create-multicast-downlink.dto"; import { - CreateChirpstackMulticastQueueItemDto, - CreateChirpstackMulticastQueueItemResponse, + CreateChirpstackMulticastQueueItemDto, + CreateChirpstackMulticastQueueItemResponse, } from "@dto/chirpstack/create-chirpstack-multicast-queue-item.dto"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; import { ChirpstackDeviceContentsDto } from "@dto/chirpstack/chirpstack-device-contents.dto"; import { - AddDeviceToMulticastGroupRequest, - CreateMulticastGroupRequest, - DeleteMulticastGroupRequest, - EnqueueMulticastGroupQueueItemRequest, - FlushMulticastGroupQueueRequest, - GetMulticastGroupRequest, - GetMulticastGroupResponse, - ListMulticastGroupQueueRequest, - ListMulticastGroupQueueResponse, - MulticastGroup, - MulticastGroupQueueItem, - MulticastGroupType, - UpdateMulticastGroupRequest, + AddDeviceToMulticastGroupRequest, + CreateMulticastGroupRequest, + DeleteMulticastGroupRequest, + EnqueueMulticastGroupQueueItemRequest, + FlushMulticastGroupQueueRequest, + GetMulticastGroupRequest, + GetMulticastGroupResponse, + ListMulticastGroupQueueRequest, + ListMulticastGroupQueueResponse, + MulticastGroup, + MulticastGroupQueueItem, + MulticastGroupType, + UpdateMulticastGroupRequest, } from "@chirpstack/chirpstack-api/api/multicast_group_pb"; import { MulticastGroupServiceClient } from "@chirpstack/chirpstack-api/api/multicast_group_grpc_pb"; import { ServiceError } from "@grpc/grpc-js"; @@ -47,541 +47,539 @@ import { multicastGroup } from "@enum/multicast-type.enum"; @Injectable() export class MulticastService extends GenericChirpstackConfigurationService { - constructor( - @InjectRepository(Multicast) - private multicastRepository: Repository, - @Inject(forwardRef(() => ApplicationService)) // because of circular reference - private applicationService: ApplicationService, - private chirpStackDeviceService: ChirpstackDeviceService - ) { - super(); + constructor( + @InjectRepository(Multicast) + private multicastRepository: Repository, + @Inject(forwardRef(() => ApplicationService)) // because of circular reference + private applicationService: ApplicationService, + private chirpStackDeviceService: ChirpstackDeviceService + ) { + super(); + } + private readonly logger = new Logger(MulticastService.name); + multicastGroupUrl = "multicast-groups"; + + async findAndCountAllWithPagination( + // inspired by datatarget and other places in this project. + // Repository syntax doesn't yet support ordering by relation: https://github.com/typeorm/typeorm/issues/2620 + // Therefore we use the QueryBuilder ... + query?: ListAllMulticastsDto, + applicationIds?: number[] + ): Promise { + const orderByColumn = this.getSortingForMulticasts(query); + const direction = query?.sort?.toUpperCase() == "DESC" ? "DESC" : "ASC"; + + let queryBuilder = this.multicastRepository + .createQueryBuilder("multicast") + .innerJoinAndSelect("multicast.application", "application") + .innerJoinAndSelect("multicast.lorawanMulticastDefinition", "lorawan-multicast") + .skip(query?.offset ? +query.offset : 0) + .take(query?.limit ? +query.limit : 100) + .orderBy(orderByColumn, direction); + + // Only apply applicationId filter, if one is given. + queryBuilder = this.filterByApplication(query, queryBuilder, applicationIds); + + const [result, total] = await queryBuilder.getManyAndCount(); + + return { + data: result, + count: total, + }; + } + private getSortingForMulticasts(query: ListAllMulticastsDto) { + let orderBy = `multicast.id`; + if ((query?.orderOn != null && query.orderOn == "id") || query.orderOn == "groupName") { + orderBy = `multicast.${query.orderOn}`; } - private readonly logger = new Logger(MulticastService.name); - multicastGroupUrl = "multicast-groups"; - - async findAndCountAllWithPagination( - // inspired by datatarget and other places in this project. - // Repository syntax doesn't yet support ordering by relation: https://github.com/typeorm/typeorm/issues/2620 - // Therefore we use the QueryBuilder ... - query?: ListAllMulticastsDto, - applicationIds?: number[] - ): Promise { - const orderByColumn = this.getSortingForMulticasts(query); - const direction = query?.sort?.toUpperCase() == "DESC" ? "DESC" : "ASC"; - - let queryBuilder = this.multicastRepository - .createQueryBuilder("multicast") - .innerJoinAndSelect("multicast.application", "application") - .innerJoinAndSelect("multicast.lorawanMulticastDefinition", "lorawan-multicast") - .skip(query?.offset ? +query.offset : 0) - .take(query?.limit ? +query.limit : 100) - .orderBy(orderByColumn, direction); - - // Only apply applicationId filter, if one is given. - queryBuilder = this.filterByApplication(query, queryBuilder, applicationIds); - - const [result, total] = await queryBuilder.getManyAndCount(); - - return { - data: result, - count: total, - }; + return orderBy; + } + private filterByApplication( + query: ListAllMulticastsDto, + queryBuilder: SelectQueryBuilder, + applicationIds: number[] + ) { + if (query.applicationId) { + queryBuilder = queryBuilder.where("multicast.application = :appId", { + appId: query.applicationId, + }); + } else if (applicationIds) { + queryBuilder = queryBuilder.where('"application"."id" IN (:...allowedApplications)', { + allowedApplications: applicationIds, + }); } - private getSortingForMulticasts(query: ListAllMulticastsDto) { - let orderBy = `multicast.id`; - if ((query?.orderOn != null && query.orderOn == "id") || query.orderOn == "groupName") { - orderBy = `multicast.${query.orderOn}`; + return queryBuilder; + } + + async findOne(id: number): Promise { + return await this.multicastRepository.findOneOrFail({ + where: { id }, + relations: ["application", "lorawanMulticastDefinition", "iotDevices"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } + + async create(createMulticastDto: CreateMulticastDto, userId: number): Promise { + const dbMulticast = new Multicast(); + dbMulticast.lorawanMulticastDefinition = new LorawanMulticastDefinition(); + + const mappedDbMulticast = await this.mapMulticastDtoToDbMulticast(createMulticastDto, dbMulticast); + mappedDbMulticast.createdBy = userId; + mappedDbMulticast.updatedBy = userId; + mappedDbMulticast.lorawanMulticastDefinition.createdBy = userId; + mappedDbMulticast.lorawanMulticastDefinition.updatedBy = userId; + + if (!!createMulticastDto.iotDevices) { + const lorawanDevices = this.filterForLoRaWAN(createMulticastDto); + if (lorawanDevices.length > 0) { + if (await this.checkForDifferentAppID(lorawanDevices)) { + // If they all have same serviceID / appID then proceed. + await this.createMulticastInChirpstack(createMulticastDto, lorawanDevices, mappedDbMulticast); + } else { + throw new BadRequestException(ErrorCodes.InvalidPost); } - return orderBy; + } } - private filterByApplication( - query: ListAllMulticastsDto, - queryBuilder: SelectQueryBuilder, - applicationIds: number[] - ) { - if (query.applicationId) { - queryBuilder = queryBuilder.where("multicast.application = :appId", { - appId: query.applicationId, - }); - } else if (applicationIds) { - queryBuilder = queryBuilder.where('"application"."id" IN (:...allowedApplications)', { - allowedApplications: applicationIds, - }); - } - return queryBuilder; + return await this.multicastRepository.save(mappedDbMulticast); + } + + async createMulticastInChirpstack( + createMulticastDto: CreateMulticastDto | UpdateMulticastDto, + lorawanDevices: LoRaWANDevice[], + mappedDbMulticast: Multicast + ): Promise { + const mappedChirpStackMulticast = await this.mapMulticastDtoToChirpStackMulticast( + createMulticastDto, + lorawanDevices[0] // used for setting appID + ); + const req = new CreateMulticastGroupRequest(); + req.setMulticastGroup(mappedChirpStackMulticast); + const result: IdResponse = await this.post(this.multicastGroupUrl, this.multicastServiceClient, req); // This creates the multicast in chirpstack. Chirpstack returns an id as a string + + await this.addDevices(createMulticastDto, result); // iotDevices are added to multicast in a seperate endpoint. + + this.handlePossibleError(result, createMulticastDto); + + if (result.id) { + mappedDbMulticast.lorawanMulticastDefinition.chirpstackGroupId = result.id; } - - async findOne(id: number): Promise { - return await this.multicastRepository.findOneOrFail({ - where: { id }, - relations: ["application", "lorawanMulticastDefinition", "iotDevices"], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); + } + + async update( + existingMulticast: Multicast, + updateMulticastDto: UpdateMulticastDto, + userId: number + ): Promise { + const oldMulticast: Multicast = { ...existingMulticast }; + const mappedMulticast = await this.mapMulticastDtoToDbMulticast(updateMulticastDto, existingMulticast); + const lorawanDevices = this.filterForLoRaWAN(updateMulticastDto); + const oldLorawanDevices = this.filterForLoRaWAN(oldMulticast); + if (lorawanDevices.length > 0 || oldLorawanDevices.length > 0) { + // check if new lorawan devices is included. If so, either create or update in chirpstack. Otherwise, just update db + if (!existingMulticast.lorawanMulticastDefinition.chirpstackGroupId) { + await this.createIfNotInChirpstack(lorawanDevices, updateMulticastDto, mappedMulticast); + } else { + await this.updateLogic(existingMulticast, lorawanDevices, updateMulticastDto); + } } - - async create(createMulticastDto: CreateMulticastDto, userId: number): Promise { - const dbMulticast = new Multicast(); - dbMulticast.lorawanMulticastDefinition = new LorawanMulticastDefinition(); - - const mappedDbMulticast = await this.mapMulticastDtoToDbMulticast(createMulticastDto, dbMulticast); - mappedDbMulticast.createdBy = userId; - mappedDbMulticast.updatedBy = userId; - mappedDbMulticast.lorawanMulticastDefinition.createdBy = userId; - mappedDbMulticast.lorawanMulticastDefinition.updatedBy = userId; - - if (!!createMulticastDto.iotDevices) { - const lorawanDevices = this.filterForLoRaWAN(createMulticastDto); - if (lorawanDevices.length > 0) { - if (await this.checkForDifferentAppID(lorawanDevices)) { - // If they all have same serviceID / appID then proceed. - await this.createMulticastInChirpstack(createMulticastDto, lorawanDevices, mappedDbMulticast); - } else { - throw new BadRequestException(ErrorCodes.InvalidPost); - } - } - } - return await this.multicastRepository.save(mappedDbMulticast); + mappedMulticast.updatedBy = userId; + return await this.multicastRepository.save(mappedMulticast); + } + + async updateMulticastToChirpstack( + updateMulticastDto: UpdateMulticastDto, + existingChirpStackMulticast: CreateMulticastChirpStackDto, + lorawanDevices: LoRaWANDevice[] + ): Promise { + const mappedChirpStackMulticast = await this.mapMulticastDtoToChirpStackMulticast( + updateMulticastDto, + lorawanDevices[0] + ); + mappedChirpStackMulticast.setId(existingChirpStackMulticast.multicastGroup.id); + const req = new UpdateMulticastGroupRequest(); + req.setMulticastGroup(mappedChirpStackMulticast); + try { + await this.put(this.multicastGroupUrl, this.multicastServiceClient, req); + } catch (e) { + throw new BadRequestException(e); } + } - async createMulticastInChirpstack( - createMulticastDto: CreateMulticastDto | UpdateMulticastDto, - lorawanDevices: LoRaWANDevice[], - mappedDbMulticast: Multicast - ): Promise { - const mappedChirpStackMulticast = await this.mapMulticastDtoToChirpStackMulticast( - createMulticastDto, - lorawanDevices[0] // used for setting appID - ); - const req = new CreateMulticastGroupRequest(); - req.setMulticastGroup(mappedChirpStackMulticast); - const result: IdResponse = await this.post(this.multicastGroupUrl, this.multicastServiceClient, req); // This creates the multicast in chirpstack. Chirpstack returns an id as a string - - await this.addDevices(createMulticastDto, result); // iotDevices are added to multicast in a seperate endpoint. + filterForLoRaWAN(multicastDto: CreateMulticastDto | Multicast | UpdateMulticastDto): LoRaWANDevice[] { + return multicastDto.iotDevices.filter(x => x.type === IoTDeviceType.LoRaWAN) as LoRaWANDevice[]; + } - this.handlePossibleError(result, createMulticastDto); + async validateNewDevicesAppID( + chirpStackMulticast: CreateMulticastChirpStackDto, + lorawanDevices: LoRaWANDevice[] + ): Promise { + const devices: ChirpstackDeviceContentsDto[] = []; - if (result.id) { - mappedDbMulticast.lorawanMulticastDefinition.chirpstackGroupId = result.id; - } + for (let index = 0; index < lorawanDevices.length; index++) { + const lora = await this.chirpStackDeviceService.getChirpstackDevice(lorawanDevices[index].deviceEUI); + devices.push(lora); } - async update( - existingMulticast: Multicast, - updateMulticastDto: UpdateMulticastDto, - userId: number - ): Promise { - const oldMulticast: Multicast = { ...existingMulticast }; - const mappedMulticast = await this.mapMulticastDtoToDbMulticast(updateMulticastDto, existingMulticast); - const lorawanDevices = this.filterForLoRaWAN(updateMulticastDto); - const oldLorawanDevices = this.filterForLoRaWAN(oldMulticast); - if (lorawanDevices.length > 0 || oldLorawanDevices.length > 0) { - // check if new lorawan devices is included. If so, either create or update in chirpstack. Otherwise, just update db - if (!existingMulticast.lorawanMulticastDefinition.chirpstackGroupId) { - await this.createIfNotInChirpstack(lorawanDevices, updateMulticastDto, mappedMulticast); - } else { - await this.updateLogic(existingMulticast, lorawanDevices, updateMulticastDto); - } - } - mappedMulticast.updatedBy = userId; - return await this.multicastRepository.save(mappedMulticast); + for (let i = 0; i < devices.length; i++) { + if (devices[i].applicationID !== chirpStackMulticast.multicastGroup.applicationID) { + // if one of the application id is different than the first one, then we know that there is different + // service profiles. Therefore, return false. + return false; + } } + return true; + } - async updateMulticastToChirpstack( - updateMulticastDto: UpdateMulticastDto, - existingChirpStackMulticast: CreateMulticastChirpStackDto, - lorawanDevices: LoRaWANDevice[] - ): Promise { - const mappedChirpStackMulticast = await this.mapMulticastDtoToChirpStackMulticast( - updateMulticastDto, - lorawanDevices[0] - ); - mappedChirpStackMulticast.setId(existingChirpStackMulticast.multicastGroup.id); - const req = new UpdateMulticastGroupRequest(); - req.setMulticastGroup(mappedChirpStackMulticast); - try { - await this.put(this.multicastGroupUrl, this.multicastServiceClient, req); - } catch (e) { - throw new BadRequestException(e); - } - } + async checkForDifferentAppID(lorawanDevices: LoRaWANDevice[]): Promise { + const devices: ChirpstackDeviceContentsDto[] = []; - filterForLoRaWAN(multicastDto: CreateMulticastDto | Multicast | UpdateMulticastDto): LoRaWANDevice[] { - return multicastDto.iotDevices.filter(x => x.type === IoTDeviceType.LoRaWAN) as LoRaWANDevice[]; + for (let index = 0; index < lorawanDevices.length; index++) { + const lora = await this.chirpStackDeviceService.getChirpstackDevice(lorawanDevices[index].deviceEUI); + devices.push(lora); } - - async validateNewDevicesAppID( - chirpStackMulticast: CreateMulticastChirpStackDto, - lorawanDevices: LoRaWANDevice[] - ): Promise { - const devices: ChirpstackDeviceContentsDto[] = []; - - for (let index = 0; index < lorawanDevices.length; index++) { - const lora = await this.chirpStackDeviceService.getChirpstackDevice(lorawanDevices[index].deviceEUI); - devices.push(lora); + if (devices.length > 0) { + const appID: string = devices[0].applicationID; // In chirpstack, an application is made on each service profile. Because of that, it's enough to + //check on AppID, instead of getting the application and then the service ID + + for (let i = 0; i < devices.length; i++) { + if (devices[i].applicationID !== appID) { + // if one of the application id is different than the first one, then we know that there is different + // service profiles. Therefore, return false. + return false; } - - for (let i = 0; i < devices.length; i++) { - if (devices[i].applicationID !== chirpStackMulticast.multicastGroup.applicationID) { - // if one of the application id is different than the first one, then we know that there is different - // service profiles. Therefore, return false. - return false; - } - } - return true; + } } - - async checkForDifferentAppID(lorawanDevices: LoRaWANDevice[]): Promise { - const devices: ChirpstackDeviceContentsDto[] = []; - - for (let index = 0; index < lorawanDevices.length; index++) { - const lora = await this.chirpStackDeviceService.getChirpstackDevice(lorawanDevices[index].deviceEUI); - devices.push(lora); - } - if (devices.length > 0) { - const appID: string = devices[0].applicationID; // In chirpstack, an application is made on each service profile. Because of that, it's enough to - //check on AppID, instead of getting the application and then the service ID - - for (let i = 0; i < devices.length; i++) { - if (devices[i].applicationID !== appID) { - // if one of the application id is different than the first one, then we know that there is different - // service profiles. Therefore, return false. - return false; - } - } - } - return true; // If the appId is equal for each element, then it's the same service profile + return true; // If the appId is equal for each element, then it's the same service profile + } + + async getChirpstackMulticast(multicastId: string): Promise { + const req = new GetMulticastGroupRequest(); + req.setId(multicastId); + const res = await this.get( + `multicast-groups/${multicastId}`, + this.multicastServiceClient, + req + ); + const multicast = res.getMulticastGroup(); + const multicastDtoContent: ChirpstackMulticastContentsDto = { + applicationID: multicast.getApplicationId(), + dr: multicast.getDr(), + fCnt: multicast.getFCnt(), + frequency: multicast.getFrequency(), + mcAddr: multicast.getMcAddr(), + mcAppSKey: multicast.getMcAppSKey(), + mcNwkSKey: multicast.getMcNwkSKey(), + name: multicast.getName(), + groupType: multicastGroup.ClassC, + id: multicast.getId(), + }; + + const returnDto: CreateMulticastChirpStackDto = { multicastGroup: multicastDtoContent }; + + return returnDto; + } + + async deleteMulticast(id: number, existingMulticast: Multicast): Promise { + const loraDevices = this.filterForLoRaWAN(existingMulticast); + if (loraDevices.length > 0) { + await this.deleteMulticastChirpstack(existingMulticast.lorawanMulticastDefinition.chirpstackGroupId); } - - async getChirpstackMulticast(multicastId: string): Promise { - const req = new GetMulticastGroupRequest(); - req.setId(multicastId); - const res = await this.get( - `multicast-groups/${multicastId}`, - this.multicastServiceClient, - req - ); - const multicast = res.getMulticastGroup(); - const multicastDtoContent: ChirpstackMulticastContentsDto = { - applicationID: multicast.getApplicationId(), - dr: multicast.getDr(), - fCnt: multicast.getFCnt(), - frequency: multicast.getFrequency(), - mcAddr: multicast.getMcAddr(), - mcAppSKey: multicast.getMcAppSKey(), - mcNwkSKey: multicast.getMcNwkSKey(), - name: multicast.getName(), - groupType: multicastGroup.ClassC, - id: multicast.getId(), - }; - - const returnDto: CreateMulticastChirpStackDto = { multicastGroup: multicastDtoContent }; - - return returnDto; + return this.multicastRepository.delete(id); + } + + async deleteMulticastChirpstack(id: string): Promise { + try { + const req = new DeleteMulticastGroupRequest(); + req.setId(id); + return await this.delete(this.multicastGroupUrl, this.multicastServiceClient, req); + } catch (err) { + throw err; } - - async deleteMulticast(id: number, existingMulticast: Multicast): Promise { - const loraDevices = this.filterForLoRaWAN(existingMulticast); - if (loraDevices.length > 0) { - await this.deleteMulticastChirpstack(existingMulticast.lorawanMulticastDefinition.chirpstackGroupId); - } - return this.multicastRepository.delete(id); + } + + private async mapMulticastDtoToDbMulticast( + multicastDto: CreateMulticastDto | UpdateMulticastDto, + multicast: Multicast + ): Promise { + multicast.groupName = multicastDto.name; + multicast.lorawanMulticastDefinition.address = multicastDto.mcAddr; + multicast.lorawanMulticastDefinition.applicationSessionKey = multicastDto.mcAppSKey; + multicast.lorawanMulticastDefinition.networkSessionKey = multicastDto.mcNwkSKey; + multicast.lorawanMulticastDefinition.dataRate = multicastDto.dr; + multicast.lorawanMulticastDefinition.frameCounter = multicastDto.fCnt; + multicast.lorawanMulticastDefinition.frequency = multicastDto.frequency; + multicast.lorawanMulticastDefinition.groupType = multicastDto.groupType; + multicast.iotDevices = multicastDto.iotDevices; + + if (multicastDto.applicationID !== null) { + try { + multicast.application = await this.applicationService.findOneWithoutRelations(multicastDto.applicationID); + } catch (err) { + this.logger.error(`Could not find application with id: ${multicastDto.applicationID}`); + + throw new BadRequestException(ErrorCodes.IdDoesNotExists); + } + } else { + throw new BadRequestException(ErrorCodes.IdMissing); } - async deleteMulticastChirpstack(id: string): Promise { - try { - const req = new DeleteMulticastGroupRequest(); - req.setId(id); - return await this.delete(this.multicastGroupUrl, this.multicastServiceClient, req); - } catch (err) { - throw err; - } + return multicast; + } + + private async mapMulticastDtoToChirpStackMulticast( + multicastDto: CreateMulticastDto | UpdateMulticastDto, + device: LoRaWANDevice + ): Promise { + const multicast = new MulticastGroup(); + + multicast.setName(multicastDto.name); + multicast.setMcAddr(multicastDto.mcAddr); + multicast.setMcAppSKey(multicastDto.mcAppSKey); + multicast.setMcNwkSKey(multicastDto.mcNwkSKey); + multicast.setDr(multicastDto.dr); + multicast.setFCnt(multicastDto.fCnt); + multicast.setFrequency(multicastDto.frequency); + multicast.setGroupType(MulticastGroupType.CLASS_C); + if (!!device) { + // if devices is included, at this point we know that devices is validated. Therefore we can use appID + multicast.setApplicationId(device.chirpstackApplicationId.toString()); + } else { + // used for update when all devices are removed + multicast.setApplicationId(multicast.getApplicationId()); } - private async mapMulticastDtoToDbMulticast( - multicastDto: CreateMulticastDto | UpdateMulticastDto, - multicast: Multicast - ): Promise { - multicast.groupName = multicastDto.name; - multicast.lorawanMulticastDefinition.address = multicastDto.mcAddr; - multicast.lorawanMulticastDefinition.applicationSessionKey = multicastDto.mcAppSKey; - multicast.lorawanMulticastDefinition.networkSessionKey = multicastDto.mcNwkSKey; - multicast.lorawanMulticastDefinition.dataRate = multicastDto.dr; - multicast.lorawanMulticastDefinition.frameCounter = multicastDto.fCnt; - multicast.lorawanMulticastDefinition.frequency = multicastDto.frequency; - multicast.lorawanMulticastDefinition.groupType = multicastDto.groupType; - multicast.iotDevices = multicastDto.iotDevices; - - if (multicastDto.applicationID !== null) { - try { - multicast.application = await this.applicationService.findOneWithoutRelations( - multicastDto.applicationID - ); - } catch (err) { - this.logger.error(`Could not find application with id: ${multicastDto.applicationID}`); - - throw new BadRequestException(ErrorCodes.IdDoesNotExists); - } - } else { - throw new BadRequestException(ErrorCodes.IdMissing); - } - - return multicast; + return multicast; + } + + private handlePossibleError( + result: IdResponse, + dto: CreateMulticastDto | UpdateMulticastDto | CreateChirpstackMulticastQueueItemDto + ): void { + if (!result.id) { + this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', failed`); + throw new BadRequestException({ + success: false, + error: result.id, + }); } - - private async mapMulticastDtoToChirpStackMulticast( - multicastDto: CreateMulticastDto | UpdateMulticastDto, - device: LoRaWANDevice - ): Promise { - const multicast = new MulticastGroup(); - - multicast.setName(multicastDto.name); - multicast.setMcAddr(multicastDto.mcAddr); - multicast.setMcAppSKey(multicastDto.mcAppSKey); - multicast.setMcNwkSKey(multicastDto.mcNwkSKey); - multicast.setDr(multicastDto.dr); - multicast.setFCnt(multicastDto.fCnt); - multicast.setFrequency(multicastDto.frequency); - multicast.setGroupType(MulticastGroupType.CLASS_C); - if (!!device) { - // if devices is included, at this point we know that devices is validated. Therefore we can use appID - multicast.setApplicationId(device.chirpstackApplicationId.toString()); + } + + private async addDevices( + multicastDto: CreateMulticastDto | UpdateMulticastDto, + chirpstackMulticastID: IdResponse // the id returned from chirpstack when the multicast is created in chirpstack. + ) { + multicastDto.iotDevices.forEach(async device => { + if (device.type === IoTDeviceType.LoRaWAN) { + let lorawanDevice: LoRaWANDevice = new LoRaWANDevice(); + lorawanDevice = device as LoRaWANDevice; // cast to LoRaWANDevice since it has DeviceEUI + const req = new AddDeviceToMulticastGroupRequest(); + req.setDevEui(lorawanDevice.deviceEUI); + req.setMulticastGroupId(chirpstackMulticastID.id); + + await this.addDeviceToMulticast( + this.multicastGroupUrl + "/" + chirpstackMulticastID.id + "/" + "devices", + this.multicastServiceClient, + req + ); + } + }); + } + + async addDeviceToMulticast( + logName: string, + client: MulticastGroupServiceClient, + request: AddDeviceToMulticastGroupRequest + ): Promise { + const metaData = this.makeMetadataHeader(); + const createPromise = new Promise((resolve, reject) => { + client.addDevice(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); } else { - // used for update when all devices are removed - multicast.setApplicationId(multicast.getApplicationId()); + this.logger.debug(`post:${logName} success`); + resolve(resp.toObject()); } - - return multicast; + }); + }); + try { + return await createPromise; + } catch (err) { + this.logger.error(`POST ${logName} got error: ${err}`); + throw new BadRequestException(); } - - private handlePossibleError( - result: IdResponse, - dto: CreateMulticastDto | UpdateMulticastDto | CreateChirpstackMulticastQueueItemDto - ): void { - if (!result.id) { - this.logger.error(`Error from Chirpstack: '${JSON.stringify(dto)}', failed`); - throw new BadRequestException({ - success: false, - error: result.id, - }); - } + } + + async getDownlinkQueue(multicastID: string): Promise { + const req = new ListMulticastGroupQueueRequest(); + req.setMulticastGroupId(multicastID); + const res = await this.getQueue(this.multicastServiceClient, req); + + const queueItems: MulticastQueueItem[] = res.getItemsList().map(queueItem => { + return { + multicastGroupId: queueItem.getMulticastGroupId(), + fCnt: queueItem.getFCnt(), + fPort: queueItem.getFPort(), + data: queueItem.getData_asB64(), + }; + }); + + const responseDto: MulticastDownlinkQueueResponseDto = { + deviceQueueItems: queueItems, + }; + return responseDto; + } + + public async createDownlink( + dto: CreateMulticastDownlinkDto, + multicast: Multicast + ): Promise { + const csDto: CreateChirpstackMulticastQueueItemDto = { + multicastQueueItem: { + fPort: dto.port, + multicastGroupID: multicast.lorawanMulticastDefinition.chirpstackGroupId, + data: this.hexBytesToBase64(dto.data), + }, + }; + + try { + return this.overwriteDownlink(csDto); + } catch (err) { + this.handlePossibleError(err, csDto); } - - private async addDevices( - multicastDto: CreateMulticastDto | UpdateMulticastDto, - chirpstackMulticastID: IdResponse // the id returned from chirpstack when the multicast is created in chirpstack. - ) { - multicastDto.iotDevices.forEach(async device => { - if (device.type === IoTDeviceType.LoRaWAN) { - let lorawanDevice: LoRaWANDevice = new LoRaWANDevice(); - lorawanDevice = device as LoRaWANDevice; // cast to LoRaWANDevice since it has DeviceEUI - const req = new AddDeviceToMulticastGroupRequest(); - req.setDevEui(lorawanDevice.deviceEUI); - req.setMulticastGroupId(chirpstackMulticastID.id); - - await this.addDeviceToMulticast( - this.multicastGroupUrl + "/" + chirpstackMulticastID.id + "/" + "devices", - this.multicastServiceClient, - req - ); - } - }); + } + async overwriteDownlink( + dto: CreateChirpstackMulticastQueueItemDto + ): Promise { + await this.deleteDownlinkQueue(dto.multicastQueueItem.multicastGroupID); + try { + const req = new EnqueueMulticastGroupQueueItemRequest(); + const queueItem = new MulticastGroupQueueItem(); + queueItem.setData(dto.multicastQueueItem.data); + queueItem.setMulticastGroupId(dto.multicastQueueItem.multicastGroupID); + queueItem.setFPort(dto.multicastQueueItem.fPort); + req.setQueueItem(queueItem); + + const res = await this.postDownlink(this.multicastServiceClient, req); + return res; + } catch (err) { + const fcntError = "enqueue downlink payload error: get next downlink fcnt for deveui error"; + if (err?.response?.data?.error?.startsWith(fcntError)) { + throw new BadRequestException(ErrorCodes.DeviceIsNotActivatedInChirpstack); + } + + throw err; } - - async addDeviceToMulticast( - logName: string, - client: MulticastGroupServiceClient, - request: AddDeviceToMulticastGroupRequest - ): Promise { - const metaData = this.makeMetadataHeader(); - const createPromise = new Promise((resolve, reject) => { - client.addDevice(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.logger.debug(`post:${logName} success`); - resolve(resp.toObject()); - } - }); - }); - try { - return await createPromise; - } catch (err) { - this.logger.error(`POST ${logName} got error: ${err}`); - throw new BadRequestException(); - } - } - - async getDownlinkQueue(multicastID: string): Promise { - const req = new ListMulticastGroupQueueRequest(); - req.setMulticastGroupId(multicastID); - const res = await this.getQueue(this.multicastServiceClient, req); - - const queueItems: MulticastQueueItem[] = res.getItemsList().map(queueItem => { - return { - multicastGroupId: queueItem.getMulticastGroupId(), - fCnt: queueItem.getFCnt(), - fPort: queueItem.getFPort(), - data: queueItem.getData_asB64(), - }; - }); - - const responseDto: MulticastDownlinkQueueResponseDto = { - deviceQueueItems: queueItems, - }; - return responseDto; - } - - public async createDownlink( - dto: CreateMulticastDownlinkDto, - multicast: Multicast - ): Promise { - const csDto: CreateChirpstackMulticastQueueItemDto = { - multicastQueueItem: { - fPort: dto.port, - multicastGroupID: multicast.lorawanMulticastDefinition.chirpstackGroupId, - data: this.hexBytesToBase64(dto.data), - }, - }; - - try { - return this.overwriteDownlink(csDto); - } catch (err) { - this.handlePossibleError(err, csDto); - } - } - async overwriteDownlink( - dto: CreateChirpstackMulticastQueueItemDto - ): Promise { - await this.deleteDownlinkQueue(dto.multicastQueueItem.multicastGroupID); - try { - const req = new EnqueueMulticastGroupQueueItemRequest(); - const queueItem = new MulticastGroupQueueItem(); - queueItem.setData(dto.multicastQueueItem.data); - queueItem.setMulticastGroupId(dto.multicastQueueItem.multicastGroupID); - queueItem.setFPort(dto.multicastQueueItem.fPort); - req.setQueueItem(queueItem); - - const res = await this.postDownlink(this.multicastServiceClient, req); - return res; - } catch (err) { - const fcntError = "enqueue downlink payload error: get next downlink fcnt for deveui error"; - if (err?.response?.data?.error?.startsWith(fcntError)) { - throw new BadRequestException(ErrorCodes.DeviceIsNotActivatedInChirpstack); - } - - throw err; - } + } + async deleteDownlinkQueue(multicastID: string): Promise { + const req = new FlushMulticastGroupQueueRequest(); + req.setMulticastGroupId(multicastID); + await this.flushQueue(this.multicastServiceClient, req); + } + + private hexBytesToBase64(hexBytes: string): string { + return Buffer.from(hexBytes, "hex").toString("base64"); + } + + private async createIfNotInChirpstack( + lorawanDevices: LoRaWANDevice[], + updateMulticastDto: UpdateMulticastDto, + mappedMulticast: Multicast + ): Promise { + if (await this.checkForDifferentAppID(lorawanDevices)) { + await this.createMulticastInChirpstack(updateMulticastDto, lorawanDevices, mappedMulticast); + } else { + throw new BadRequestException(ErrorCodes.InvalidPost); } - async deleteDownlinkQueue(multicastID: string): Promise { - const req = new FlushMulticastGroupQueueRequest(); - req.setMulticastGroupId(multicastID); - await this.flushQueue(this.multicastServiceClient, req); + } + + private async updateLogic( + existingMulticast: Multicast, + lorawanDevices: LoRaWANDevice[], + updateMulticastDto: UpdateMulticastDto + ): Promise { + const existingChirpStackMulticast = await this.getChirpstackMulticast( + existingMulticast.lorawanMulticastDefinition.chirpstackGroupId + ); + if (await this.checkForDifferentAppID(lorawanDevices)) { + if ( + await this.validateNewDevicesAppID( + // check if the new devices has the same service profile as the multicast. + existingChirpStackMulticast, + lorawanDevices + ) + ) { + await this.updateMulticastToChirpstack(updateMulticastDto, existingChirpStackMulticast, lorawanDevices); + } else { + throw new BadRequestException(ErrorCodes.InvalidPost); + } + } else { + throw new BadRequestException(ErrorCodes.DifferentServiceProfile); } - - private hexBytesToBase64(hexBytes: string): string { - return Buffer.from(hexBytes, "hex").toString("base64"); - } - - private async createIfNotInChirpstack( - lorawanDevices: LoRaWANDevice[], - updateMulticastDto: UpdateMulticastDto, - mappedMulticast: Multicast - ): Promise { - if (await this.checkForDifferentAppID(lorawanDevices)) { - await this.createMulticastInChirpstack(updateMulticastDto, lorawanDevices, mappedMulticast); + } + + async getQueue( + client: MulticastGroupServiceClient, + request: ListMulticastGroupQueueRequest + ): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + client.listQueue(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); } else { - throw new BadRequestException(ErrorCodes.InvalidPost); + this.logger.debug(`get from Queue success`); + resolve(resp); } + }); + }); + try { + return await getPromise; + } catch (err) { + throw new NotFoundException(); } - - private async updateLogic( - existingMulticast: Multicast, - lorawanDevices: LoRaWANDevice[], - updateMulticastDto: UpdateMulticastDto - ): Promise { - const existingChirpStackMulticast = await this.getChirpstackMulticast( - existingMulticast.lorawanMulticastDefinition.chirpstackGroupId - ); - if (await this.checkForDifferentAppID(lorawanDevices)) { - if ( - await this.validateNewDevicesAppID( - // check if the new devices has the same service profile as the multicast. - existingChirpStackMulticast, - lorawanDevices - ) - ) { - await this.updateMulticastToChirpstack(updateMulticastDto, existingChirpStackMulticast, lorawanDevices); - } else { - throw new BadRequestException(ErrorCodes.InvalidPost); - } + } + + async flushQueue(client: MulticastGroupServiceClient, request: FlushMulticastGroupQueueRequest): Promise { + const metaData = this.makeMetadataHeader(); + const getPromise = new Promise((resolve, reject) => { + client.flushQueue(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); } else { - throw new BadRequestException(ErrorCodes.DifferentServiceProfile); + this.logger.debug(`Delete queue success`); + resolve(resp); } + }); + }); + try { + return await getPromise; + } catch (err) { + this.logger.error(`DELETE queue got error: ${err}`); + throw new BadRequestException(); } - - async getQueue( - client: MulticastGroupServiceClient, - request: ListMulticastGroupQueueRequest - ): Promise { - const metaData = this.makeMetadataHeader(); - const getPromise = new Promise((resolve, reject) => { - client.listQueue(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.logger.debug(`get from Queue success`); - resolve(resp); - } - }); - }); - try { - return await getPromise; - } catch (err) { - throw new NotFoundException(); - } - } - - async flushQueue(client: MulticastGroupServiceClient, request: FlushMulticastGroupQueueRequest): Promise { - const metaData = this.makeMetadataHeader(); - const getPromise = new Promise((resolve, reject) => { - client.flushQueue(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.logger.debug(`Delete queue success`); - resolve(resp); - } - }); - }); - try { - return await getPromise; - } catch (err) { - this.logger.error(`DELETE queue got error: ${err}`); - throw new BadRequestException(); - } - } - async postDownlink( - client: MulticastGroupServiceClient, - request: EnqueueMulticastGroupQueueItemRequest - ): Promise { - const metaData = this.makeMetadataHeader(); - const createPromise = new Promise((resolve, reject) => { - client.enqueue(request, metaData, (err: ServiceError, resp: any) => { - if (err) { - reject(err); - } else { - this.logger.debug(`post downlink success`); - resolve(resp.toObject()); - } - }); - }); - try { - return await createPromise; - } catch (err) { - this.logger.error(`POST downlink got error: ${err}`); - throw new BadRequestException(); + } + async postDownlink( + client: MulticastGroupServiceClient, + request: EnqueueMulticastGroupQueueItemRequest + ): Promise { + const metaData = this.makeMetadataHeader(); + const createPromise = new Promise((resolve, reject) => { + client.enqueue(request, metaData, (err: ServiceError, resp: any) => { + if (err) { + reject(err); + } else { + this.logger.debug(`post downlink success`); + resolve(resp.toObject()); } + }); + }); + try { + return await createPromise; + } catch (err) { + this.logger.error(`POST downlink got error: ${err}`); + throw new BadRequestException(); } + } } diff --git a/src/services/csv-generator.service.ts b/src/services/csv-generator.service.ts index 65e67f6b..9138cb4e 100644 --- a/src/services/csv-generator.service.ts +++ b/src/services/csv-generator.service.ts @@ -4,97 +4,97 @@ import { EncryptionHelperService } from "@services/encryption-helper.service"; @Injectable() export class CsvGeneratorService { - constructor(private encryptionHelperService: EncryptionHelperService) {} - public generateDeviceMetadataCsv(devices: IoTDevice[]) { - const csvHeader = csvFields.join(","); - let csvString = csvHeader + "\n"; - devices.forEach(device => { - csvString += this.generateCsvRow(device) + "\n"; - }); - return csvString; - } + constructor(private encryptionHelperService: EncryptionHelperService) {} + public generateDeviceMetadataCsv(devices: IoTDevice[]) { + const csvHeader = csvFields.join(","); + let csvString = csvHeader + "\n"; + devices.forEach(device => { + csvString += this.generateCsvRow(device) + "\n"; + }); + return csvString; + } - // Having device as any required due to fields originating from different types - private generateCsvRow(device: any): string { - const { - name, - id, - type, - location, - commentOnLocation, - comment, - apiKey, - mqttURL, - mqttPort, - mqtttopicname, - authenticationType, - mqttusername, - caCertificate, - deviceCertificate, - lorawanSettings, - } = device; + // Having device as any required due to fields originating from different types + private generateCsvRow(device: any): string { + const { + name, + id, + type, + location, + commentOnLocation, + comment, + apiKey, + mqttURL, + mqttPort, + mqtttopicname, + authenticationType, + mqttusername, + caCertificate, + deviceCertificate, + lorawanSettings, + } = device; - const mqttpassword = this.encryptionHelperService.basicDecrypt(device.mqttpassword); - const deviceCertificateKey = this.encryptionHelperService.basicDecrypt(device.deviceCertificateKey); + const mqttpassword = this.encryptionHelperService.basicDecrypt(device.mqttpassword); + const deviceCertificateKey = this.encryptionHelperService.basicDecrypt(device.deviceCertificateKey); - let csvRow = - `${name},` + - `${id},` + - `${type},` + - `${location.coordinates[1] ?? ""},` + - `${location.coordinates[0] ?? ""},` + - `${commentOnLocation ?? ""},` + - `${comment ?? ""},` + - `${device.deviceModel?.id ?? ""},` + - `${apiKey ?? ""},` + - `${mqttURL ?? ""},` + - `${mqttPort ?? ""},` + - `${mqtttopicname ?? ""},` + - `${authenticationType ?? ""},` + - `${mqttusername ?? ""},` + - `${mqttpassword ?? ""},` + - `${this.base64Encode(caCertificate) ?? ""},` + - `${this.base64Encode(deviceCertificate) ?? ""},` + - `${this.base64Encode(deviceCertificateKey) ?? ""},` + - `${lorawanSettings?.devEUI ?? ""},` + - `${lorawanSettings?.deviceProfileID ?? ""},` + - `${lorawanSettings?.skipFCntCheck ?? ""},` + - `${lorawanSettings?.activationType ?? ""},` + - `${lorawanSettings?.OTAAapplicationKey ?? ""}`; + let csvRow = + `${name},` + + `${id},` + + `${type},` + + `${location.coordinates[1] ?? ""},` + + `${location.coordinates[0] ?? ""},` + + `${commentOnLocation ?? ""},` + + `${comment ?? ""},` + + `${device.deviceModel?.id ?? ""},` + + `${apiKey ?? ""},` + + `${mqttURL ?? ""},` + + `${mqttPort ?? ""},` + + `${mqtttopicname ?? ""},` + + `${authenticationType ?? ""},` + + `${mqttusername ?? ""},` + + `${mqttpassword ?? ""},` + + `${this.base64Encode(caCertificate) ?? ""},` + + `${this.base64Encode(deviceCertificate) ?? ""},` + + `${this.base64Encode(deviceCertificateKey) ?? ""},` + + `${lorawanSettings?.devEUI ?? ""},` + + `${lorawanSettings?.deviceProfileID ?? ""},` + + `${lorawanSettings?.skipFCntCheck ?? ""},` + + `${lorawanSettings?.activationType ?? ""},` + + `${lorawanSettings?.OTAAapplicationKey ?? ""}`; - return csvRow; - } + return csvRow; + } - private base64Encode(input: string) { - if (!input) { - return undefined; - } - return Buffer.from(input, "binary").toString("base64"); + private base64Encode(input: string) { + if (!input) { + return undefined; } + return Buffer.from(input, "binary").toString("base64"); + } } const csvFields = [ - "name", - "id", - "type", - "latitude", - "longitude", - "commentOnLocation", - "comment", - "deviceModelId", - "apiKey", - "mqttURL", - "mqttPort", - "mqtttopicname", - "authenticationType", - "mqttusername", - "mqttpassword", - "caCertificate", - "deviceCertificate", - "deviceCertificateKey", - "devEUI", - "deviceProfileID", - "skipFCntCheck", - "activationType", - "OTAAapplicationKey", + "name", + "id", + "type", + "latitude", + "longitude", + "commentOnLocation", + "comment", + "deviceModelId", + "apiKey", + "mqttURL", + "mqttPort", + "mqtttopicname", + "authenticationType", + "mqttusername", + "mqttpassword", + "caCertificate", + "deviceCertificate", + "deviceCertificateKey", + "devEUI", + "deviceProfileID", + "skipFCntCheck", + "activationType", + "OTAAapplicationKey", ]; diff --git a/src/services/data-management/chirpstack-mqtt-listener.service.ts b/src/services/data-management/chirpstack-mqtt-listener.service.ts index 8c88d98d..1461fb2d 100644 --- a/src/services/data-management/chirpstack-mqtt-listener.service.ts +++ b/src/services/data-management/chirpstack-mqtt-listener.service.ts @@ -1,7 +1,7 @@ import { MqttClientId } from "@config/constants/mqtt-constants"; import { - ChirpstackMQTTConnectionStateMessageDto, - ChirpstackMQTTMessageDto, + ChirpstackMQTTConnectionStateMessageDto, + ChirpstackMQTTMessageDto, } from "@dto/chirpstack/chirpstack-mqtt-message.dto"; import { ChirpstackMQTTConnectionStateMessage } from "@dto/chirpstack/state/chirpstack-mqtt-state-message.dto"; import { IoTDeviceType } from "@enum/device-type.enum"; @@ -16,109 +16,104 @@ import * as Protobuf from "protobufjs"; @Injectable() export class ChirpstackMQTTListenerService implements OnApplicationBootstrap { - constructor(private receiveDataService: ReceiveDataService, private iotDeviceService: IoTDeviceService) { - const connStateFullTemplate = Protobuf.loadSync(ChirpstackStateTemplatePath); - this.connStateType = connStateFullTemplate.lookupType("ConnState"); - } - - private readonly logger = new Logger(ChirpstackMQTTListenerService.name); - private readonly connStateType: Protobuf.Type; - - MQTT_URL = `mqtt://${process.env.CS_MQTT_HOSTNAME || "localhost"}:${process.env.CS_MQTT_PORT || "1883"}`; - client: Client; + constructor(private receiveDataService: ReceiveDataService, private iotDeviceService: IoTDeviceService) { + const connStateFullTemplate = Protobuf.loadSync(ChirpstackStateTemplatePath); + this.connStateType = connStateFullTemplate.lookupType("ConnState"); + } - private readonly CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX = "application/"; - private readonly CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC = - this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX + "+/device/+/event/up"; - private readonly CHIRPSTACK_MQTT_GATEWAY_PREFIX = "gateway/"; - private readonly CHIRPSTACK_MQTT_GATEWAY_TOPIC = this.CHIRPSTACK_MQTT_GATEWAY_PREFIX + "+/state/conn"; + private readonly logger = new Logger(ChirpstackMQTTListenerService.name); + private readonly connStateType: Protobuf.Type; - public async onApplicationBootstrap(): Promise { - this.logger.debug("Pre-init"); + MQTT_URL = `mqtt://${process.env.CS_MQTT_HOSTNAME || "localhost"}:${process.env.CS_MQTT_PORT || "1883"}`; + client: Client; - this.client = mqtt.connect(this.MQTT_URL, { - clean: true, - clientId: MqttClientId, - }); - this.client.on("connect", () => { - this.client.subscribe(this.CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC); - this.client.subscribe(this.CHIRPSTACK_MQTT_GATEWAY_TOPIC); + private readonly CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX = "application/"; + private readonly CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC = this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX + "+/device/+/event/up"; + private readonly CHIRPSTACK_MQTT_GATEWAY_PREFIX = "gateway/"; + private readonly CHIRPSTACK_MQTT_GATEWAY_TOPIC = this.CHIRPSTACK_MQTT_GATEWAY_PREFIX + "+/state/conn"; - this.client.on("message", async (topic, message) => { - this.logger.debug(`Received MQTT - Topic: '${topic}' - message: '${message}'`); + public async onApplicationBootstrap(): Promise { + this.logger.debug("Pre-init"); - if (topic.startsWith(this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX)) { - await this.receiveMqttMessage(message.toString()); - } else if (topic.startsWith(this.CHIRPSTACK_MQTT_GATEWAY_PREFIX)) { - try { - const decoded = this.connStateType.decode(message); - await this.receiveMqttGatewayStatusMessage(decoded.toJSON()); - } catch (error) { - this.logger.error(`Gateway status data could not be processed. Error: ${error}`); - } - } else { - this.logger.warn("Unrecognized MQTT topic " + topic); - } - }); - this.logger.debug("Connected to MQTT."); - }); - } + this.client = mqtt.connect(this.MQTT_URL, { + clean: true, + clientId: MqttClientId, + }); + this.client.on("connect", () => { + this.client.subscribe(this.CHIRPSTACK_MQTT_DEVICE_DATA_TOPIC); + this.client.subscribe(this.CHIRPSTACK_MQTT_GATEWAY_TOPIC); - async receiveMqttMessage(message: string): Promise { - const dto: ChirpstackMQTTMessageDto = JSON.parse(message); - const iotDevice = await this.iotDeviceService.findLoRaWANDeviceByDeviceEUI(dto.deviceInfo.devEui); + this.client.on("message", async (topic, message) => { + this.logger.debug(`Received MQTT - Topic: '${topic}' - message: '${message}'`); - if (!iotDevice) { - this.logger.warn( - `Chirpstack sent MQTT message for devEUI ${dto.deviceInfo.devEui}, but that's not registered in OS2IoT` - ); - return; + if (topic.startsWith(this.CHIRPSTACK_MQTT_DEVICE_DATA_PREFIX)) { + await this.receiveMqttMessage(message.toString()); + } else if (topic.startsWith(this.CHIRPSTACK_MQTT_GATEWAY_PREFIX)) { + try { + const decoded = this.connStateType.decode(message); + await this.receiveMqttGatewayStatusMessage(decoded.toJSON()); + } catch (error) { + this.logger.error(`Gateway status data could not be processed. Error: ${error}`); + } + } else { + this.logger.warn("Unrecognized MQTT topic " + topic); } + }); + this.logger.debug("Connected to MQTT."); + }); + } - await this.receiveDataService.sendRawIotDeviceRequestToKafka( - iotDevice, - message, - IoTDeviceType.LoRaWAN.toString() - ); + async receiveMqttMessage(message: string): Promise { + const dto: ChirpstackMQTTMessageDto = JSON.parse(message); + const iotDevice = await this.iotDeviceService.findLoRaWANDeviceByDeviceEUI(dto.deviceInfo.devEui); + + if (!iotDevice) { + this.logger.warn( + `Chirpstack sent MQTT message for devEUI ${dto.deviceInfo.devEui}, but that's not registered in OS2IoT` + ); + return; } - async receiveMqttGatewayStatusMessage(message: Record): Promise { - if ( - (hasProps(message, nameof("gatewayId")) || - hasProps(message, nameof("gatewayIdLegacy"))) && - (typeof message.gatewayId === "string" || typeof message.gatewayIdLegacy === "string") - ) { - const dto: ChirpstackMQTTConnectionStateMessageDto = { - gatewayId: message.gatewayId - ? message.gatewayId.toString() - : message.gatewayIdLegacy - ? Buffer.from(message.gatewayIdLegacy.toString(), "base64").toString("hex") - : undefined, - isOnline: message.state === "ONLINE", - }; + await this.receiveDataService.sendRawIotDeviceRequestToKafka(iotDevice, message, IoTDeviceType.LoRaWAN.toString()); + } - if (!dto.gatewayId) { - this.logger.error( - `Gateway status message is not properly formatted. Gateway id, if any, is ${ - message?.gatewayId - ? message?.gatewayId - : Buffer.from(message.gatewayIdLegacy as any, "base64").toString("hex") - }` - ); - return; - } + async receiveMqttGatewayStatusMessage(message: Record): Promise { + if ( + (hasProps(message, nameof("gatewayId")) || + hasProps(message, nameof("gatewayIdLegacy"))) && + (typeof message.gatewayId === "string" || typeof message.gatewayIdLegacy === "string") + ) { + const dto: ChirpstackMQTTConnectionStateMessageDto = { + gatewayId: message.gatewayId + ? message.gatewayId.toString() + : message.gatewayIdLegacy + ? Buffer.from(message.gatewayIdLegacy.toString(), "base64").toString("hex") + : undefined, + isOnline: message.state === "ONLINE", + }; - const jsonDto = JSON.stringify(dto); + if (!dto.gatewayId) { + this.logger.error( + `Gateway status message is not properly formatted. Gateway id, if any, is ${ + message?.gatewayId + ? message?.gatewayId + : Buffer.from(message.gatewayIdLegacy as any, "base64").toString("hex") + }` + ); + return; + } - await this.receiveDataService.sendRawGatewayStateToKafka(dto.gatewayId, jsonDto); - } else { - this.logger.error( - `Gateway status message is not properly formatted. Gateway id, if any, is ${ - message?.gatewayId - ? message?.gatewayId - : Buffer.from(message.gatewayIdLegacy as any, "base64").toString("hex") - }` - ); - } + const jsonDto = JSON.stringify(dto); + + await this.receiveDataService.sendRawGatewayStateToKafka(dto.gatewayId, jsonDto); + } else { + this.logger.error( + `Gateway status message is not properly formatted. Gateway id, if any, is ${ + message?.gatewayId + ? message?.gatewayId + : Buffer.from(message.gatewayIdLegacy as any, "base64").toString("hex") + }` + ); } + } } diff --git a/src/services/data-management/device-integration-persistence.service.ts b/src/services/data-management/device-integration-persistence.service.ts index fba9216c..004cd520 100644 --- a/src/services/data-management/device-integration-persistence.service.ts +++ b/src/services/data-management/device-integration-persistence.service.ts @@ -6,10 +6,7 @@ import { ReceivedMessage } from "@entities/received-message.entity"; import { IoTDeviceType } from "@enum/device-type.enum"; import { KafkaTopic } from "@enum/kafka-topic.enum"; import { subtractHours, subtractYears } from "@helpers/date.helper"; -import { - isValidLoRaWANPayload, - isValidSigFoxPayload, -} from "@helpers/message-payload.helper"; +import { isValidLoRaWANPayload, isValidSigFoxPayload } from "@helpers/message-payload.helper"; import { Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; @@ -20,331 +17,266 @@ import { LessThan, MoreThan, Repository } from "typeorm"; @Injectable() export class DeviceIntegrationPersistenceService extends AbstractKafkaConsumer { - constructor( - @InjectRepository(ReceivedMessage) - private receivedMessageRepository: Repository, - @InjectRepository(ReceivedMessageMetadata) - private receivedMessageMetadataRepository: Repository, - private ioTDeviceService: IoTDeviceService, - @InjectRepository(ReceivedMessageSigFoxSignals) - private receivedMessageSigFoxSignalsRepository: Repository - ) { - super(); + constructor( + @InjectRepository(ReceivedMessage) + private receivedMessageRepository: Repository, + @InjectRepository(ReceivedMessageMetadata) + private receivedMessageMetadataRepository: Repository, + private ioTDeviceService: IoTDeviceService, + @InjectRepository(ReceivedMessageSigFoxSignals) + private receivedMessageSigFoxSignalsRepository: Repository + ) { + super(); + } + + private readonly logger = new Logger(DeviceIntegrationPersistenceService.name); + private readonly defaultMetadataSavedCount = 20; + /** + * Limit how many messages can be stored within a time period. At the time, + * this limit is set conservatively. + * + * As SigFox is limited to 140 messages/day, this should be plenty + */ + private readonly maxSigFoxSignalsMessagesPerHour = 10; + + protected registerTopic(): void { + this.addTopic(KafkaTopic.RAW_REQUEST, "DeviceIntegrationPersistence"); + } + + // Listen to Kafka event + @CombinedSubscribeTo(KafkaTopic.RAW_REQUEST, "DeviceIntegrationPersistence") + async rawRequestListener(payload: KafkaPayload): Promise { + this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); + const dto = payload.body as RawIoTDeviceRequestDto; + let relatedIoTDevice; + try { + relatedIoTDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); + } catch (err) { + this.logger.error(`Could not find IoTDevice by ID: ${dto.iotDeviceId}`); + return; } - private readonly logger = new Logger(DeviceIntegrationPersistenceService.name); - private readonly defaultMetadataSavedCount = 20; - /** - * Limit how many messages can be stored within a time period. At the time, - * this limit is set conservatively. - * - * As SigFox is limited to 140 messages/day, this should be plenty - */ - private readonly maxSigFoxSignalsMessagesPerHour = 10; - - protected registerTopic(): void { - this.addTopic(KafkaTopic.RAW_REQUEST, "DeviceIntegrationPersistence"); - } - - // Listen to Kafka event - @CombinedSubscribeTo(KafkaTopic.RAW_REQUEST, "DeviceIntegrationPersistence") - async rawRequestListener(payload: KafkaPayload): Promise { - this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); - const dto = payload.body as RawIoTDeviceRequestDto; - let relatedIoTDevice; - try { - relatedIoTDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); - } catch (err) { - this.logger.error(`Could not find IoTDevice by ID: ${dto.iotDeviceId}`); - return; - } - - // Save latest message - const latestMessage = await this.saveLatestMessage(dto, relatedIoTDevice); + // Save latest message + const latestMessage = await this.saveLatestMessage(dto, relatedIoTDevice); - // Save last X messages worth of metadata - await this.saveMessageMetadata(dto, relatedIoTDevice); + // Save last X messages worth of metadata + await this.saveMessageMetadata(dto, relatedIoTDevice); - // In order to make statistics on SigFox data, we need to store it for long-term - if (relatedIoTDevice.type === IoTDeviceType.SigFox) { - await this.saveSigFoxStats(latestMessage); - await this.deleteSigFoxStatsSinceLastHour( - latestMessage.sentTime, - relatedIoTDevice.id - ); - await this.deleteOldSigFoxStats(relatedIoTDevice.id); - } + // In order to make statistics on SigFox data, we need to store it for long-term + if (relatedIoTDevice.type === IoTDeviceType.SigFox) { + await this.saveSigFoxStats(latestMessage); + await this.deleteSigFoxStatsSinceLastHour(latestMessage.sentTime, relatedIoTDevice.id); + await this.deleteOldSigFoxStats(relatedIoTDevice.id); } + } - private async saveLatestMessage( - dto: RawIoTDeviceRequestDto, - relatedIoTDevice: IoTDevice - ): Promise { - let existingMessage = await this.findExistingRecevedMessage(relatedIoTDevice); - - if (existingMessage) { - this.logger.debug( - `There was already a ReceivedMessage for device with id: ${dto.iotDeviceId}. Will be overwritten.` - ); - } else { - existingMessage = new ReceivedMessage(); - } + private async saveLatestMessage(dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice): Promise { + let existingMessage = await this.findExistingRecevedMessage(relatedIoTDevice); - const mappedMessage = this.mapDtoToReceivedMessage( - dto, - existingMessage, - relatedIoTDevice - ); - await this.receivedMessageRepository.save(mappedMessage); - this.logger.debug( - "Saved ReceivedMessage for device with id: " + mappedMessage.device.id - ); - - return mappedMessage; + if (existingMessage) { + this.logger.debug( + `There was already a ReceivedMessage for device with id: ${dto.iotDeviceId}. Will be overwritten.` + ); + } else { + existingMessage = new ReceivedMessage(); } - private async findExistingRecevedMessage( - relatedIoTDevice: IoTDevice - ): Promise { - // Use QueryBuilder since the relation only exists from IoT-Device to ReceivedMessage - return await this.receivedMessageRepository - .createQueryBuilder("msg") - .leftJoinAndSelect("msg.device", "iot_device") - .where("iot_device.id = :id", { id: relatedIoTDevice.id }) - .getOne(); + const mappedMessage = this.mapDtoToReceivedMessage(dto, existingMessage, relatedIoTDevice); + await this.receivedMessageRepository.save(mappedMessage); + this.logger.debug("Saved ReceivedMessage for device with id: " + mappedMessage.device.id); + + return mappedMessage; + } + + private async findExistingRecevedMessage(relatedIoTDevice: IoTDevice): Promise { + // Use QueryBuilder since the relation only exists from IoT-Device to ReceivedMessage + return await this.receivedMessageRepository + .createQueryBuilder("msg") + .leftJoinAndSelect("msg.device", "iot_device") + .where("iot_device.id = :id", { id: relatedIoTDevice.id }) + .getOne(); + } + + mapDtoToReceivedMessage( + dto: RawIoTDeviceRequestDto, + existingMessage: ReceivedMessage, + relatedIoTDevice: IoTDevice + ): ReceivedMessage { + existingMessage.device = relatedIoTDevice; + existingMessage.rawData = dto.rawPayload; + existingMessage.sentTime = dto.unixTimestamp ? new Date(dto.unixTimestamp) : new Date(); + + this.mapRxInfoToReceivedMessage(dto.rawPayload, existingMessage, relatedIoTDevice.type); + + return existingMessage; + } + + private mapRxInfoToReceivedMessage(rawPayload: JSON, message: ReceivedMessage, type: IoTDeviceType) { + if (!rawPayload) { + return; } - mapDtoToReceivedMessage( - dto: RawIoTDeviceRequestDto, - existingMessage: ReceivedMessage, - relatedIoTDevice: IoTDevice - ): ReceivedMessage { - existingMessage.device = relatedIoTDevice; - existingMessage.rawData = dto.rawPayload; - existingMessage.sentTime = dto.unixTimestamp - ? new Date(dto.unixTimestamp) - : new Date(); - - this.mapRxInfoToReceivedMessage( - dto.rawPayload, - existingMessage, - relatedIoTDevice.type - ); - - return existingMessage; + // JSON is the same as a Record which is easier to work with + const rawPayloadRecord = rawPayload as unknown as Record; + + try { + switch (type) { + case IoTDeviceType.LoRaWAN: + this.mapLoRaWANInfoToReceivedMessage(rawPayloadRecord, message); + break; + case IoTDeviceType.SigFox: + this.mapSigFoxInfoToReceivedMessage(rawPayloadRecord, message); + break; + case IoTDeviceType.GenericHttp: + break; + default: + this.logger.debug("Unable to determine raw payload type of ReceivedMessage"); + break; + } + } catch (error) { + // Continue. Failing to pre-fill device-specifc fields doesn't matter } - - private mapRxInfoToReceivedMessage( - rawPayload: JSON, - message: ReceivedMessage, - type: IoTDeviceType - ) { - if (!rawPayload) { - return; - } - - // JSON is the same as a Record which is easier to work with - const rawPayloadRecord = (rawPayload as unknown) as Record; - - try { - switch (type) { - case IoTDeviceType.LoRaWAN: - this.mapLoRaWANInfoToReceivedMessage(rawPayloadRecord, message); - break; - case IoTDeviceType.SigFox: - this.mapSigFoxInfoToReceivedMessage(rawPayloadRecord, message); - break; - case IoTDeviceType.GenericHttp: - break; - default: - this.logger.debug( - "Unable to determine raw payload type of ReceivedMessage" - ); - break; - } - } catch (error) { - // Continue. Failing to pre-fill device-specifc fields doesn't matter - } + } + + private mapLoRaWANInfoToReceivedMessage(payload: Record, message: ReceivedMessage) { + if (isValidLoRaWANPayload(payload)) { + // There's signal info for each nearby gateway. Retrieve the strongest signal strength + const rssi = Math.max(...payload.rxInfo.map(info => info.rssi)); + const snr = Math.max(...payload.rxInfo.map(info => info.snr)); + message.rssi = Number.isInteger(rssi) ? rssi : message.rssi; + message.snr = Number.isInteger(snr) ? snr : message.snr; + } else { + this.logger.debug("The received LoRaWAN message is either not valid or incomplete"); } - - private mapLoRaWANInfoToReceivedMessage( - payload: Record, - message: ReceivedMessage - ) { - if (isValidLoRaWANPayload(payload)) { - // There's signal info for each nearby gateway. Retrieve the strongest signal strength - const rssi = Math.max(...payload.rxInfo.map(info => info.rssi)); - const snr = Math.max(...payload.rxInfo.map(info => info.snr)); - message.rssi = Number.isInteger(rssi) ? rssi : message.rssi; - message.snr = Number.isInteger(snr) ? snr : message.snr; - } else { - this.logger.debug( - "The received LoRaWAN message is either not valid or incomplete" - ); - } + } + + /** + * Same principle as {@link mapLoRaWANInfoToReceivedMessage} + */ + private mapSigFoxInfoToReceivedMessage(payload: Record, message: ReceivedMessage) { + if (isValidSigFoxPayload(payload)) { + const rssi = Math.max(...payload.duplicates.map(info => info.rssi)); + const snr = Math.max(...payload.duplicates.map(info => info.snr)); + message.rssi = Number.isInteger(rssi) ? rssi : message.rssi; + message.snr = Number.isInteger(snr) ? snr : message.snr; + } else { + this.logger.debug("The received Sigfox message is either not valid or incomplete"); } - - /** - * Same principle as {@link mapLoRaWANInfoToReceivedMessage} - */ - private mapSigFoxInfoToReceivedMessage( - payload: Record, - message: ReceivedMessage - ) { - if (isValidSigFoxPayload(payload)) { - const rssi = Math.max(...payload.duplicates.map(info => info.rssi)); - const snr = Math.max(...payload.duplicates.map(info => info.snr)); - message.rssi = Number.isInteger(rssi) ? rssi : message.rssi; - message.snr = Number.isInteger(snr) ? snr : message.snr; - } else { - this.logger.debug( - "The received Sigfox message is either not valid or incomplete" - ); - } + } + + private async saveMessageMetadata(dto: RawIoTDeviceRequestDto, relatedIoTDevice: IoTDevice): Promise { + // Save this + const mappedMetadata = this.mapDtoToNewReceivedMessageMetadata(dto, relatedIoTDevice); + const savedMetadata = await this.receivedMessageMetadataRepository.save(mappedMetadata); + this.logger.debug( + `Saved ReceivedMessageMetadata for device: ${savedMetadata.device.id}. ${JSON.stringify(savedMetadata)}` + ); + + await this.deleteOldMetadata(relatedIoTDevice); + } + + private async deleteOldMetadata(relatedIoTDevice: IoTDevice): Promise { + const countToKeep: number = +process.env.METADATA_SAVED_COUNT || this.defaultMetadataSavedCount; + // Find the oldest item to be kept. + const newestToDelete = await this.receivedMessageMetadataRepository.find({ + where: { device: { id: relatedIoTDevice.id } }, + skip: countToKeep, + order: { + sentTime: "DESC", + }, + }); + + if (newestToDelete.length == 0) { + this.logger.debug("We don't need to delete any metadata"); + return; } - private async saveMessageMetadata( - dto: RawIoTDeviceRequestDto, - relatedIoTDevice: IoTDevice - ): Promise { - // Save this - const mappedMetadata = this.mapDtoToNewReceivedMessageMetadata( - dto, - relatedIoTDevice - ); - const savedMetadata = await this.receivedMessageMetadataRepository.save( - mappedMetadata - ); - this.logger.debug( - `Saved ReceivedMessageMetadata for device: ${ - savedMetadata.device.id - }. ${JSON.stringify(savedMetadata)}` - ); - - await this.deleteOldMetadata(relatedIoTDevice); - } - - private async deleteOldMetadata(relatedIoTDevice: IoTDevice): Promise { - const countToKeep: number = - +process.env.METADATA_SAVED_COUNT || this.defaultMetadataSavedCount; - // Find the oldest item to be kept. - const newestToDelete = await this.receivedMessageMetadataRepository.find({ - where: { device: { id: relatedIoTDevice.id } }, - skip: countToKeep, - order: { - sentTime: "DESC", - }, - }); - - if (newestToDelete.length == 0) { - this.logger.debug("We don't need to delete any metadata"); - return; + // Delete all older than X + await this.doDeleteOldMetadata(relatedIoTDevice, newestToDelete); + } + + private async doDeleteOldMetadata(relatedIoTDevice: IoTDevice, newestToDelete: ReceivedMessageMetadata[]) { + const result = await this.receivedMessageMetadataRepository + .createQueryBuilder("received_message_metadata") + .delete() + .from(ReceivedMessageMetadata) + .where( + 'received_message_metadata."deviceId" = :deviceId and received_message_metadata."sentTime" <= :earliestToKeep', + { + deviceId: relatedIoTDevice.id, + earliestToKeep: newestToDelete[0].sentTime, } - - // Delete all older than X - await this.doDeleteOldMetadata(relatedIoTDevice, newestToDelete); - } - - private async doDeleteOldMetadata( - relatedIoTDevice: IoTDevice, - newestToDelete: ReceivedMessageMetadata[] - ) { - const result = await this.receivedMessageMetadataRepository - .createQueryBuilder("received_message_metadata") - .delete() - .from(ReceivedMessageMetadata) - .where( - 'received_message_metadata."deviceId" = :deviceId and received_message_metadata."sentTime" <= :earliestToKeep', - { - deviceId: relatedIoTDevice.id, - earliestToKeep: newestToDelete[0].sentTime, - } - ) - .execute(); - this.logger.debug( - `Deleted: ${result.affected} rows from received_message_metadata` - ); - } - - mapDtoToNewReceivedMessageMetadata( - dto: RawIoTDeviceRequestDto, - relatedIoTDevice: IoTDevice - ): ReceivedMessageMetadata { - const newMetadata = new ReceivedMessageMetadata(); - newMetadata.device = relatedIoTDevice; - newMetadata.sentTime = dto.unixTimestamp - ? new Date(dto.unixTimestamp) - : new Date(); - - return newMetadata; - } - - private async saveSigFoxStats(dto: ReceivedMessage): Promise { - const sigFoxMessage: ReceivedMessageSigFoxSignals = { ...dto, id: undefined }; - await this.receivedMessageSigFoxSignalsRepository.insert(sigFoxMessage); + ) + .execute(); + this.logger.debug(`Deleted: ${result.affected} rows from received_message_metadata`); + } + + mapDtoToNewReceivedMessageMetadata( + dto: RawIoTDeviceRequestDto, + relatedIoTDevice: IoTDevice + ): ReceivedMessageMetadata { + const newMetadata = new ReceivedMessageMetadata(); + newMetadata.device = relatedIoTDevice; + newMetadata.sentTime = dto.unixTimestamp ? new Date(dto.unixTimestamp) : new Date(); + + return newMetadata; + } + + private async saveSigFoxStats(dto: ReceivedMessage): Promise { + const sigFoxMessage: ReceivedMessageSigFoxSignals = { ...dto, id: undefined }; + await this.receivedMessageSigFoxSignalsRepository.insert(sigFoxMessage); + } + + /** + * Make sure we never have stats for more than X messages per device per hour + * to avoid filling the database + * @param latestMessageTime + * @param deviceId + */ + private async deleteSigFoxStatsSinceLastHour(latestMessageTime: Date, deviceId: number): Promise { + const lastHour = subtractHours(latestMessageTime); + // Find the oldest items since the last hour + const oldestToDelete = await this.receivedMessageSigFoxSignalsRepository.find({ + where: { device: { id: deviceId }, sentTime: MoreThan(lastHour) }, + skip: this.maxSigFoxSignalsMessagesPerHour, + order: { + sentTime: "DESC", + }, + }); + + if (oldestToDelete.length === 0) { + this.logger.debug( + `Less than ${this.maxSigFoxSignalsMessagesPerHour} SigFox stat objects for device ${deviceId} found in database. Deleting no rows.` + ); + return; } - /** - * Make sure we never have stats for more than X messages per device per hour - * to avoid filling the database - * @param latestMessageTime - * @param deviceId - */ - private async deleteSigFoxStatsSinceLastHour( - latestMessageTime: Date, - deviceId: number - ): Promise { - const lastHour = subtractHours(latestMessageTime); - // Find the oldest items since the last hour - const oldestToDelete = await this.receivedMessageSigFoxSignalsRepository.find({ - where: { device: { id: deviceId }, sentTime: MoreThan(lastHour) }, - skip: this.maxSigFoxSignalsMessagesPerHour, - order: { - sentTime: "DESC", - }, - }); - - if (oldestToDelete.length === 0) { - this.logger.debug( - `Less than ${this.maxSigFoxSignalsMessagesPerHour} SigFox stat objects for device ${deviceId} found in database. Deleting no rows.` - ); - return; - } - - const result = await this.receivedMessageSigFoxSignalsRepository.delete( - oldestToDelete.map(old => old.id) - ); - - this.logger.debug( - `Deleted: ${result.affected} rows from received_message_sigfox_signals` - ); + const result = await this.receivedMessageSigFoxSignalsRepository.delete(oldestToDelete.map(old => old.id)); + + this.logger.debug(`Deleted: ${result.affected} rows from received_message_sigfox_signals`); + } + + /** + * Clean up SigFox stats for the device if they are older than 1 year + * @param deviceId + */ + private async deleteOldSigFoxStats(deviceId: number): Promise { + const lastYear = subtractYears(new Date()); + // Find messages older than a date and delete them + const oldestToDelete = await this.receivedMessageSigFoxSignalsRepository.find({ + where: [ + { device: { id: deviceId }, sentTime: LessThan(lastYear) }, + { device: { id: deviceId }, updatedAt: LessThan(lastYear) }, + ], + }); + + if (oldestToDelete.length === 0) { + this.logger.debug("There's no old SigFox signal messages"); + return; } - /** - * Clean up SigFox stats for the device if they are older than 1 year - * @param deviceId - */ - private async deleteOldSigFoxStats(deviceId: number): Promise { - const lastYear = subtractYears(new Date()); - // Find messages older than a date and delete them - const oldestToDelete = await this.receivedMessageSigFoxSignalsRepository.find({ - where: [ - { device: { id: deviceId }, sentTime: LessThan(lastYear) }, - { device: { id: deviceId }, updatedAt: LessThan(lastYear) }, - ], - }); - - if (oldestToDelete.length === 0) { - this.logger.debug("There's no old SigFox signal messages"); - return; - } - - const result = await this.receivedMessageSigFoxSignalsRepository.delete( - oldestToDelete.map(old => old.id) - ); + const result = await this.receivedMessageSigFoxSignalsRepository.delete(oldestToDelete.map(old => old.id)); - this.logger.debug( - `Deleted: ${result.affected} rows from received_message_sigfox_signals` - ); - } + this.logger.debug(`Deleted: ${result.affected} rows from received_message_sigfox_signals`); + } } diff --git a/src/services/data-management/gateway-persistence.service.ts b/src/services/data-management/gateway-persistence.service.ts index d862db5e..e27844b4 100644 --- a/src/services/data-management/gateway-persistence.service.ts +++ b/src/services/data-management/gateway-persistence.service.ts @@ -12,98 +12,98 @@ import { LessThan, MoreThan, Repository } from "typeorm"; @Injectable() export class GatewayPersistenceService extends AbstractKafkaConsumer { - private readonly gatewayStatusSavedDays = 30; - /** - * Limit how many messages can be stored for each hour - */ - private readonly maxStatusMessagesPerHour = 10; - - constructor( - @InjectRepository(GatewayStatusHistory) - private gatewayStatusHistoryRepository: Repository - ) { - super(); + private readonly gatewayStatusSavedDays = 30; + /** + * Limit how many messages can be stored for each hour + */ + private readonly maxStatusMessagesPerHour = 10; + + constructor( + @InjectRepository(GatewayStatusHistory) + private gatewayStatusHistoryRepository: Repository + ) { + super(); + } + + private readonly logger = new Logger(GatewayPersistenceService.name); + + protected registerTopic(): void { + this.addTopic(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence"); + } + + // Listen to Kafka event + @CombinedSubscribeTo(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence") + async rawRequestListener(payload: KafkaPayload): Promise { + this.logger.debug(`RAW_GATEWAY_STATE: '${JSON.stringify(payload)}'`); + const dto = payload.body as RawGatewayStateDto; + const messageState = dto.rawPayload as unknown as ChirpstackMQTTConnectionStateMessageDto; + + const statusHistory = this.mapDtoToEntity(dto, messageState); + await this.gatewayStatusHistoryRepository.save(statusHistory); + + // Clean up old statuses + await this.deleteStatusHistoriesSinceLastHour(statusHistory.timestamp, dto.gatewayId); + await this.deleteOldStatusHistories(dto.gatewayId); + } + + private mapDtoToEntity(dto: RawGatewayStateDto, messageState: ChirpstackMQTTConnectionStateMessageDto) { + const statusHistory = new GatewayStatusHistory(); + statusHistory.mac = dto.gatewayId; + statusHistory.timestamp = dto.unixTimestamp ? new Date(dto.unixTimestamp) : new Date(); + statusHistory.wasOnline = !!messageState?.isOnline; + return statusHistory; + } + + /** + * Make sure we never have histories for more than X messages per gateway per hour + * to avoid filling the database + * @param latestMessageTime + * @param gatewayId + */ + private async deleteStatusHistoriesSinceLastHour(latestMessageTime: Date, gatewayId: string): Promise { + const lastHour = subtractHours(latestMessageTime); + // Find the oldest items since the last hour + const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ + where: { mac: gatewayId, timestamp: MoreThan(lastHour) }, + skip: this.maxStatusMessagesPerHour, + order: { + timestamp: "DESC", + }, + }); + + if (oldestToDelete.length === 0) { + this.logger.debug( + `Less than ${this.maxStatusMessagesPerHour} gateway status' for gateway ${gatewayId} found in database. Deleting no rows.` + ); + return; } - private readonly logger = new Logger(GatewayPersistenceService.name); - - protected registerTopic(): void { - this.addTopic(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence"); + const result = await this.gatewayStatusHistoryRepository.delete(oldestToDelete.map(old => old.id)); + + this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); + } + + /** + * Clean up data if it's older than a specified time period + * @param deviceId + */ + private async deleteOldStatusHistories(gatewayId: string): Promise { + const minDate = subtractDays(new Date(), this.gatewayStatusSavedDays); + // Find messages older than a date and delete them + const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ + where: [ + { mac: gatewayId, timestamp: LessThan(minDate) }, + { mac: gatewayId, updatedAt: LessThan(minDate) }, + ], + }); + + if (oldestToDelete.length === 0) { + this.logger.debug("There's no old gateway status messages"); + return; } - // Listen to Kafka event - @CombinedSubscribeTo(KafkaTopic.RAW_GATEWAY_STATE, "GatewayPersistence") - async rawRequestListener(payload: KafkaPayload): Promise { - this.logger.debug(`RAW_GATEWAY_STATE: '${JSON.stringify(payload)}'`); - const dto = payload.body as RawGatewayStateDto; - const messageState = dto.rawPayload as unknown as ChirpstackMQTTConnectionStateMessageDto; - - const statusHistory = this.mapDtoToEntity(dto, messageState); - await this.gatewayStatusHistoryRepository.save(statusHistory); + const result = await this.gatewayStatusHistoryRepository.delete(oldestToDelete.map(old => old.id)); - // Clean up old statuses - await this.deleteStatusHistoriesSinceLastHour(statusHistory.timestamp, dto.gatewayId); - await this.deleteOldStatusHistories(dto.gatewayId); - } - - private mapDtoToEntity(dto: RawGatewayStateDto, messageState: ChirpstackMQTTConnectionStateMessageDto) { - const statusHistory = new GatewayStatusHistory(); - statusHistory.mac = dto.gatewayId; - statusHistory.timestamp = dto.unixTimestamp ? new Date(dto.unixTimestamp) : new Date(); - statusHistory.wasOnline = !!messageState?.isOnline; - return statusHistory; - } - - /** - * Make sure we never have histories for more than X messages per gateway per hour - * to avoid filling the database - * @param latestMessageTime - * @param gatewayId - */ - private async deleteStatusHistoriesSinceLastHour(latestMessageTime: Date, gatewayId: string): Promise { - const lastHour = subtractHours(latestMessageTime); - // Find the oldest items since the last hour - const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ - where: { mac: gatewayId, timestamp: MoreThan(lastHour) }, - skip: this.maxStatusMessagesPerHour, - order: { - timestamp: "DESC", - }, - }); - - if (oldestToDelete.length === 0) { - this.logger.debug( - `Less than ${this.maxStatusMessagesPerHour} gateway status' for gateway ${gatewayId} found in database. Deleting no rows.` - ); - return; - } - - const result = await this.gatewayStatusHistoryRepository.delete(oldestToDelete.map(old => old.id)); - - this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); - } - - /** - * Clean up data if it's older than a specified time period - * @param deviceId - */ - private async deleteOldStatusHistories(gatewayId: string): Promise { - const minDate = subtractDays(new Date(), this.gatewayStatusSavedDays); - // Find messages older than a date and delete them - const oldestToDelete = await this.gatewayStatusHistoryRepository.find({ - where: [ - { mac: gatewayId, timestamp: LessThan(minDate) }, - { mac: gatewayId, updatedAt: LessThan(minDate) }, - ], - }); - - if (oldestToDelete.length === 0) { - this.logger.debug("There's no old gateway status messages"); - return; - } - - const result = await this.gatewayStatusHistoryRepository.delete(oldestToDelete.map(old => old.id)); - - this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); - } + this.logger.debug(`Deleted: ${result.affected} rows from gateway_status_history`); + } } diff --git a/src/services/data-management/internal-mqtt-broker-listener.service.ts b/src/services/data-management/internal-mqtt-broker-listener.service.ts index 5a50c1a5..18983a06 100644 --- a/src/services/data-management/internal-mqtt-broker-listener.service.ts +++ b/src/services/data-management/internal-mqtt-broker-listener.service.ts @@ -14,105 +14,97 @@ import { caCertPath } from "@resources/resource-paths"; @Injectable() export class InternalMqttBrokerListenerService implements OnApplicationBootstrap { - constructor( - private receiveDataService: ReceiveDataService, - private iotDeviceService: IoTDeviceService, - private mqttService: MqttService, - @InjectRepository(MQTTInternalBrokerDevice) - private mqttInternalBrokerDeviceRepository: Repository - ) {} + constructor( + private receiveDataService: ReceiveDataService, + private iotDeviceService: IoTDeviceService, + private mqttService: MqttService, + @InjectRepository(MQTTInternalBrokerDevice) + private mqttInternalBrokerDeviceRepository: Repository + ) {} - private superUserName = "SuperUser"; - private superuserPassword = process.env.MQTT_SUPER_USER_PASSWORD || "SuperUser"; - private MQTT_URL = `mqtts://${process.env.MQTT_BROKER_HOSTNAME || "localhost"}`; - private MQTT_PASSWORD_PORT = "8885"; - passwordClient: Client; - private readonly logger = new Logger(InternalMqttBrokerListenerService.name); + private superUserName = "SuperUser"; + private superuserPassword = process.env.MQTT_SUPER_USER_PASSWORD || "SuperUser"; + private MQTT_URL = `mqtts://${process.env.MQTT_BROKER_HOSTNAME || "localhost"}`; + private MQTT_PASSWORD_PORT = "8885"; + passwordClient: Client; + private readonly logger = new Logger(InternalMqttBrokerListenerService.name); - private readonly MQTT_DEVICE_DATA_PREFIX = "devices/"; - private readonly MQTT_DEVICE_DATA_TOPIC = this.MQTT_DEVICE_DATA_PREFIX + "#"; + private readonly MQTT_DEVICE_DATA_PREFIX = "devices/"; + private readonly MQTT_DEVICE_DATA_TOPIC = this.MQTT_DEVICE_DATA_PREFIX + "#"; - public async onApplicationBootstrap(): Promise { - await this.seedSuperUser(); - const superUser = await this.iotDeviceService.getMqttSuperUser(); - const caCert = fs.readFileSync(caCertPath); - this.passwordClient = connect(this.MQTT_URL, { - clean: true, - port: Number(this.MQTT_PASSWORD_PORT), - clientId: "PasswordSuperUserClient", - username: superUser.name, - password: this.superuserPassword, - ca: caCert, - }); + public async onApplicationBootstrap(): Promise { + await this.seedSuperUser(); + const superUser = await this.iotDeviceService.getMqttSuperUser(); + const caCert = fs.readFileSync(caCertPath); + this.passwordClient = connect(this.MQTT_URL, { + clean: true, + port: Number(this.MQTT_PASSWORD_PORT), + clientId: "PasswordSuperUserClient", + username: superUser.name, + password: this.superuserPassword, + ca: caCert, + }); - this.configureClient(this.passwordClient); - } - - private configureClient(client: Client) { - client.on("connect", () => { - client.subscribe(this.MQTT_DEVICE_DATA_TOPIC); + this.configureClient(this.passwordClient); + } - client.on("message", async (topic, message) => { - this.logger.debug( - `Received MQTT - Topic: '${topic}' - message: '${message}'` - ); + private configureClient(client: Client) { + client.on("connect", () => { + client.subscribe(this.MQTT_DEVICE_DATA_TOPIC); - if (topic.startsWith(this.MQTT_DEVICE_DATA_PREFIX)) { - // Handle data coming in - await this.handleMessage(message.toString(), topic); - } else { - this.logger.warn("Unknown MQTT Topic " + topic); - } - }); + client.on("message", async (topic, message) => { + this.logger.debug(`Received MQTT - Topic: '${topic}' - message: '${message}'`); - this.logger.debug("Connected to MQTT Internal"); - }); - } + if (topic.startsWith(this.MQTT_DEVICE_DATA_PREFIX)) { + // Handle data coming in + await this.handleMessage(message.toString(), topic); + } else { + this.logger.warn("Unknown MQTT Topic " + topic); + } + }); - private async handleMessage(message: string, topic: string) { - const topicPath = topic.split("/"); - const deviceId = Number(topicPath[topicPath.length - 1]); - const iotDevice = await this.iotDeviceService.findMQTTDevice(deviceId); + this.logger.debug("Connected to MQTT Internal"); + }); + } - if (iotDevice === undefined) { - this.logger.warn( - `Unknown DeviceId attempted to send data via MQTT DeviceId: ${deviceId}` - ); - return; - } + private async handleMessage(message: string, topic: string) { + const topicPath = topic.split("/"); + const deviceId = Number(topicPath[topicPath.length - 1]); + const iotDevice = await this.iotDeviceService.findMQTTDevice(deviceId); - await this.receiveDataService.sendRawIotDeviceRequestToKafka( - iotDevice, - message, - IoTDeviceType.MQTTInternalBroker.toString() - ); + if (iotDevice === undefined) { + this.logger.warn(`Unknown DeviceId attempted to send data via MQTT DeviceId: ${deviceId}`); + return; } - private async seedSuperUser() { - if (await this.iotDeviceService.getMqttSuperUser()) { - this.logger.debug( - "MQTT Listener superuser already exists. New one wont be seeded" - ); - return; - } - - const certificateDetails = await this.mqttService.generateCertificate( - this.superUserName - ); + await this.receiveDataService.sendRawIotDeviceRequestToKafka( + iotDevice, + message, + IoTDeviceType.MQTTInternalBroker.toString() + ); + } - await this.mqttInternalBrokerDeviceRepository.save({ - type: IoTDeviceType.MQTTInternalBroker, - applicationId: null, - latitude: 0, - longitude: 0, - name: this.superUserName, - authenticationType: AuthenticationType.PASSWORD, - mqttpasswordhash: this.mqttService.hashPassword(this.superuserPassword), - mqttusername: this.superUserName, - permissions: MQTTPermissionLevel.superUser, - deviceCertificate: certificateDetails.deviceCertificate, - deviceCertificateKey: certificateDetails.deviceCertificateKey, - }); - this.logger.log("Created MQTT Listener SuperUser"); + private async seedSuperUser() { + if (await this.iotDeviceService.getMqttSuperUser()) { + this.logger.debug("MQTT Listener superuser already exists. New one wont be seeded"); + return; } + + const certificateDetails = await this.mqttService.generateCertificate(this.superUserName); + + await this.mqttInternalBrokerDeviceRepository.save({ + type: IoTDeviceType.MQTTInternalBroker, + applicationId: null, + latitude: 0, + longitude: 0, + name: this.superUserName, + authenticationType: AuthenticationType.PASSWORD, + mqttpasswordhash: this.mqttService.hashPassword(this.superuserPassword), + mqttusername: this.superUserName, + permissions: MQTTPermissionLevel.superUser, + deviceCertificate: certificateDetails.deviceCertificate, + deviceCertificateKey: certificateDetails.deviceCertificateKey, + }); + this.logger.log("Created MQTT Listener SuperUser"); + } } diff --git a/src/services/data-management/internal-mqtt-client-listener.service.ts b/src/services/data-management/internal-mqtt-client-listener.service.ts index 0372284d..86bc084e 100644 --- a/src/services/data-management/internal-mqtt-client-listener.service.ts +++ b/src/services/data-management/internal-mqtt-client-listener.service.ts @@ -1,10 +1,4 @@ -import { - forwardRef, - Inject, - Injectable, - Logger, - OnApplicationBootstrap, -} from "@nestjs/common"; +import { forwardRef, Inject, Injectable, Logger, OnApplicationBootstrap } from "@nestjs/common"; import { ReceiveDataService } from "@services/data-management/receive-data.service"; import { Client, connect } from "mqtt"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; @@ -15,83 +9,75 @@ import { EncryptionHelperService } from "@services/encryption-helper.service"; @Injectable() export class InternalMqttClientListenerService implements OnApplicationBootstrap { - constructor( - private receiveDataService: ReceiveDataService, - @Inject(forwardRef(() => IoTDeviceService)) - private iotDeviceService: IoTDeviceService, - private encryptionHelperService: EncryptionHelperService - ) { - this.clientDictionary = new Map(); - } + constructor( + private receiveDataService: ReceiveDataService, + @Inject(forwardRef(() => IoTDeviceService)) + private iotDeviceService: IoTDeviceService, + private encryptionHelperService: EncryptionHelperService + ) { + this.clientDictionary = new Map(); + } - clientDictionary: Map; - private readonly logger = new Logger(InternalMqttClientListenerService.name); + clientDictionary: Map; + private readonly logger = new Logger(InternalMqttClientListenerService.name); - public async onApplicationBootstrap(): Promise { - // Get all subscriber devices - const mqttSubscribers = await this.iotDeviceService.getAllValidMQTTExternalBrokers(); + public async onApplicationBootstrap(): Promise { + // Get all subscriber devices + const mqttSubscribers = await this.iotDeviceService.getAllValidMQTTExternalBrokers(); - // Create clients for each device - this.createMQTTClients( - mqttSubscribers.filter(d => !this.clientDictionary.has(d.id)) - ); - } + // Create clients for each device + this.createMQTTClients(mqttSubscribers.filter(d => !this.clientDictionary.has(d.id))); + } - public createMQTTClients(mqttSubscribers: MQTTExternalBrokerDevice[]) { - mqttSubscribers.forEach(async d => { - // Cannot create clients without a topic - if (!d.mqtttopicname) { - this.logger.error( - `Something went wrong while connecting device ${d.name} removed client` - ); - await this.iotDeviceService.markMqttExternalBrokerAsInvalid(d); - return; - } - const client = connect(d.mqttURL, { - clean: true, - port: d.mqttPort, - clientId: MqttClientId + ": " + d.name, - username: d.mqttusername, - password: this.encryptionHelperService.basicDecrypt(d.mqttpassword), - cert: d.deviceCertificate, - key: this.encryptionHelperService.basicDecrypt(d.deviceCertificateKey), - ca: d.caCertificate, - rejectUnauthorized: false, - }); - this.setupClient(client, d); - this.clientDictionary.set(d.id, client); - }); - } + public createMQTTClients(mqttSubscribers: MQTTExternalBrokerDevice[]) { + mqttSubscribers.forEach(async d => { + // Cannot create clients without a topic + if (!d.mqtttopicname) { + this.logger.error(`Something went wrong while connecting device ${d.name} removed client`); + await this.iotDeviceService.markMqttExternalBrokerAsInvalid(d); + return; + } + const client = connect(d.mqttURL, { + clean: true, + port: d.mqttPort, + clientId: MqttClientId + ": " + d.name, + username: d.mqttusername, + password: this.encryptionHelperService.basicDecrypt(d.mqttpassword), + cert: d.deviceCertificate, + key: this.encryptionHelperService.basicDecrypt(d.deviceCertificateKey), + ca: d.caCertificate, + rejectUnauthorized: false, + }); + this.setupClient(client, d); + this.clientDictionary.set(d.id, client); + }); + } - public removeMQTTClient(mqttSubscriber: MQTTExternalBrokerDevice) { - this.clientDictionary - .get(mqttSubscriber.id) - ?.end(false, {}, () => this.clientDictionary.delete(mqttSubscriber.id)); - this.logger.debug(`Removed client for deviceId: ${mqttSubscriber.id}`); - } + public removeMQTTClient(mqttSubscriber: MQTTExternalBrokerDevice) { + this.clientDictionary.get(mqttSubscriber.id)?.end(false, {}, () => this.clientDictionary.delete(mqttSubscriber.id)); + this.logger.debug(`Removed client for deviceId: ${mqttSubscriber.id}`); + } - private setupClient(client: Client, device: MQTTExternalBrokerDevice) { - client.on("connect", () => { - client.subscribe(device.mqtttopicname); - this.logger.debug( - `Connected to ${device.mqttURL} for deviceId: ${device.id}` - ); + private setupClient(client: Client, device: MQTTExternalBrokerDevice) { + client.on("connect", () => { + client.subscribe(device.mqtttopicname); + this.logger.debug(`Connected to ${device.mqttURL} for deviceId: ${device.id}`); - client.on("message", async (topic, message) => { - await this.handleMessage(message.toString(), device); - }); - }); - client.on("end", async () => { - await this.iotDeviceService.markMqttExternalBrokerAsInvalid(device); - this.removeMQTTClient(device); - }); - } + client.on("message", async (topic, message) => { + await this.handleMessage(message.toString(), device); + }); + }); + client.on("end", async () => { + await this.iotDeviceService.markMqttExternalBrokerAsInvalid(device); + this.removeMQTTClient(device); + }); + } - private async handleMessage(message: string, device: MQTTExternalBrokerDevice) { - await this.receiveDataService.sendRawIotDeviceRequestToKafka( - device, - message, - IoTDeviceType.MQTTExternalBroker.toString() - ); - } + private async handleMessage(message: string, device: MQTTExternalBrokerDevice) { + await this.receiveDataService.sendRawIotDeviceRequestToKafka( + device, + message, + IoTDeviceType.MQTTExternalBroker.toString() + ); + } } diff --git a/src/services/data-management/open-data-dk-sharing.service.ts b/src/services/data-management/open-data-dk-sharing.service.ts index 4be62c38..aeea7864 100644 --- a/src/services/data-management/open-data-dk-sharing.service.ts +++ b/src/services/data-management/open-data-dk-sharing.service.ts @@ -17,166 +17,166 @@ import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device. @Injectable() export class OpenDataDkSharingService { - constructor( - @InjectRepository(OpenDataDkDataset) - private repository: Repository, - private payloadDecoderExecutorService: PayloadDecoderExecutorService, - private chirpstackDeviceService: ChirpstackDeviceService - ) {} - - private readonly BACKEND_BASE_URL = configuration()["backend"]["baseurl"]; - private readonly logger = new Logger(OpenDataDkSharingService.name); - - async getDecodedDataInDataset(dataset: OpenDataDkDataset): Promise { - const rawData = await this.repository - .createQueryBuilder("dataset") - .innerJoinAndSelect("dataset.dataTarget", "dt") - .innerJoinAndSelect("dt.connections", "connections") - .innerJoinAndSelect("connections.iotDevices", "devices") - .leftJoinAndSelect("devices.deviceModel", "dm") - .leftJoinAndSelect("connections.payloadDecoder", "pd") - .innerJoinAndSelect("devices.latestReceivedMessage", "msg") - .where("dataset.id = :id", { id: dataset.id }) - .getOne(); - - if (!rawData) { - return { error: ErrorCodes.NoData }; - } - - return await this.decodeData(rawData); - } - - private async decodeData(rawData: OpenDataDkDataset) { - const results: any[] = []; - for (const connection of rawData.dataTarget.connections) { - this.logger.debug(`Got connection(${connection.id})`); - for (const device of connection.iotDevices) { - await this.decodeDevice(device, connection, results); - } - } - return results; - } - - private async decodeDevice( - device: IoTDevice, - connection: IoTDevicePayloadDecoderDataTargetConnection, - results: any[] - ) { - this.logger.debug(`Doing device ${device.name} / ${device.id}`); - if (!device.latestReceivedMessage) { - this.logger.debug(`Device ${device.name} / ${device.id} has no data ... skipping`); - return; - } - try { - if (connection.payloadDecoder != null) { - // Enrich lorawan devices with chirpstack data - if ( - device.type === IoTDeviceType.LoRaWAN && - connection.payloadDecoder.decodingFunction.includes("lorawanSettings") - ) { - device = await this.chirpstackDeviceService.enrichLoRaWANDevice(device); - } - - const decoded = await this.payloadDecoderExecutorService.callUntrustedCode( - connection.payloadDecoder.decodingFunction, - device, - device.latestReceivedMessage.rawData - ); - results.push(JSON.parse(decoded)); - } else { - results.push(device.latestReceivedMessage.rawData); - } - } catch (err) { - this.logger.error( - `Got error during decode of device(${device.id}) with decoder(${connection.payloadDecoder.id}): ${err}` - ); - results.push({ - error: `Could not decode data for device(${device.id})`, - }); - } - } - - async createDCAT(organization: Organization): Promise { - const datasets = await this.getAllOpenDataDkSharesForOrganization(organization); - - return this.mapToDCAT(organization, datasets); + constructor( + @InjectRepository(OpenDataDkDataset) + private repository: Repository, + private payloadDecoderExecutorService: PayloadDecoderExecutorService, + private chirpstackDeviceService: ChirpstackDeviceService + ) {} + + private readonly BACKEND_BASE_URL = configuration()["backend"]["baseurl"]; + private readonly logger = new Logger(OpenDataDkSharingService.name); + + async getDecodedDataInDataset(dataset: OpenDataDkDataset): Promise { + const rawData = await this.repository + .createQueryBuilder("dataset") + .innerJoinAndSelect("dataset.dataTarget", "dt") + .innerJoinAndSelect("dt.connections", "connections") + .innerJoinAndSelect("connections.iotDevices", "devices") + .leftJoinAndSelect("devices.deviceModel", "dm") + .leftJoinAndSelect("connections.payloadDecoder", "pd") + .innerJoinAndSelect("devices.latestReceivedMessage", "msg") + .where("dataset.id = :id", { id: dataset.id }) + .getOne(); + + if (!rawData) { + return { error: ErrorCodes.NoData }; } - async findById(shareId: number, organizationId: number): Promise { - return await this.findDatasetWithRelations() - .where("dataset.id = :datasetId and org.id = :organizationId", { - datasetId: shareId, - organizationId: organizationId, - }) - .getOne(); - } - - async getAllOpenDataDkSharesForOrganization(organization: Organization): Promise { - return this.findDatasetWithRelations().where("org.id = :orgId", { orgId: organization.id }).getMany(); - } - - private findDatasetWithRelations() { - return this.repository - .createQueryBuilder("dataset") - .innerJoin("dataset.dataTarget", "dt") - .innerJoin("dt.application", "app") - .innerJoin("app.belongsTo", "org"); - } - - private mapToDCAT(organization: Organization, datasets: OpenDataDkDataset[]): DCATRootObject { - const root = new DCATRootObject(); - root["@context"] = "https://project-open-data.cio.gov/v1.1/schema/catalog.jsonld"; - root["@type"] = "dcat:Catalog"; - root.conformsTo = "https://project-open-data.cio.gov/v1.1/schema"; - root.describedBy = "https://project-open-data.cio.gov/v1.1/schema/catalog.json"; + return await this.decodeData(rawData); + } - root.dataset = datasets.map(x => { - return this.mapDataset(organization, x); - }); - - return root; - } - - private mapDataset(organization: Organization, dataset: OpenDataDkDataset) { - const ds = new Dataset(); - ds["@type"] = "dcat:Dataset"; - ds.accessLevel = "public"; - - ds.identifier = this.generateUrl(organization, dataset); - ds.landingPage = undefined; - ds.title = dataset.name; - ds.description = dataset.description; - ds.keyword = dataset.keywords != null ? dataset.keywords : []; - ds.issued = dataset.createdAt; - ds.modified = dataset.updatedAt; - ds.publisher = { - name: organization.name, - }; - ds.contactPoint = new ContactPoint(); - ds.contactPoint["@type"] = "vcard:Contact"; - ds.contactPoint.fn = dataset.authorName; - ds.contactPoint.hasEmail = `mailto:${dataset.authorEmail}`; - - ds.distribution = [this.mapDistribution(organization, dataset)]; - - return ds; + private async decodeData(rawData: OpenDataDkDataset) { + const results: any[] = []; + for (const connection of rawData.dataTarget.connections) { + this.logger.debug(`Got connection(${connection.id})`); + for (const device of connection.iotDevices) { + await this.decodeDevice(device, connection, results); + } } - - private mapDistribution(organization: Organization, dataset: OpenDataDkDataset) { - const distribution = new Distribution(); - distribution["@type"] = "dcat:Distribution"; - distribution.mediaType = "application/json"; - distribution.format = "JSON"; - distribution.license = dataset.license; - - distribution.accessURL = this.generateUrl(organization, dataset); - distribution.title = dataset.resourceTitle; - return distribution; + return results; + } + + private async decodeDevice( + device: IoTDevice, + connection: IoTDevicePayloadDecoderDataTargetConnection, + results: any[] + ) { + this.logger.debug(`Doing device ${device.name} / ${device.id}`); + if (!device.latestReceivedMessage) { + this.logger.debug(`Device ${device.name} / ${device.id} has no data ... skipping`); + return; } + try { + if (connection.payloadDecoder != null) { + // Enrich lorawan devices with chirpstack data + if ( + device.type === IoTDeviceType.LoRaWAN && + connection.payloadDecoder.decodingFunction.includes("lorawanSettings") + ) { + device = await this.chirpstackDeviceService.enrichLoRaWANDevice(device); + } - private generateUrl(organization: Organization, dataset: OpenDataDkDataset): string { - const controllerUrl = Reflect.getMetadata(PATH_METADATA, OpenDataDkSharingController); - const organizationId = organization.id; - return `${this.BACKEND_BASE_URL}/api/v1/${controllerUrl}/${organizationId}/data/${dataset.id}`; + const decoded = await this.payloadDecoderExecutorService.callUntrustedCode( + connection.payloadDecoder.decodingFunction, + device, + device.latestReceivedMessage.rawData + ); + results.push(JSON.parse(decoded)); + } else { + results.push(device.latestReceivedMessage.rawData); + } + } catch (err) { + this.logger.error( + `Got error during decode of device(${device.id}) with decoder(${connection.payloadDecoder.id}): ${err}` + ); + results.push({ + error: `Could not decode data for device(${device.id})`, + }); } + } + + async createDCAT(organization: Organization): Promise { + const datasets = await this.getAllOpenDataDkSharesForOrganization(organization); + + return this.mapToDCAT(organization, datasets); + } + + async findById(shareId: number, organizationId: number): Promise { + return await this.findDatasetWithRelations() + .where("dataset.id = :datasetId and org.id = :organizationId", { + datasetId: shareId, + organizationId: organizationId, + }) + .getOne(); + } + + async getAllOpenDataDkSharesForOrganization(organization: Organization): Promise { + return this.findDatasetWithRelations().where("org.id = :orgId", { orgId: organization.id }).getMany(); + } + + private findDatasetWithRelations() { + return this.repository + .createQueryBuilder("dataset") + .innerJoin("dataset.dataTarget", "dt") + .innerJoin("dt.application", "app") + .innerJoin("app.belongsTo", "org"); + } + + private mapToDCAT(organization: Organization, datasets: OpenDataDkDataset[]): DCATRootObject { + const root = new DCATRootObject(); + root["@context"] = "https://project-open-data.cio.gov/v1.1/schema/catalog.jsonld"; + root["@type"] = "dcat:Catalog"; + root.conformsTo = "https://project-open-data.cio.gov/v1.1/schema"; + root.describedBy = "https://project-open-data.cio.gov/v1.1/schema/catalog.json"; + + root.dataset = datasets.map(x => { + return this.mapDataset(organization, x); + }); + + return root; + } + + private mapDataset(organization: Organization, dataset: OpenDataDkDataset) { + const ds = new Dataset(); + ds["@type"] = "dcat:Dataset"; + ds.accessLevel = "public"; + + ds.identifier = this.generateUrl(organization, dataset); + ds.landingPage = undefined; + ds.title = dataset.name; + ds.description = dataset.description; + ds.keyword = dataset.keywords != null ? dataset.keywords : []; + ds.issued = dataset.createdAt; + ds.modified = dataset.updatedAt; + ds.publisher = { + name: organization.name, + }; + ds.contactPoint = new ContactPoint(); + ds.contactPoint["@type"] = "vcard:Contact"; + ds.contactPoint.fn = dataset.authorName; + ds.contactPoint.hasEmail = `mailto:${dataset.authorEmail}`; + + ds.distribution = [this.mapDistribution(organization, dataset)]; + + return ds; + } + + private mapDistribution(organization: Organization, dataset: OpenDataDkDataset) { + const distribution = new Distribution(); + distribution["@type"] = "dcat:Distribution"; + distribution.mediaType = "application/json"; + distribution.format = "JSON"; + distribution.license = dataset.license; + + distribution.accessURL = this.generateUrl(organization, dataset); + distribution.title = dataset.resourceTitle; + return distribution; + } + + private generateUrl(organization: Organization, dataset: OpenDataDkDataset): string { + const controllerUrl = Reflect.getMetadata(PATH_METADATA, OpenDataDkSharingController); + const organizationId = organization.id; + return `${this.BACKEND_BASE_URL}/api/v1/${controllerUrl}/${organizationId}/data/${dataset.id}`; + } } diff --git a/src/services/data-management/payload-decoder-executor.service.ts b/src/services/data-management/payload-decoder-executor.service.ts index b475c4f4..8f297ba7 100644 --- a/src/services/data-management/payload-decoder-executor.service.ts +++ b/src/services/data-management/payload-decoder-executor.service.ts @@ -4,34 +4,34 @@ import * as worker_threads from "node:worker_threads"; @Injectable() export class PayloadDecoderExecutorService { - private readonly logger = new Logger(PayloadDecoderExecutorService.name); - - async allUntrustedCodeWithJsonStrings( - code: string, - iotDeviceString: string, - rawPayloadString: string - ): Promise { - const iotDevice = JSON.parse(iotDeviceString); - const rawPayload = JSON.parse(rawPayloadString); - const parsedCode = JSON.parse(code); - - return await this.callUntrustedCode(parsedCode, iotDevice, rawPayload); - } - - async callUntrustedCode(code: string, iotDevice: IoTDevice | any, rawPayload: JSON): Promise { - // Left as check of surrounding code for worker function - // const workerFunction = () => { - // const { parentPort, workerData } = require("worker_threads"); - // const innerPayload = workerData.innerPayload; - // const innerIotDevice = workerData.innerIotDevice; - // - // code; - // - // const result = decode(innerPayload, innerIotDevice); - // parentPort.postMessage(result); - // }; - - const workerCode = ` + private readonly logger = new Logger(PayloadDecoderExecutorService.name); + + async allUntrustedCodeWithJsonStrings( + code: string, + iotDeviceString: string, + rawPayloadString: string + ): Promise { + const iotDevice = JSON.parse(iotDeviceString); + const rawPayload = JSON.parse(rawPayloadString); + const parsedCode = JSON.parse(code); + + return await this.callUntrustedCode(parsedCode, iotDevice, rawPayload); + } + + async callUntrustedCode(code: string, iotDevice: IoTDevice | any, rawPayload: JSON): Promise { + // Left as check of surrounding code for worker function + // const workerFunction = () => { + // const { parentPort, workerData } = require("worker_threads"); + // const innerPayload = workerData.innerPayload; + // const innerIotDevice = workerData.innerIotDevice; + // + // code; + // + // const result = decode(innerPayload, innerIotDevice); + // parentPort.postMessage(result); + // }; + + const workerCode = ` const { parentPort, workerData } = require("worker_threads"); const innerPayload = workerData.innerPayload; const innerIotDevice = workerData.innerIotDevice; @@ -41,23 +41,23 @@ export class PayloadDecoderExecutorService { const result = decode(innerPayload, innerIotDevice); parentPort.postMessage(result);`; - const workerFunction = new Promise((resolve, reject) => { - const worker = new worker_threads.Worker(workerCode, { - eval: true, - workerData: { innerPayload: rawPayload, innerIotDevice: iotDevice }, - }); - - worker.on("message", message => { - resolve(message); - worker.terminate(); - }); - - worker.on("error", err => { - reject(err); - worker.terminate(); - }); - }); - - return JSON.stringify(await workerFunction); - } + const workerFunction = new Promise((resolve, reject) => { + const worker = new worker_threads.Worker(workerCode, { + eval: true, + workerData: { innerPayload: rawPayload, innerIotDevice: iotDevice }, + }); + + worker.on("message", message => { + resolve(message); + worker.terminate(); + }); + + worker.on("error", err => { + reject(err); + worker.terminate(); + }); + }); + + return JSON.stringify(await workerFunction); + } } diff --git a/src/services/data-management/payload-decoder-listener.service.ts b/src/services/data-management/payload-decoder-listener.service.ts index 20a3e07c..54f75dbb 100644 --- a/src/services/data-management/payload-decoder-listener.service.ts +++ b/src/services/data-management/payload-decoder-listener.service.ts @@ -18,120 +18,118 @@ import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device. @Injectable() export class PayloadDecoderListenerService extends AbstractKafkaConsumer { - constructor( - private connectionService: IoTDevicePayloadDecoderDataTargetConnectionService, - private chirpstackDeviceService: ChirpstackDeviceService, - private kafkaService: KafkaService, - private executor: PayloadDecoderExecutorService - ) { - super(); + constructor( + private connectionService: IoTDevicePayloadDecoderDataTargetConnectionService, + private chirpstackDeviceService: ChirpstackDeviceService, + private kafkaService: KafkaService, + private executor: PayloadDecoderExecutorService + ) { + super(); + } + + private readonly logger = new Logger(PayloadDecoderListenerService.name); + + protected registerTopic(): void { + this.addTopic(KafkaTopic.RAW_REQUEST, PayloadDecoderListenerService.name); + } + + @CombinedSubscribeTo(KafkaTopic.RAW_REQUEST, PayloadDecoderListenerService.name) + async rawRequestListener(payload: KafkaPayload): Promise { + this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); + + // Fetch related objects + const dto = payload.body as RawIoTDeviceRequestDto; + const connections = await this.connectionService.findAllByIoTDeviceIdWithDeviceModel(dto.iotDeviceId); + this.logger.debug(`Found ${connections.count} connections for IoT-Device ${dto.iotDeviceId}`); + + // Find Unique payloadDecoders + await this.doTransformationsAndSend(connections, dto); + } + + private async doTransformationsAndSend(connections: ListAllConnectionsResponseDto, dto: RawIoTDeviceRequestDto) { + const uniqueCombinations = _.uniqBy(connections.data, x => x.payloadDecoder?.id); + for (const connection of uniqueCombinations) { + try { + const iotDevice = connection.iotDevices.find(x => x.id === dto.iotDeviceId); + + await this.decodeAndSendTransformed(connection.payloadDecoder, iotDevice, dto.rawPayload); + } catch (err) { + this.logger.error(err); + } } - - private readonly logger = new Logger(PayloadDecoderListenerService.name); - - protected registerTopic(): void { - this.addTopic(KafkaTopic.RAW_REQUEST, PayloadDecoderListenerService.name); - } - - @CombinedSubscribeTo(KafkaTopic.RAW_REQUEST, PayloadDecoderListenerService.name) - async rawRequestListener(payload: KafkaPayload): Promise { - this.logger.debug(`RAW_REQUEST: '${JSON.stringify(payload)}'`); - - // Fetch related objects - const dto = payload.body as RawIoTDeviceRequestDto; - const connections = await this.connectionService.findAllByIoTDeviceIdWithDeviceModel(dto.iotDeviceId); - this.logger.debug(`Found ${connections.count} connections for IoT-Device ${dto.iotDeviceId}`); - - // Find Unique payloadDecoders - await this.doTransformationsAndSend(connections, dto); - } - - private async doTransformationsAndSend(connections: ListAllConnectionsResponseDto, dto: RawIoTDeviceRequestDto) { - const uniqueCombinations = _.uniqBy(connections.data, x => x.payloadDecoder?.id); - for (const connection of uniqueCombinations) { - try { - const iotDevice = connection.iotDevices.find(x => x.id === dto.iotDeviceId); - - await this.decodeAndSendTransformed(connection.payloadDecoder, iotDevice, dto.rawPayload); - } catch (err) { - this.logger.error(err); - } - } + } + + private async decodeAndSendTransformed( + payloadDecoder: PayloadDecoder, + relatedIoTDevice: IoTDevice, + rawPayload: JSON + ) { + const payloadToSend = await this.decodeIfNeeded(payloadDecoder, relatedIoTDevice, rawPayload); + + // Add transformed request to Kafka + await this.sendTransformedRequest(relatedIoTDevice, payloadDecoder, payloadToSend); + } + + private async decodeIfNeeded(payloadDecoder: PayloadDecoder, relatedIoTDevice: IoTDevice, rawPayload: JSON) { + let res: string; + if (payloadDecoder != undefined) { + this.logger.debug(`Decoding payload of IoT-Device ${relatedIoTDevice.id} with decoder ${payloadDecoder?.id}`); + + let localDevice = relatedIoTDevice; + // Check if lorawanSettings are read, if they are the iotDevice needs enrichment + if ( + relatedIoTDevice.type === IoTDeviceType.LoRaWAN && + payloadDecoder.decodingFunction.includes("lorawanSettings") + ) { + localDevice = await this.chirpstackDeviceService.enrichLoRaWANDevice(relatedIoTDevice); + } + + // Decode the payload + res = await this.executor.callUntrustedCode(payloadDecoder.decodingFunction, localDevice, rawPayload); + + this.logger.debug(`Decoded payload to: '${res}'`); + } else { + this.logger.debug(`Skip decoding payload ...`); + res = JSON.stringify(rawPayload); } - - private async decodeAndSendTransformed( - payloadDecoder: PayloadDecoder, - relatedIoTDevice: IoTDevice, - rawPayload: JSON - ) { - const payloadToSend = await this.decodeIfNeeded(payloadDecoder, relatedIoTDevice, rawPayload); - - // Add transformed request to Kafka - await this.sendTransformedRequest(relatedIoTDevice, payloadDecoder, payloadToSend); - } - - private async decodeIfNeeded(payloadDecoder: PayloadDecoder, relatedIoTDevice: IoTDevice, rawPayload: JSON) { - let res: string; - if (payloadDecoder != undefined) { - this.logger.debug( - `Decoding payload of IoT-Device ${relatedIoTDevice.id} with decoder ${payloadDecoder?.id}` - ); - - let localDevice = relatedIoTDevice; - // Check if lorawanSettings are read, if they are the iotDevice needs enrichment - if ( - relatedIoTDevice.type === IoTDeviceType.LoRaWAN && - payloadDecoder.decodingFunction.includes("lorawanSettings") - ) { - localDevice = await this.chirpstackDeviceService.enrichLoRaWANDevice(relatedIoTDevice); - } - - // Decode the payload - res = await this.executor.callUntrustedCode(payloadDecoder.decodingFunction, localDevice, rawPayload); - - this.logger.debug(`Decoded payload to: '${res}'`); - } else { - this.logger.debug(`Skip decoding payload ...`); - res = JSON.stringify(rawPayload); - } - return res; - } - - private async sendTransformedRequest( - relatedIoTDevice: IoTDevice, - payloadTransformer: PayloadDecoder, - decoded: string - ): Promise { - const transformedPayloadDto: TransformedPayloadDto = await this.makeTransformedPayload( - relatedIoTDevice.id, - payloadTransformer != null ? payloadTransformer.id : null, - decoded - ); - - const kafkapayload: KafkaPayload = { - messageId: "transformedRequest" + new Date().valueOf(), - body: transformedPayloadDto, - messageType: "transformedRequest.decoded", - topicName: KafkaTopic.TRANSFORMED_REQUEST, - }; - - const rawStatus = await this.kafkaService.sendMessage(KafkaTopic.TRANSFORMED_REQUEST, kafkapayload); - - if (rawStatus) { - const metadata = rawStatus as RecordMetadata[]; - this.logger.debug(`kafka status '${metadata[0].errorCode}'`); - } - } - - async makeTransformedPayload( - id: number, - payloadTransformerId: number, - decodedPayload: string - ): Promise { - return { - iotDeviceId: id, - payload: JSON.parse(decodedPayload), - payloadDecoderId: payloadTransformerId, - }; + return res; + } + + private async sendTransformedRequest( + relatedIoTDevice: IoTDevice, + payloadTransformer: PayloadDecoder, + decoded: string + ): Promise { + const transformedPayloadDto: TransformedPayloadDto = await this.makeTransformedPayload( + relatedIoTDevice.id, + payloadTransformer != null ? payloadTransformer.id : null, + decoded + ); + + const kafkapayload: KafkaPayload = { + messageId: "transformedRequest" + new Date().valueOf(), + body: transformedPayloadDto, + messageType: "transformedRequest.decoded", + topicName: KafkaTopic.TRANSFORMED_REQUEST, + }; + + const rawStatus = await this.kafkaService.sendMessage(KafkaTopic.TRANSFORMED_REQUEST, kafkapayload); + + if (rawStatus) { + const metadata = rawStatus as RecordMetadata[]; + this.logger.debug(`kafka status '${metadata[0].errorCode}'`); } + } + + async makeTransformedPayload( + id: number, + payloadTransformerId: number, + decodedPayload: string + ): Promise { + return { + iotDeviceId: id, + payload: JSON.parse(decodedPayload), + payloadDecoderId: payloadTransformerId, + }; + } } diff --git a/src/services/data-management/payload-decoder.service.ts b/src/services/data-management/payload-decoder.service.ts index 1cab6a8e..ce3713f5 100644 --- a/src/services/data-management/payload-decoder.service.ts +++ b/src/services/data-management/payload-decoder.service.ts @@ -12,112 +12,89 @@ import { OrganizationService } from "@services/user-management/organization.serv @Injectable() export class PayloadDecoderService { - constructor( - @InjectRepository(PayloadDecoder) - private payloadDecoderRepository: Repository, - private organizationService: OrganizationService - ) {} + constructor( + @InjectRepository(PayloadDecoder) + private payloadDecoderRepository: Repository, + private organizationService: OrganizationService + ) {} - async findOne(id: number): Promise { - return await this.payloadDecoderRepository.findOneOrFail({ - where: { id }, - relations: ["organization"], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - } + async findOne(id: number): Promise { + return await this.payloadDecoderRepository.findOneOrFail({ + where: { id }, + relations: ["organization"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } - private getSorting(query: ListAllEntitiesDto) { - const sorting: { [id: string]: string | number } = {}; - if ( - query?.orderOn != null && - (query.orderOn == "id" || query.orderOn == "name") - ) { - sorting[query.orderOn] = query.sort.toLocaleUpperCase(); - } else { - sorting["id"] = "ASC"; - } - return sorting; + private getSorting(query: ListAllEntitiesDto) { + const sorting: { [id: string]: string | number } = {}; + if (query?.orderOn != null && (query.orderOn == "id" || query.orderOn == "name")) { + sorting[query.orderOn] = query.sort.toLocaleUpperCase(); + } else { + sorting["id"] = "ASC"; } + return sorting; + } - async findAndCountWithPagination( - query: ListAllEntitiesDto, - organizationId: number | null | undefined - ): Promise { - const [result, total] = await this.payloadDecoderRepository.findAndCount({ - where: organizationId ? { organization: {id: organizationId} } : {}, - take: query.limit, - skip: query.offset, - order: this.getSorting(query), - relations: ["organization"], - }); - - return { - data: result, - count: total, - }; - } + async findAndCountWithPagination( + query: ListAllEntitiesDto, + organizationId: number | null | undefined + ): Promise { + const [result, total] = await this.payloadDecoderRepository.findAndCount({ + where: organizationId ? { organization: { id: organizationId } } : {}, + take: query.limit, + skip: query.offset, + order: this.getSorting(query), + relations: ["organization"], + }); - async create( - createDto: CreatePayloadDecoderDto, - userId: number - ): Promise { - const newPayloadDecoder = new PayloadDecoder(); - const mappedPayloadDecoder = await this.mapDtoToPayloadDecoder( - createDto, - newPayloadDecoder - ); - mappedPayloadDecoder.createdBy = userId; - mappedPayloadDecoder.updatedBy = userId; + return { + data: result, + count: total, + }; + } - return await this.payloadDecoderRepository.save(mappedPayloadDecoder); - } + async create(createDto: CreatePayloadDecoderDto, userId: number): Promise { + const newPayloadDecoder = new PayloadDecoder(); + const mappedPayloadDecoder = await this.mapDtoToPayloadDecoder(createDto, newPayloadDecoder); + mappedPayloadDecoder.createdBy = userId; + mappedPayloadDecoder.updatedBy = userId; - async update( - id: number, - updateDto: UpdatePayloadDecoderDto, - userId: number - ): Promise { - const payloadDecoder = await this.payloadDecoderRepository.findOneByOrFail({ - id, - }); + return await this.payloadDecoderRepository.save(mappedPayloadDecoder); + } - const mappedPayloadDecoder = await this.mapDtoToPayloadDecoder( - updateDto, - payloadDecoder - ); - mappedPayloadDecoder.updatedBy = userId; + async update(id: number, updateDto: UpdatePayloadDecoderDto, userId: number): Promise { + const payloadDecoder = await this.payloadDecoderRepository.findOneByOrFail({ + id, + }); - return await this.payloadDecoderRepository.save(mappedPayloadDecoder); - } + const mappedPayloadDecoder = await this.mapDtoToPayloadDecoder(updateDto, payloadDecoder); + mappedPayloadDecoder.updatedBy = userId; - async delete(id: number): Promise { - return await this.payloadDecoderRepository.delete(id); - } + return await this.payloadDecoderRepository.save(mappedPayloadDecoder); + } - private async mapDtoToPayloadDecoder( - createDto: CreatePayloadDecoderDto, - newPayloadDecoder: PayloadDecoder - ) { - newPayloadDecoder.name = createDto.name; - try { - newPayloadDecoder.decodingFunction = JSON.parse(createDto.decodingFunction); - } catch (err) { - Logger.error("Failed to parse decodingFunction", err); - throw new BadRequestException(ErrorCodes.BadEncoding); - } - try { - newPayloadDecoder.organization = await this.organizationService.findById( - createDto.organizationId - ); - } catch (err) { - Logger.error( - `Could not find Organization with id ${createDto.organizationId}` - ); - throw new BadRequestException(ErrorCodes.OrganizationDoesNotExists); - } + async delete(id: number): Promise { + return await this.payloadDecoderRepository.delete(id); + } - return newPayloadDecoder; + private async mapDtoToPayloadDecoder(createDto: CreatePayloadDecoderDto, newPayloadDecoder: PayloadDecoder) { + newPayloadDecoder.name = createDto.name; + try { + newPayloadDecoder.decodingFunction = JSON.parse(createDto.decodingFunction); + } catch (err) { + Logger.error("Failed to parse decodingFunction", err); + throw new BadRequestException(ErrorCodes.BadEncoding); } + try { + newPayloadDecoder.organization = await this.organizationService.findById(createDto.organizationId); + } catch (err) { + Logger.error(`Could not find Organization with id ${createDto.organizationId}`); + throw new BadRequestException(ErrorCodes.OrganizationDoesNotExists); + } + + return newPayloadDecoder; + } } diff --git a/src/services/data-management/receive-data.service.ts b/src/services/data-management/receive-data.service.ts index 7f39a659..17601437 100644 --- a/src/services/data-management/receive-data.service.ts +++ b/src/services/data-management/receive-data.service.ts @@ -11,80 +11,63 @@ import { RecordMetadata } from "kafkajs"; @Injectable() export class ReceiveDataService { - constructor(private kafkaService: KafkaService) {} - private readonly logger = new Logger(ReceiveDataService.name); + constructor(private kafkaService: KafkaService) {} + private readonly logger = new Logger(ReceiveDataService.name); - async sendRawIotDeviceRequestToKafka( - iotDevice: IoTDevice, - data: string, - type: IoTDeviceType[number], - timestamp?: number - ): Promise { - const dto = new RawIoTDeviceRequestDto(); - dto.iotDeviceId = iotDevice.id; - try { - dto.rawPayload = JSON.parse(data); - } catch (e) { - dto.rawPayload = JSON.parse("{}"); - } - const payload = this.buildMessage(dto, type, KafkaTopic.RAW_REQUEST, timestamp); - - await this.doSendToKafka(payload, KafkaTopic.RAW_REQUEST); + async sendRawIotDeviceRequestToKafka( + iotDevice: IoTDevice, + data: string, + type: IoTDeviceType[number], + timestamp?: number + ): Promise { + const dto = new RawIoTDeviceRequestDto(); + dto.iotDeviceId = iotDevice.id; + try { + dto.rawPayload = JSON.parse(data); + } catch (e) { + dto.rawPayload = JSON.parse("{}"); } + const payload = this.buildMessage(dto, type, KafkaTopic.RAW_REQUEST, timestamp); - async sendRawGatewayStateToKafka( - gatewayId: string, - data: string, - timestamp?: number - ): Promise { - const dto = new RawGatewayStateDto(); - dto.gatewayId = gatewayId; - dto.rawPayload = JSON.parse(data); - const payload = this.buildMessage( - dto, - "GATEWAY", - KafkaTopic.RAW_GATEWAY_STATE, - timestamp - ); + await this.doSendToKafka(payload, KafkaTopic.RAW_REQUEST); + } - await this.doSendToKafka(payload, KafkaTopic.RAW_GATEWAY_STATE); - } + async sendRawGatewayStateToKafka(gatewayId: string, data: string, timestamp?: number): Promise { + const dto = new RawGatewayStateDto(); + dto.gatewayId = gatewayId; + dto.rawPayload = JSON.parse(data); + const payload = this.buildMessage(dto, "GATEWAY", KafkaTopic.RAW_GATEWAY_STATE, timestamp); - private buildMessage( - dto: RawRequestDto, - type: string, - topicName: KafkaTopic, - timestamp?: number - ): KafkaPayload { - this.logger.debug(`Received data, sending to Kafka`); - dto.type = type; - // We cannot generically know when it was sent by the device, "now" is accurate enough - dto.unixTimestamp = - timestamp !== null && timestamp !== undefined - ? timestamp - : new Date().valueOf(); + await this.doSendToKafka(payload, KafkaTopic.RAW_GATEWAY_STATE); + } - const payload: KafkaPayload = { - messageId: `${type}${new Date().valueOf()}`, - body: dto, - messageType: `receiveData.${type}`, - topicName, - }; - this.logger.debug(`Made payload: '${JSON.stringify(payload)}'`); + private buildMessage(dto: RawRequestDto, type: string, topicName: KafkaTopic, timestamp?: number): KafkaPayload { + this.logger.debug(`Received data, sending to Kafka`); + dto.type = type; + // We cannot generically know when it was sent by the device, "now" is accurate enough + dto.unixTimestamp = timestamp !== null && timestamp !== undefined ? timestamp : new Date().valueOf(); - return payload; - } + const payload: KafkaPayload = { + messageId: `${type}${new Date().valueOf()}`, + body: dto, + messageType: `receiveData.${type}`, + topicName, + }; + this.logger.debug(`Made payload: '${JSON.stringify(payload)}'`); + + return payload; + } - private async doSendToKafka(payload: KafkaPayload, topic: KafkaTopic) { - const rawStatus = await this.kafkaService.sendMessage(topic, payload); + private async doSendToKafka(payload: KafkaPayload, topic: KafkaTopic) { + const rawStatus = await this.kafkaService.sendMessage(topic, payload); - this.logger.debug(`Sent message to Kafka: ${JSON.stringify(rawStatus)}`); + this.logger.debug(`Sent message to Kafka: ${JSON.stringify(rawStatus)}`); - if (rawStatus) { - const metadata = rawStatus as RecordMetadata[]; - this.logger.debug(`kafka status '${metadata[0].errorCode}'`); - } else { - this.logger.warn(`Did not get a raw status from Kafka ...`); - } + if (rawStatus) { + const metadata = rawStatus as RecordMetadata[]; + this.logger.debug(`kafka status '${metadata[0].errorCode}'`); + } else { + this.logger.warn(`Did not get a raw status from Kafka ...`); } + } } diff --git a/src/services/data-management/search.service.ts b/src/services/data-management/search.service.ts index cfd2704b..11a0de72 100644 --- a/src/services/data-management/search.service.ts +++ b/src/services/data-management/search.service.ts @@ -16,224 +16,218 @@ import { Repository, SelectQueryBuilder } from "typeorm"; @Injectable() export class SearchService { - constructor( - private gatewayService: ChirpstackGatewayService, - @InjectRepository(IoTDevice) - private iotDeviceRepository: Repository, - @InjectRepository(Application) - private applicationRepository: Repository - ) {} - - private readonly SEARCH_RESULT_LIMIT = 100; - private readonly logger = new Logger(SearchService.name); - - async findByQuery( - req: AuthenticatedRequest, - query: string, - limit = this.SEARCH_RESULT_LIMIT, - offset = 0 - ): Promise { - const urlDecoded = decodeURIComponent(query); - const trimmedQuery = urlDecoded.trim(); - - const gatewayPromise = this.findGatewaysAndMapType(trimmedQuery); - const applicationPromise = this.findApplicationsAndMapType(req, trimmedQuery); - const devicePromise = this.findDevicesAndMapType(req, trimmedQuery); - - const results = _.filter( - _.flatMap(await Promise.all([applicationPromise, devicePromise, gatewayPromise])), - x => x != null - ); - - return { - data: this.limitAndOrder(results, limit, offset), - count: results.length, - }; + constructor( + private gatewayService: ChirpstackGatewayService, + @InjectRepository(IoTDevice) + private iotDeviceRepository: Repository, + @InjectRepository(Application) + private applicationRepository: Repository + ) {} + + private readonly SEARCH_RESULT_LIMIT = 100; + private readonly logger = new Logger(SearchService.name); + + async findByQuery( + req: AuthenticatedRequest, + query: string, + limit = this.SEARCH_RESULT_LIMIT, + offset = 0 + ): Promise { + const urlDecoded = decodeURIComponent(query); + const trimmedQuery = urlDecoded.trim(); + + const gatewayPromise = this.findGatewaysAndMapType(trimmedQuery); + const applicationPromise = this.findApplicationsAndMapType(req, trimmedQuery); + const devicePromise = this.findDevicesAndMapType(req, trimmedQuery); + + const results = _.filter( + _.flatMap(await Promise.all([applicationPromise, devicePromise, gatewayPromise])), + x => x != null + ); + + return { + data: this.limitAndOrder(results, limit, offset), + count: results.length, + }; + } + + private findDevicesAndMapType(req: AuthenticatedRequest, trimmedQuery: string) { + return this.findIoTDevices(req, trimmedQuery) + .then(x => { + return this.addTypeToResults(x, SearchResultType.IoTDevice); + }) + .catch(err => { + this.logger.error(`Failed to search for IOTDevice, error: ${err}`); + }); + } + + private findApplicationsAndMapType(req: AuthenticatedRequest, trimmedQuery: string) { + return this.findApplications(req, trimmedQuery) + .then(x => { + return this.addTypeToResults(x, SearchResultType.Application); + }) + .catch(err => { + this.logger.error(`Failed to search for Application, error: ${err}`); + }); + } + + private findGatewaysAndMapType(trimmedQuery: string) { + return this.findGateways(trimmedQuery) + .then(x => { + return this.addTypeToResults(x, SearchResultType.Gateway); + }) + .catch(err => { + this.logger.error(`Failed to search for Gateway, error: ${err}`); + }); + } + + private limitAndOrder(data: SearchResultDto[], limit: number, offset: number): SearchResultDto[] { + const r = _.orderBy(data, ["updatedAt"], ["desc"]); + const sliced = _.slice(r, offset, offset + limit); + return sliced; + } + + private async findGateways(trimmedQuery: string): Promise { + const gatewayClient = new GatewayServiceClient(this.gatewayService.baseUrlGRPC, credentials.createInsecure()); + const escapedQuery = encodeURI(trimmedQuery); + const req = new ListGatewaysRequest(); + + req.setSearch(escapedQuery); + + const gateways = await this.gatewayService.getAllWithPagination( + `gateways`, + gatewayClient, + req, + 1000, + 0 + ); + + const mapped = await Promise.all( + gateways.resultList.map(async x => { + const createdAt = timestampToDate(x.createdAt); + const updatedAt = timestampToDate(x.updatedAt); + + const resultDto = new SearchResultDto(x.name, x.gatewayId, createdAt, updatedAt, x.gatewayId); + const detailedInfo = await this.gatewayService.getOne(x.gatewayId); + + resultDto.organizationId = detailedInfo.gateway.organizationId; + return resultDto; + }) + ); + + return mapped; + } + + private addTypeToResults(data: SearchResultDto[], type: SearchResultType) { + data.forEach(x => { + x.type = type; + }); + return data; + } + + private async findApplications(req: AuthenticatedRequest, trimmedQuery: string): Promise { + const qb = this.applicationRepository + .createQueryBuilder("app") + .where('"app"."name" ilike :name', { name: `%${trimmedQuery}%` }); + + return await this.applySecuityAndSelect(req, qb, "app", "id"); + } + + private async findIoTDevices(req: AuthenticatedRequest, query: string): Promise { + if (isHexadecimal(query)) { + if (query.length == 16) { + // LoRaWAN + return await this.findIoTDevicesByNameOrLoRaWANDeviceID(req, query); + } else if (query.length >= 4 && query.length <= 12) { + // Sigfox + return await this.findIoTDevicesByNameOrSigFoxDeviceId(req, query); + } + } else if (isUUID(query)) { + // Generic + return await this.findIoTDevicesByNameOrGenericHttpApiKey(req, query); } - private findDevicesAndMapType(req: AuthenticatedRequest, trimmedQuery: string) { - return this.findIoTDevices(req, trimmedQuery) - .then(x => { - return this.addTypeToResults(x, SearchResultType.IoTDevice); - }) - .catch(err => { - this.logger.error(`Failed to search for IOTDevice, error: ${err}`); - }); + return await this.findIoTDevicesByName(req, query); + } + + private async findIoTDevicesByNameOrGenericHttpApiKey( + req: AuthenticatedRequest, + query: string + ): Promise { + const qb = this.getIoTDeviceQueryBuilder().where(`((device.name ilike :name) OR ("device"."apiKey" ilike :id))`, { + name: `%${query}%`, + id: query, + }); + + return this.applySecuityAndSelect(req, qb, "device", "applicationId"); + } + + private async findIoTDevicesByNameOrSigFoxDeviceId( + req: AuthenticatedRequest, + query: string + ): Promise { + const qb = this.getIoTDeviceQueryBuilder().where(`((device.name ilike :name) OR ("device"."deviceId" ilike :id))`, { + name: `%${query}%`, + id: query, + }); + + return this.applySecuityAndSelect(req, qb, "device", "applicationId"); + } + + private async findIoTDevicesByNameOrLoRaWANDeviceID( + req: AuthenticatedRequest, + query: string + ): Promise { + const qb = this.getIoTDeviceQueryBuilder().where( + `((device.name ilike :name) OR ("device"."deviceEUI" ilike :id))`, + { + name: `%${query}%`, + id: query, + } + ); + + return this.applySecuityAndSelect(req, qb, "device", "applicationId"); + } + + private async findIoTDevicesByName(req: AuthenticatedRequest, query: string): Promise { + const qb = this.getIoTDeviceQueryBuilder().where(`device.name ilike :name`, { + name: `%${query}%`, + }); + + return this.applySecuityAndSelect(req, qb, "device", "applicationId"); + } + + private getIoTDeviceQueryBuilder() { + return this.iotDeviceRepository.createQueryBuilder("device"); + } + + private async applySecuityAndSelect( + req: AuthenticatedRequest, + qb: SelectQueryBuilder, + alias: string, + applicationIdColumn: string + ): Promise { + if (!req.user.permissions.isGlobalAdmin) { + if (req.user.permissions.getAllApplicationsWithAtLeastRead().length == 0) { + return []; + } + qb = qb.andWhere(`"${alias}"."${applicationIdColumn}" IN (:...allowedApplications)`, { + allowedApplications: req.user.permissions.getAllApplicationsWithAtLeastRead(), + }); } - private findApplicationsAndMapType(req: AuthenticatedRequest, trimmedQuery: string) { - return this.findApplications(req, trimmedQuery) - .then(x => { - return this.addTypeToResults(x, SearchResultType.Application); - }) - .catch(err => { - this.logger.error(`Failed to search for Application, error: ${err}`); - }); - } - - private findGatewaysAndMapType(trimmedQuery: string) { - return this.findGateways(trimmedQuery) - .then(x => { - return this.addTypeToResults(x, SearchResultType.Gateway); - }) - .catch(err => { - this.logger.error(`Failed to search for Gateway, error: ${err}`); - }); - } - - private limitAndOrder(data: SearchResultDto[], limit: number, offset: number): SearchResultDto[] { - const r = _.orderBy(data, ["updatedAt"], ["desc"]); - const sliced = _.slice(r, offset, offset + limit); - return sliced; - } - - private async findGateways(trimmedQuery: string): Promise { - const gatewayClient = new GatewayServiceClient(this.gatewayService.baseUrlGRPC, credentials.createInsecure()); - const escapedQuery = encodeURI(trimmedQuery); - const req = new ListGatewaysRequest(); - - req.setSearch(escapedQuery); - - const gateways = await this.gatewayService.getAllWithPagination( - `gateways`, - gatewayClient, - req, - 1000, - 0 - ); - - const mapped = await Promise.all( - gateways.resultList.map(async x => { - const createdAt = timestampToDate(x.createdAt); - const updatedAt = timestampToDate(x.updatedAt); - - const resultDto = new SearchResultDto(x.name, x.gatewayId, createdAt, updatedAt, x.gatewayId); - const detailedInfo = await this.gatewayService.getOne(x.gatewayId); - - resultDto.organizationId = detailedInfo.gateway.organizationId; - return resultDto; - }) - ); - - return mapped; - } - - private addTypeToResults(data: SearchResultDto[], type: SearchResultType) { - data.forEach(x => { - x.type = type; - }); - return data; - } - - private async findApplications(req: AuthenticatedRequest, trimmedQuery: string): Promise { - const qb = this.applicationRepository - .createQueryBuilder("app") - .where('"app"."name" ilike :name', { name: `%${trimmedQuery}%` }); - - return await this.applySecuityAndSelect(req, qb, "app", "id"); - } - - private async findIoTDevices(req: AuthenticatedRequest, query: string): Promise { - if (isHexadecimal(query)) { - if (query.length == 16) { - // LoRaWAN - return await this.findIoTDevicesByNameOrLoRaWANDeviceID(req, query); - } else if (query.length >= 4 && query.length <= 12) { - // Sigfox - return await this.findIoTDevicesByNameOrSigFoxDeviceId(req, query); - } - } else if (isUUID(query)) { - // Generic - return await this.findIoTDevicesByNameOrGenericHttpApiKey(req, query); - } - - return await this.findIoTDevicesByName(req, query); - } - - private async findIoTDevicesByNameOrGenericHttpApiKey( - req: AuthenticatedRequest, - query: string - ): Promise { - const qb = this.getIoTDeviceQueryBuilder().where( - `((device.name ilike :name) OR ("device"."apiKey" ilike :id))`, - { - name: `%${query}%`, - id: query, - } - ); - - return this.applySecuityAndSelect(req, qb, "device", "applicationId"); - } - - private async findIoTDevicesByNameOrSigFoxDeviceId( - req: AuthenticatedRequest, - query: string - ): Promise { - const qb = this.getIoTDeviceQueryBuilder().where( - `((device.name ilike :name) OR ("device"."deviceId" ilike :id))`, - { - name: `%${query}%`, - id: query, - } - ); - - return this.applySecuityAndSelect(req, qb, "device", "applicationId"); - } - - private async findIoTDevicesByNameOrLoRaWANDeviceID( - req: AuthenticatedRequest, - query: string - ): Promise { - const qb = this.getIoTDeviceQueryBuilder().where( - `((device.name ilike :name) OR ("device"."deviceEUI" ilike :id))`, - { - name: `%${query}%`, - id: query, - } - ); - - return this.applySecuityAndSelect(req, qb, "device", "applicationId"); - } - - private async findIoTDevicesByName(req: AuthenticatedRequest, query: string): Promise { - const qb = this.getIoTDeviceQueryBuilder().where(`device.name ilike :name`, { - name: `%${query}%`, - }); - - return this.applySecuityAndSelect(req, qb, "device", "applicationId"); - } - - private getIoTDeviceQueryBuilder() { - return this.iotDeviceRepository.createQueryBuilder("device"); - } - - private async applySecuityAndSelect( - req: AuthenticatedRequest, - qb: SelectQueryBuilder, - alias: string, - applicationIdColumn: string - ): Promise { - if (!req.user.permissions.isGlobalAdmin) { - if (req.user.permissions.getAllApplicationsWithAtLeastRead().length == 0) { - return []; - } - qb = qb.andWhere(`"${alias}"."${applicationIdColumn}" IN (:...allowedApplications)`, { - allowedApplications: req.user.permissions.getAllApplicationsWithAtLeastRead(), - }); - } - - const toSelect = [`"${alias}"."id"`, `"${alias}"."createdAt"`, `"${alias}"."updatedAt"`, `"${alias}"."name"`]; - const select = qb; - if (alias == "device") { - return select - .select(toSelect.concat(['"deviceId"', '"deviceEUI"', '"apiKey"'])) - .addSelect('"type"', "deviceType") - .addSelect('"applicationId"', "applicationId") - .leftJoin("application", "app", '"app"."id" = "device"."applicationId"') - .addSelect('"app"."belongsToId"', "organizationId") - .getRawMany(); - } else if (alias == "app") { - return select.select(toSelect).addSelect('"app"."belongsToId"', "organizationId").getRawMany(); - } + const toSelect = [`"${alias}"."id"`, `"${alias}"."createdAt"`, `"${alias}"."updatedAt"`, `"${alias}"."name"`]; + const select = qb; + if (alias == "device") { + return select + .select(toSelect.concat(['"deviceId"', '"deviceEUI"', '"apiKey"'])) + .addSelect('"type"', "deviceType") + .addSelect('"applicationId"', "applicationId") + .leftJoin("application", "app", '"app"."id" = "device"."applicationId"') + .addSelect('"app"."belongsToId"', "organizationId") + .getRawMany(); + } else if (alias == "app") { + return select.select(toSelect).addSelect('"app"."belongsToId"', "organizationId").getRawMany(); } + } } diff --git a/src/services/data-targets/base-data-target.service.ts b/src/services/data-targets/base-data-target.service.ts index 24f7ea3c..63975586 100644 --- a/src/services/data-targets/base-data-target.service.ts +++ b/src/services/data-targets/base-data-target.service.ts @@ -8,24 +8,20 @@ import { DataTarget } from "@entities/data-target.entity"; * This class exposes general functionality used for the DataTarget */ export abstract class BaseDataTargetService { - protected readonly baseLogger = new Logger(BaseDataTargetService.name); + protected readonly baseLogger = new Logger(BaseDataTargetService.name); - success(receiver: string): DataTargetSendStatus { - this.baseLogger.debug(`Send to ${receiver} sucessful!`); - return { status: SendStatus.OK }; - } + success(receiver: string): DataTargetSendStatus { + this.baseLogger.debug(`Send to ${receiver} sucessful!`); + return { status: SendStatus.OK }; + } - failure( - receiver: string, - errorMessage: string, - dataTarget: DataTarget - ): DataTargetSendStatus { - this.baseLogger.error( - `Datatarget {Id: ${dataTarget.id}, Name: ${dataTarget.name}} Send to ${receiver} failed with error ${errorMessage}` - ); - return { - status: SendStatus.ERROR, - errorMessage: errorMessage.toString(), - }; - } + failure(receiver: string, errorMessage: string, dataTarget: DataTarget): DataTargetSendStatus { + this.baseLogger.error( + `Datatarget {Id: ${dataTarget.id}, Name: ${dataTarget.name}} Send to ${receiver} failed with error ${errorMessage}` + ); + return { + status: SendStatus.ERROR, + errorMessage: errorMessage.toString(), + }; + } } diff --git a/src/services/data-targets/data-target-kafka-listener.service.ts b/src/services/data-targets/data-target-kafka-listener.service.ts index 00e24547..b2ffc43e 100644 --- a/src/services/data-targets/data-target-kafka-listener.service.ts +++ b/src/services/data-targets/data-target-kafka-listener.service.ts @@ -19,85 +19,85 @@ const UNIQUE_NAME_FOR_KAFKA = "DataTargetKafka"; @Injectable() export class DataTargetKafkaListenerService extends AbstractKafkaConsumer { - constructor( - private ioTDeviceService: IoTDeviceService, - private dataTargetService: DataTargetService, - private httpPushDataTargetService: HttpPushDataTargetService, - private fiwareDataTargetService: FiwareDataTargetService, - private mqttDataTargetService: MqttDataTargetService, - private ioTDevicePayloadDecoderDataTargetConnectionService: IoTDevicePayloadDecoderDataTargetConnectionService - ) { - super(); - } - private readonly logger = new Logger(DataTargetKafkaListenerService.name); + constructor( + private ioTDeviceService: IoTDeviceService, + private dataTargetService: DataTargetService, + private httpPushDataTargetService: HttpPushDataTargetService, + private fiwareDataTargetService: FiwareDataTargetService, + private mqttDataTargetService: MqttDataTargetService, + private ioTDevicePayloadDecoderDataTargetConnectionService: IoTDevicePayloadDecoderDataTargetConnectionService + ) { + super(); + } + private readonly logger = new Logger(DataTargetKafkaListenerService.name); - protected registerTopic(): void { - this.addTopic(KafkaTopic.TRANSFORMED_REQUEST, UNIQUE_NAME_FOR_KAFKA); - } + protected registerTopic(): void { + this.addTopic(KafkaTopic.TRANSFORMED_REQUEST, UNIQUE_NAME_FOR_KAFKA); + } - @CombinedSubscribeTo(KafkaTopic.TRANSFORMED_REQUEST, UNIQUE_NAME_FOR_KAFKA) - async transformedRequestListener(payload: KafkaPayload): Promise { - this.logger.debug(`TRANSFORMED_REQUEST: '${JSON.stringify(payload)}'`); + @CombinedSubscribeTo(KafkaTopic.TRANSFORMED_REQUEST, UNIQUE_NAME_FOR_KAFKA) + async transformedRequestListener(payload: KafkaPayload): Promise { + this.logger.debug(`TRANSFORMED_REQUEST: '${JSON.stringify(payload)}'`); - const dto = payload.body as TransformedPayloadDto; - let iotDevice: IoTDevice; - try { - iotDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); - } catch (err) { - this.logger.error(`Error finding IoTDevice by id: ${dto.iotDeviceId}. Stopping.`); - return; - } + const dto = payload.body as TransformedPayloadDto; + let iotDevice: IoTDevice; + try { + iotDevice = await this.ioTDeviceService.findOne(dto.iotDeviceId); + } catch (err) { + this.logger.error(`Error finding IoTDevice by id: ${dto.iotDeviceId}. Stopping.`); + return; + } - this.logger.debug(`Sending payload from deviceId: ${iotDevice.id}; Name: '${iotDevice.name}'`); + this.logger.debug(`Sending payload from deviceId: ${iotDevice.id}; Name: '${iotDevice.name}'`); - await this.findDataTargetsAndSend(iotDevice, dto); - } + await this.findDataTargetsAndSend(iotDevice, dto); + } - private async findDataTargetsAndSend(iotDevice: IoTDevice, dto: TransformedPayloadDto) { - // Get connections in order to only send to the dataTargets which is identified by the pair of IoTDevice and PayloadDecoder - const dataTargets = await this.dataTargetService.findDataTargetsByConnectionPayloadDecoderAndIoTDevice( - iotDevice.id, - dto.payloadDecoderId - ); + private async findDataTargetsAndSend(iotDevice: IoTDevice, dto: TransformedPayloadDto) { + // Get connections in order to only send to the dataTargets which is identified by the pair of IoTDevice and PayloadDecoder + const dataTargets = await this.dataTargetService.findDataTargetsByConnectionPayloadDecoderAndIoTDevice( + iotDevice.id, + dto.payloadDecoderId + ); - const ids = dataTargets.map(x => x.id).join(", "); - this.logger.debug( - `Found ${dataTargets.length} datatargets to send to: [${ids}] for iotDeviceId: '${iotDevice.id}' and payloadDecoderId: '${dto.payloadDecoderId}'` - ); - this.sendToDataTargets(dataTargets, dto); - } + const ids = dataTargets.map(x => x.id).join(", "); + this.logger.debug( + `Found ${dataTargets.length} datatargets to send to: [${ids}] for iotDeviceId: '${iotDevice.id}' and payloadDecoderId: '${dto.payloadDecoderId}'` + ); + this.sendToDataTargets(dataTargets, dto); + } - private sendToDataTargets(dataTargets: DataTarget[], dto: TransformedPayloadDto) { - dataTargets.forEach(async target => { - if (target.type == DataTargetType.HttpPush) { - try { - const status = await this.httpPushDataTargetService.send(target, dto); - this.logger.debug(`Sent to HttpPush target: ${JSON.stringify(status)}`); - } catch (err) { - this.logger.error(`Error while sending to Http Push DataTarget: ${err}`); - } - } else if (target.type == DataTargetType.Fiware) { - try { - const status = await this.fiwareDataTargetService.send(target, dto); - this.logger.debug(`Sent to FIWARE target: ${JSON.stringify(status)}`); - } catch (err) { - this.logger.error(`Error while sending to FIWARE DataTarget: ${err}`); - } - } else if (target.type === DataTargetType.MQTT) { - try { - this.mqttDataTargetService.send(target, dto, this.onSendDone); - } catch (err) { - this.logger.error(`Error while sending to MQTT DataTarget: ${err}`); - } - } else if (target.type === DataTargetType.OpenDataDK) { - // OpenDataDk data targets are handled uniquely and ignored here. - } else { - this.logger.error(`Not implemented for: ${target.type}`); - } - }); - } + private sendToDataTargets(dataTargets: DataTarget[], dto: TransformedPayloadDto) { + dataTargets.forEach(async target => { + if (target.type == DataTargetType.HttpPush) { + try { + const status = await this.httpPushDataTargetService.send(target, dto); + this.logger.debug(`Sent to HttpPush target: ${JSON.stringify(status)}`); + } catch (err) { + this.logger.error(`Error while sending to Http Push DataTarget: ${err}`); + } + } else if (target.type == DataTargetType.Fiware) { + try { + const status = await this.fiwareDataTargetService.send(target, dto); + this.logger.debug(`Sent to FIWARE target: ${JSON.stringify(status)}`); + } catch (err) { + this.logger.error(`Error while sending to FIWARE DataTarget: ${err}`); + } + } else if (target.type === DataTargetType.MQTT) { + try { + this.mqttDataTargetService.send(target, dto, this.onSendDone); + } catch (err) { + this.logger.error(`Error while sending to MQTT DataTarget: ${err}`); + } + } else if (target.type === DataTargetType.OpenDataDK) { + // OpenDataDk data targets are handled uniquely and ignored here. + } else { + this.logger.error(`Not implemented for: ${target.type}`); + } + }); + } - private onSendDone = (status: DataTargetSendStatus, targetType: DataTargetType) => { - this.logger.debug(`Sent to ${targetType} target: ${JSON.stringify(status)}`); - }; + private onSendDone = (status: DataTargetSendStatus, targetType: DataTargetType) => { + this.logger.debug(`Sent to ${targetType} target: ${JSON.stringify(status)}`); + }; } diff --git a/src/services/data-targets/data-target.service.ts b/src/services/data-targets/data-target.service.ts index aea365d3..cee648f7 100644 --- a/src/services/data-targets/data-target.service.ts +++ b/src/services/data-targets/data-target.service.ts @@ -12,293 +12,253 @@ import { OpenDataDkDataset } from "@entities/open-data-dk-dataset.entity"; import { dataTargetTypeMap } from "@enum/data-target-type-mapping"; import { DataTargetType } from "@enum/data-target-type.enum"; import { ErrorCodes } from "@enum/error-codes.enum"; -import { - BadRequestException, - forwardRef, - Inject, - Injectable, - Logger, -} from "@nestjs/common"; +import { BadRequestException, forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { ApplicationService } from "@services/device-management/application.service"; import { OS2IoTMail } from "@services/os2iot-mail.service"; import { DeleteResult, Repository, SelectQueryBuilder } from "typeorm"; -import { - CLIENT_SECRET_PROVIDER, - ClientSecretProvider, -} from "../../helpers/fiware-token.helper"; +import { CLIENT_SECRET_PROVIDER, ClientSecretProvider } from "../../helpers/fiware-token.helper"; import { User } from "@entities/user.entity"; @Injectable() export class DataTargetService { - constructor( - @InjectRepository(DataTarget) - private dataTargetRepository: Repository, - @InjectRepository(User) - private userRepository: Repository, - @Inject(forwardRef(() => ApplicationService)) - private applicationService: ApplicationService, - @Inject(CLIENT_SECRET_PROVIDER) - private clientSecretProvider: ClientSecretProvider, - private oS2IoTMail: OS2IoTMail - ) {} - private readonly logger = new Logger(DataTargetService.name); + constructor( + @InjectRepository(DataTarget) + private dataTargetRepository: Repository, + @InjectRepository(User) + private userRepository: Repository, + @Inject(forwardRef(() => ApplicationService)) + private applicationService: ApplicationService, + @Inject(CLIENT_SECRET_PROVIDER) + private clientSecretProvider: ClientSecretProvider, + private oS2IoTMail: OS2IoTMail + ) {} + private readonly logger = new Logger(DataTargetService.name); - async findAndCountAllWithPagination( - query?: ListAllDataTargetsDto, - applicationIds?: number[] - ): Promise { - let queryBuilder = this.dataTargetRepository - .createQueryBuilder("datatarget") - .innerJoinAndSelect("datatarget.application", "application") - .limit(query.limit) - .offset(query.offset) - .orderBy(query.orderOn, "ASC"); + async findAndCountAllWithPagination( + query?: ListAllDataTargetsDto, + applicationIds?: number[] + ): Promise { + let queryBuilder = this.dataTargetRepository + .createQueryBuilder("datatarget") + .innerJoinAndSelect("datatarget.application", "application") + .limit(query.limit) + .offset(query.offset) + .orderBy(query.orderOn, "ASC"); - // Only apply applicationId filter, if one is given. - queryBuilder = this.filterByApplication(query, queryBuilder, applicationIds); + // Only apply applicationId filter, if one is given. + queryBuilder = this.filterByApplication(query, queryBuilder, applicationIds); - const [result, total] = await queryBuilder.getManyAndCount(); + const [result, total] = await queryBuilder.getManyAndCount(); - return { - data: result, - count: total, - }; - } + return { + data: result, + count: total, + }; + } - private filterByApplication( - query: ListAllDataTargetsDto, - queryBuilder: SelectQueryBuilder, - applicationIds: number[] - ) { - if (query.applicationId) { - queryBuilder = queryBuilder.where("datatarget.application = :appId", { - appId: query.applicationId, - }); - } else if (applicationIds) { - queryBuilder = queryBuilder.where( - '"application"."id" IN (:...allowedApplications)', - { - allowedApplications: applicationIds, - } - ); - } - return queryBuilder; + private filterByApplication( + query: ListAllDataTargetsDto, + queryBuilder: SelectQueryBuilder, + applicationIds: number[] + ) { + if (query.applicationId) { + queryBuilder = queryBuilder.where("datatarget.application = :appId", { + appId: query.applicationId, + }); + } else if (applicationIds) { + queryBuilder = queryBuilder.where('"application"."id" IN (:...allowedApplications)', { + allowedApplications: applicationIds, + }); } + return queryBuilder; + } - async findOne(id: number): Promise { - return await this.dataTargetRepository.findOneOrFail({ - where: { id }, - relations: ["application", "openDataDkDataset"], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - } + async findOne(id: number): Promise { + return await this.dataTargetRepository.findOneOrFail({ + where: { id }, + relations: ["application", "openDataDkDataset"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } - async findDataTargetsByApplicationId(applicationId: number): Promise { - return await this.dataTargetRepository.findBy({ - application: { id: applicationId }, - }); - } + async findDataTargetsByApplicationId(applicationId: number): Promise { + return await this.dataTargetRepository.findBy({ + application: { id: applicationId }, + }); + } - async findDataTargetsByConnectionPayloadDecoderAndIoTDevice( - iotDeviceId: number, - payloadDecoderId?: number - ): Promise { - const res = await this.dataTargetRepository - .createQueryBuilder("dt") - .addSelect("dt.clientSecret") - .innerJoin( - "iot_device_payload_decoder_data_target_connection", - "con", - 'con."dataTargetId" = dt.id' - ) - .innerJoin( - "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev", - "jt", - 'jt."iotDevicePayloadDecoderDataTargetConnectionId" = con.id' - ) - .where('jt."iotDeviceId" = :iotDeviceId', { iotDeviceId: iotDeviceId }); - if (payloadDecoderId === null) { - return res.andWhere('con."payloadDecoderId" is null').getMany(); - } else { - return res - .andWhere('con."payloadDecoderId" = :decoderId', { - decoderId: payloadDecoderId, - }) - .getMany(); - } + async findDataTargetsByConnectionPayloadDecoderAndIoTDevice( + iotDeviceId: number, + payloadDecoderId?: number + ): Promise { + const res = await this.dataTargetRepository + .createQueryBuilder("dt") + .addSelect("dt.clientSecret") + .innerJoin("iot_device_payload_decoder_data_target_connection", "con", 'con."dataTargetId" = dt.id') + .innerJoin( + "iot_dev_pay_dec_dat_tar_con_iot_dev_iot_dev", + "jt", + 'jt."iotDevicePayloadDecoderDataTargetConnectionId" = con.id' + ) + .where('jt."iotDeviceId" = :iotDeviceId', { iotDeviceId: iotDeviceId }); + if (payloadDecoderId === null) { + return res.andWhere('con."payloadDecoderId" is null').getMany(); + } else { + return res + .andWhere('con."payloadDecoderId" = :decoderId', { + decoderId: payloadDecoderId, + }) + .getMany(); } + } - async create( - createDataTargetDto: CreateDataTargetDto, - userId: number - ): Promise { - const childType = dataTargetTypeMap[createDataTargetDto.type]; + async create(createDataTargetDto: CreateDataTargetDto, userId: number): Promise { + const childType = dataTargetTypeMap[createDataTargetDto.type]; - const dataTarget = this.createDataTargetByDto(childType); - const mappedDataTarget = await this.mapDtoToDataTarget( - createDataTargetDto, - dataTarget - ); + const dataTarget = this.createDataTargetByDto(childType); + const mappedDataTarget = await this.mapDtoToDataTarget(createDataTargetDto, dataTarget); - if (createDataTargetDto.openDataDkDataset) { - dataTarget.openDataDkDataset = new OpenDataDkDataset(); - mappedDataTarget.openDataDkDataset = this.mapOpenDataDk( - createDataTargetDto.openDataDkDataset, - dataTarget.openDataDkDataset - ); - mappedDataTarget.openDataDkDataset.createdBy = userId; - mappedDataTarget.openDataDkDataset.updatedBy = userId; - } else { - mappedDataTarget.openDataDkDataset = null; - } - - mappedDataTarget.createdBy = userId; - mappedDataTarget.updatedBy = userId; - // Use the generic manager since we cannot use a general repository. - const entityManager = this.dataTargetRepository.manager; - return await entityManager.save(mappedDataTarget, {}); + if (createDataTargetDto.openDataDkDataset) { + dataTarget.openDataDkDataset = new OpenDataDkDataset(); + mappedDataTarget.openDataDkDataset = this.mapOpenDataDk( + createDataTargetDto.openDataDkDataset, + dataTarget.openDataDkDataset + ); + mappedDataTarget.openDataDkDataset.createdBy = userId; + mappedDataTarget.openDataDkDataset.updatedBy = userId; + } else { + mappedDataTarget.openDataDkDataset = null; } - async update( - id: number, - updateDataTargetDto: UpdateDataTargetDto, - userId: number - ): Promise { - const existing = await this.dataTargetRepository.createQueryBuilder('target') - .addSelect('target.clientSecret') - .leftJoinAndSelect('target.openDataDkDataset', 'openDataDkDataset') - .where('target.id = :id', { id }) - .getOneOrFail(); - - const mappedDataTarget = await this.mapDtoToDataTarget( - updateDataTargetDto, - existing - ); + mappedDataTarget.createdBy = userId; + mappedDataTarget.updatedBy = userId; + // Use the generic manager since we cannot use a general repository. + const entityManager = this.dataTargetRepository.manager; + return await entityManager.save(mappedDataTarget, {}); + } - if (updateDataTargetDto.openDataDkDataset) { - if (existing.openDataDkDataset == null) { - existing.openDataDkDataset = new OpenDataDkDataset(); - } - mappedDataTarget.openDataDkDataset = this.mapOpenDataDk( - updateDataTargetDto.openDataDkDataset, - existing.openDataDkDataset - ); - mappedDataTarget.openDataDkDataset.updatedBy = userId; - } else { - mappedDataTarget.openDataDkDataset = null; - } - mappedDataTarget.updatedBy = userId; - const res = this.dataTargetRepository.save(mappedDataTarget); + async update(id: number, updateDataTargetDto: UpdateDataTargetDto, userId: number): Promise { + const existing = await this.dataTargetRepository + .createQueryBuilder("target") + .addSelect("target.clientSecret") + .leftJoinAndSelect("target.openDataDkDataset", "openDataDkDataset") + .where("target.id = :id", { id }) + .getOneOrFail(); - return res; - } + const mappedDataTarget = await this.mapDtoToDataTarget(updateDataTargetDto, existing); - async delete(id: number): Promise { - return this.dataTargetRepository.delete(id); + if (updateDataTargetDto.openDataDkDataset) { + if (existing.openDataDkDataset == null) { + existing.openDataDkDataset = new OpenDataDkDataset(); + } + mappedDataTarget.openDataDkDataset = this.mapOpenDataDk( + updateDataTargetDto.openDataDkDataset, + existing.openDataDkDataset + ); + mappedDataTarget.openDataDkDataset.updatedBy = userId; + } else { + mappedDataTarget.openDataDkDataset = null; } + mappedDataTarget.updatedBy = userId; + const res = this.dataTargetRepository.save(mappedDataTarget); - private async mapDtoToDataTarget( - dataTargetDto: CreateDataTargetDto, - dataTarget: DataTarget - ): Promise { - dataTarget.name = dataTargetDto.name; - if (dataTargetDto.applicationId != null) { - try { - dataTarget.application = await this.applicationService.findOneWithoutRelations( - dataTargetDto.applicationId - ); - } catch (err) { - this.logger.error( - `Could not find application with id: ${dataTargetDto.applicationId}` - ); + return res; + } - throw new BadRequestException(ErrorCodes.IdDoesNotExists); - } - } else { - throw new BadRequestException(ErrorCodes.IdMissing); - } + async delete(id: number): Promise { + return this.dataTargetRepository.delete(id); + } - await this.mapDtoToTypeSpecificDataTarget(dataTargetDto, dataTarget); + private async mapDtoToDataTarget(dataTargetDto: CreateDataTargetDto, dataTarget: DataTarget): Promise { + dataTarget.name = dataTargetDto.name; + if (dataTargetDto.applicationId != null) { + try { + dataTarget.application = await this.applicationService.findOneWithoutRelations(dataTargetDto.applicationId); + } catch (err) { + this.logger.error(`Could not find application with id: ${dataTargetDto.applicationId}`); - return dataTarget; + throw new BadRequestException(ErrorCodes.IdDoesNotExists); + } + } else { + throw new BadRequestException(ErrorCodes.IdMissing); } - private mapOpenDataDk( - dto: CreateOpenDataDkDatasetDto, - o: OpenDataDkDataset - ): OpenDataDkDataset { - o.name = dto.name; - o.license = dto.license; - o.authorName = dto.authorName; - o.authorEmail = dto.authorEmail; + await this.mapDtoToTypeSpecificDataTarget(dataTargetDto, dataTarget); - o.description = dto.description; - o.keywords = dto.keywords; - o.resourceTitle = dto.resourceTitle; - return o; - } + return dataTarget; + } - private async mapDtoToTypeSpecificDataTarget( - dataTargetDto: CreateDataTargetDto, - dataTarget: DataTarget - ) { - if (dataTargetDto.type === DataTargetType.HttpPush) { - const httpPushDataTarget = dataTarget as HttpPushDataTarget; - httpPushDataTarget.url = dataTargetDto.url; - httpPushDataTarget.timeout = dataTargetDto.timeout; - httpPushDataTarget.authorizationHeader = dataTargetDto.authorizationHeader; - } else if (dataTargetDto.type === DataTargetType.Fiware) { - const fiwareDataTarget = dataTarget as FiwareDataTarget; - fiwareDataTarget.url = dataTargetDto.url; - fiwareDataTarget.timeout = dataTargetDto.timeout; - fiwareDataTarget.authorizationHeader = dataTargetDto.authorizationHeader; - fiwareDataTarget.tokenEndpoint = dataTargetDto.tokenEndpoint; - fiwareDataTarget.clientId = dataTargetDto.clientId; + private mapOpenDataDk(dto: CreateOpenDataDkDatasetDto, o: OpenDataDkDataset): OpenDataDkDataset { + o.name = dto.name; + o.license = dto.license; + o.authorName = dto.authorName; + o.authorEmail = dto.authorEmail; - // NOTE: If there is no client secret we keep it as it was - if (dataTargetDto.clientSecret) { - fiwareDataTarget.clientSecret = await this.clientSecretProvider.store(dataTargetDto.clientSecret); - } - fiwareDataTarget.tenant = dataTargetDto.tenant; - fiwareDataTarget.context = dataTargetDto.context; - } else if (dataTargetDto.type === DataTargetType.MQTT) { - const mqttTarget = dataTarget as MqttDataTarget; - mqttTarget.url = dataTargetDto.url; - mqttTarget.timeout = dataTargetDto.timeout; - mqttTarget.mqttPort = dataTargetDto.mqttPort; - mqttTarget.mqttTopic = dataTargetDto.mqttTopic; - mqttTarget.mqttQos = dataTargetDto.mqttQos; - mqttTarget.mqttUsername = dataTargetDto.mqttUsername; - mqttTarget.mqttPassword = dataTargetDto.mqttPassword; - } - } + o.description = dto.description; + o.keywords = dto.keywords; + o.resourceTitle = dto.resourceTitle; + return o; + } + + private async mapDtoToTypeSpecificDataTarget(dataTargetDto: CreateDataTargetDto, dataTarget: DataTarget) { + if (dataTargetDto.type === DataTargetType.HttpPush) { + const httpPushDataTarget = dataTarget as HttpPushDataTarget; + httpPushDataTarget.url = dataTargetDto.url; + httpPushDataTarget.timeout = dataTargetDto.timeout; + httpPushDataTarget.authorizationHeader = dataTargetDto.authorizationHeader; + } else if (dataTargetDto.type === DataTargetType.Fiware) { + const fiwareDataTarget = dataTarget as FiwareDataTarget; + fiwareDataTarget.url = dataTargetDto.url; + fiwareDataTarget.timeout = dataTargetDto.timeout; + fiwareDataTarget.authorizationHeader = dataTargetDto.authorizationHeader; + fiwareDataTarget.tokenEndpoint = dataTargetDto.tokenEndpoint; + fiwareDataTarget.clientId = dataTargetDto.clientId; - private createDataTargetByDto(childDataTargetType: any): T { - return new childDataTargetType(); + // NOTE: If there is no client secret we keep it as it was + if (dataTargetDto.clientSecret) { + fiwareDataTarget.clientSecret = await this.clientSecretProvider.store(dataTargetDto.clientSecret); + } + fiwareDataTarget.tenant = dataTargetDto.tenant; + fiwareDataTarget.context = dataTargetDto.context; + } else if (dataTargetDto.type === DataTargetType.MQTT) { + const mqttTarget = dataTarget as MqttDataTarget; + mqttTarget.url = dataTargetDto.url; + mqttTarget.timeout = dataTargetDto.timeout; + mqttTarget.mqttPort = dataTargetDto.mqttPort; + mqttTarget.mqttTopic = dataTargetDto.mqttTopic; + mqttTarget.mqttQos = dataTargetDto.mqttQos; + mqttTarget.mqttUsername = dataTargetDto.mqttUsername; + mqttTarget.mqttPassword = dataTargetDto.mqttPassword; } + } - public async sendOpenDataDkMail( - mailInfoDto: OddkMailInfo, - userId: number, - ): Promise { - const user = await this.userRepository.findOneByOrFail({id: userId}); - await this.oS2IoTMail.sendMailChecked({ - to: "info@opendata.dk", - subject: "Ny integration til OS2IoT", - html: `

+ private createDataTargetByDto(childDataTargetType: any): T { + return new childDataTargetType(); + } + + public async sendOpenDataDkMail(mailInfoDto: OddkMailInfo, userId: number): Promise { + const user = await this.userRepository.findOneByOrFail({ id: userId }); + await this.oS2IoTMail.sendMailChecked({ + to: "info@opendata.dk", + subject: "Ny integration til OS2IoT", + html: + `

Hej Open Data DK,

Vi har oprettet en integration fra vores organisation i OS2iot til Open Data DK, som I gerne må begynde at høste.
I kan høste fra ${mailInfoDto.sharingUrl}
Vores data skal knyttes til følgende organisation på opendata.dk: ${mailInfoDto.organizationOddkAlias}
-
` - + (mailInfoDto.comment ? ('Kommentar: ' + mailInfoDto.comment + '

') : '') - + 'Mvh.
' + user.name + '
' + user.email - + '

', - }); - return true; - } +
` + + (mailInfoDto.comment ? "Kommentar: " + mailInfoDto.comment + "

" : "") + + "Mvh.
" + + user.name + + "
" + + user.email + + "

", + }); + return true; + } } diff --git a/src/services/data-targets/fiware-data-target.service.ts b/src/services/data-targets/fiware-data-target.service.ts index da7227f7..6ed81d7e 100644 --- a/src/services/data-targets/fiware-data-target.service.ts +++ b/src/services/data-targets/fiware-data-target.service.ts @@ -14,133 +14,102 @@ import { SendStatus } from "../../entities/enum/send-status.enum"; @Injectable() export class FiwareDataTargetService extends BaseDataTargetService { - constructor( - private httpService: HttpService, - private authenticationTokenProvider: AuthenticationTokenProvider - ) { - super(); + constructor(private httpService: HttpService, private authenticationTokenProvider: AuthenticationTokenProvider) { + super(); + } + + protected readonly logger = new Logger(FiwareDataTargetService.name); + + // eslint-disable-next-line max-lines-per-function + async send(datatarget: DataTarget, dto: TransformedPayloadDto): Promise { + const config: FiwareDataTargetConfiguration = (datatarget as FiwareDataTarget).toConfiguration(); + + // NOTE: For context broker secured with OAuth2 we want to have extra retry in case the cached token is expired. + const retries = config.tokenEndpoint ? 1 : 0; + + return this.retry(async () => this.sendInternal(config, dto, datatarget), retries); + } + + async sendInternal( + config: FiwareDataTargetConfiguration, + dto: TransformedPayloadDto, + dataTarget: DataTarget + ): Promise { + const endpointUrl = `${config.url}/ngsi-ld/v1/entityOperations/upsert/`; + const target = `FiwareDataTarget(${endpointUrl})`; + + try { + // Setup HTTP client + const axiosConfig = await this.makeAxiosConfiguration(config); + + const rawBody: string = JSON.stringify(dto.payload); + const result = await this.httpService.post(endpointUrl, rawBody, axiosConfig).toPromise(); + + this.logger.debug(`FiwareDataTarget result: '${JSON.stringify(result.data)}'`); + if (!result.status.toString().startsWith("2")) { + this.logger.warn(`Got a non-2xx status-code: ${result.status.toString()} and message: ${result.statusText}`); + } + return this.success(target); + } catch (err) { + this.logger.error(`FiwareDataTarget got error: ${err}`); + await this.authenticationTokenProvider.clearConfig(config); + return this.failure(target, err, dataTarget); } - - protected readonly logger = new Logger(FiwareDataTargetService.name); - - // eslint-disable-next-line max-lines-per-function - async send( - datatarget: DataTarget, - dto: TransformedPayloadDto - ): Promise { - const config: FiwareDataTargetConfiguration = ( - datatarget as FiwareDataTarget - ).toConfiguration(); - - // NOTE: For context broker secured with OAuth2 we want to have extra retry in case the cached token is expired. - const retries = config.tokenEndpoint ? 1 : 0; - - return this.retry( - async () => this.sendInternal(config, dto, datatarget), - retries - ); - } - - async sendInternal( - config: FiwareDataTargetConfiguration, - dto: TransformedPayloadDto, - dataTarget: DataTarget - ): Promise { - const endpointUrl = `${config.url}/ngsi-ld/v1/entityOperations/upsert/`; - const target = `FiwareDataTarget(${endpointUrl})`; - - try { - // Setup HTTP client - const axiosConfig = await this.makeAxiosConfiguration(config); - - const rawBody: string = JSON.stringify(dto.payload); - const result = await this.httpService - .post(endpointUrl, rawBody, axiosConfig) - .toPromise(); - - this.logger.debug( - `FiwareDataTarget result: '${JSON.stringify(result.data)}'` - ); - if (!result.status.toString().startsWith("2")) { - this.logger.warn( - `Got a non-2xx status-code: ${result.status.toString()} and message: ${ - result.statusText - }` - ); - } - return this.success(target); - } catch (err) { - this.logger.error(`FiwareDataTarget got error: ${err}`); - await this.authenticationTokenProvider.clearConfig(config); - return this.failure(target, err, dataTarget); - } + } + + async retry(action: () => Promise, retries: number): Promise { + do { + const result = await action(); + if (result.status === SendStatus.ERROR && retries > 0) { + this.logger.warn("Sending request to Fiware failed. Retrying..."); + retries--; + continue; + } else { + return result; + } + } while (true); + } + + async makeAxiosConfiguration(config: FiwareDataTargetConfiguration): Promise { + const axiosConfig: AxiosRequestConfig = { + timeout: config.timeout, + headers: this.getHeaders(config), + }; + + if (config.authorizationType !== null && config.authorizationType !== AuthorizationType.NO_AUTHORIZATION) { + if (config.authorizationType === AuthorizationType.HTTP_BASIC_AUTHORIZATION) { + axiosConfig.auth = { + username: config.username, + password: config.password, + }; + } else if (config.authorizationType === AuthorizationType.HEADER_BASED_AUTHORIZATION) { + axiosConfig.headers["Authorization"] = config.authorizationHeader; + } else if (config.authorizationType === AuthorizationType.OAUTH_AUTHORIZATION) { + const token = await this.authenticationTokenProvider.getToken(config); + axiosConfig.headers["Authorization"] = `Bearer ${token}`; + } } - - async retry( - action: () => Promise, - retries: number - ): Promise { - do { - const result = await action(); - if (result.status === SendStatus.ERROR && retries > 0) { - this.logger.warn("Sending request to Fiware failed. Retrying..."); - retries--; - continue; - } else { - return result; - } - } while (true); + return axiosConfig; + } + + getHeaders(config: FiwareDataTargetConfiguration): any { + let headers: any = {}; + + if (config.context) { + headers = { + "Content-Type": "application/json", + Link: `<${config.context}>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"`, + }; + } else { + headers = { + "Content-Type": "application/ld+json", + }; } - async makeAxiosConfiguration( - config: FiwareDataTargetConfiguration - ): Promise { - const axiosConfig: AxiosRequestConfig = { - timeout: config.timeout, - headers: this.getHeaders(config), - }; - - if ( - config.authorizationType !== null && - config.authorizationType !== AuthorizationType.NO_AUTHORIZATION - ) { - if (config.authorizationType === AuthorizationType.HTTP_BASIC_AUTHORIZATION) { - axiosConfig.auth = { - username: config.username, - password: config.password, - }; - } else if ( - config.authorizationType === AuthorizationType.HEADER_BASED_AUTHORIZATION - ) { - axiosConfig.headers["Authorization"] = config.authorizationHeader; - } else if ( - config.authorizationType === AuthorizationType.OAUTH_AUTHORIZATION - ) { - const token = await this.authenticationTokenProvider.getToken(config); - axiosConfig.headers["Authorization"] = `Bearer ${token}`; - } - } - return axiosConfig; + if (config.tenant) { + headers["NGSILD-Tenant"] = config.tenant; } - getHeaders(config: FiwareDataTargetConfiguration): any { - let headers: any = {}; - - if (config.context) { - headers = { - "Content-Type": "application/json", - Link: `<${config.context}>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"`, - }; - } else { - headers = { - "Content-Type": "application/ld+json", - }; - } - - if (config.tenant) { - headers["NGSILD-Tenant"] = config.tenant; - } - - return headers; - } + return headers; + } } diff --git a/src/services/data-targets/http-push-data-target.service.ts b/src/services/data-targets/http-push-data-target.service.ts index a5310cd3..45d29a8d 100644 --- a/src/services/data-targets/http-push-data-target.service.ts +++ b/src/services/data-targets/http-push-data-target.service.ts @@ -12,78 +12,57 @@ import { AxiosRequestConfig } from "axios"; @Injectable() export class HttpPushDataTargetService extends BaseDataTargetService { - constructor(private httpService: HttpService) { - super(); - } + constructor(private httpService: HttpService) { + super(); + } - protected readonly logger = new Logger(HttpPushDataTargetService.name); + protected readonly logger = new Logger(HttpPushDataTargetService.name); - // eslint-disable-next-line max-lines-per-function - async send( - datatarget: DataTarget, - dto: TransformedPayloadDto - ): Promise { - const data: HttpPushDataTargetData = { - rawBody: JSON.stringify(dto.payload), - mimeType: "application/json", - }; - const config: HttpPushDataTargetConfiguration = ( - datatarget as HttpPushDataTarget - ).toConfiguration(); + // eslint-disable-next-line max-lines-per-function + async send(datatarget: DataTarget, dto: TransformedPayloadDto): Promise { + const data: HttpPushDataTargetData = { + rawBody: JSON.stringify(dto.payload), + mimeType: "application/json", + }; + const config: HttpPushDataTargetConfiguration = (datatarget as HttpPushDataTarget).toConfiguration(); - // Setup HTTP client - const axiosConfig = HttpPushDataTargetService.makeAxiosConfiguration( - config, - data - ); - const target = `HttpTarget(${config.url})`; + // Setup HTTP client + const axiosConfig = HttpPushDataTargetService.makeAxiosConfiguration(config, data); + const target = `HttpTarget(${config.url})`; - try { - const result = await this.httpService - .post(config.url, data.rawBody, axiosConfig) - .toPromise(); + try { + const result = await this.httpService.post(config.url, data.rawBody, axiosConfig).toPromise(); - this.logger.debug( - `HttpPushDataTarget result: '${JSON.stringify(result.data)}'` - ); - if (!result.status.toString().startsWith("2")) { - this.logger.warn( - `Got a non-2xx status-code: ${result.status.toString()} and message: ${ - result.statusText - }` - ); - } - return this.success(target); - } catch (err) { - // TODO: Error handling for common errors - this.logger.error(`HttpPushDataTarget got error: ${err}`); - return this.failure(target, err, datatarget); - } + this.logger.debug(`HttpPushDataTarget result: '${JSON.stringify(result.data)}'`); + if (!result.status.toString().startsWith("2")) { + this.logger.warn(`Got a non-2xx status-code: ${result.status.toString()} and message: ${result.statusText}`); + } + return this.success(target); + } catch (err) { + // TODO: Error handling for common errors + this.logger.error(`HttpPushDataTarget got error: ${err}`); + return this.failure(target, err, datatarget); } + } - static makeAxiosConfiguration( - config: HttpPushDataTargetConfiguration, - data: HttpPushDataTargetData - ): AxiosRequestConfig { - const axiosConfig: AxiosRequestConfig = { - timeout: config.timeout, - headers: { "Content-Type": data.mimeType }, + static makeAxiosConfiguration( + config: HttpPushDataTargetConfiguration, + data: HttpPushDataTargetData + ): AxiosRequestConfig { + const axiosConfig: AxiosRequestConfig = { + timeout: config.timeout, + headers: { "Content-Type": data.mimeType }, + }; + if (config.authorizationType !== null && config.authorizationType !== AuthorizationType.NO_AUTHORIZATION) { + if (config.authorizationType === AuthorizationType.HTTP_BASIC_AUTHORIZATION) { + axiosConfig.auth = { + username: config.username, + password: config.password, }; - if ( - config.authorizationType !== null && - config.authorizationType !== AuthorizationType.NO_AUTHORIZATION - ) { - if (config.authorizationType === AuthorizationType.HTTP_BASIC_AUTHORIZATION) { - axiosConfig.auth = { - username: config.username, - password: config.password, - }; - } else if ( - config.authorizationType === AuthorizationType.HEADER_BASED_AUTHORIZATION - ) { - axiosConfig.headers["Authorization"] = config.authorizationHeader; - } - } - return axiosConfig; + } else if (config.authorizationType === AuthorizationType.HEADER_BASED_AUTHORIZATION) { + axiosConfig.headers["Authorization"] = config.authorizationHeader; + } } + return axiosConfig; + } } diff --git a/src/services/data-targets/mqtt-data-target.service.ts b/src/services/data-targets/mqtt-data-target.service.ts index 6d9fee3b..c81ef85c 100644 --- a/src/services/data-targets/mqtt-data-target.service.ts +++ b/src/services/data-targets/mqtt-data-target.service.ts @@ -12,60 +12,47 @@ import { BaseDataTargetService } from "./base-data-target.service"; @Injectable() export class MqttDataTargetService extends BaseDataTargetService { - constructor(private httpService: HttpService) { - super(); - } + constructor(private httpService: HttpService) { + super(); + } - protected readonly logger = new Logger(MqttDataTargetService.name); + protected readonly logger = new Logger(MqttDataTargetService.name); - send( - datatarget: DataTarget, - dto: TransformedPayloadDto, - onDone: (status: DataTargetSendStatus, targetType: DataTargetType) => void - ): void { - const config: MqttDataTargetConfiguration = ( - datatarget as MqttDataTarget - ).toConfiguration(); + send( + datatarget: DataTarget, + dto: TransformedPayloadDto, + onDone: (status: DataTargetSendStatus, targetType: DataTargetType) => void + ): void { + const config: MqttDataTargetConfiguration = (datatarget as MqttDataTarget).toConfiguration(); - // Setup client - const client = mqtt.connect(config.url, { - clean: true, - clientId: MqttClientId, - username: config.username, - password: config.password, - port: config.port, - connectTimeout: config.timeout, - // Accept connection to servers with self-signed certificates - rejectUnauthorized: false, - }); - const targetForLogging = `MqttDataTarget(URL '${config.url}', topic '${config.topic}')`; + // Setup client + const client = mqtt.connect(config.url, { + clean: true, + clientId: MqttClientId, + username: config.username, + password: config.password, + port: config.port, + connectTimeout: config.timeout, + // Accept connection to servers with self-signed certificates + rejectUnauthorized: false, + }); + const targetForLogging = `MqttDataTarget(URL '${config.url}', topic '${config.topic}')`; - client.once("connect", () => { - client.publish( - config.topic, - JSON.stringify(dto.payload), - { qos: config.qos }, - (err, packet) => { - try { - if (err) { - const status = this.failure( - targetForLogging, - err?.message, - datatarget - ); - onDone(status, DataTargetType.MQTT); - } else { - this.logger.debug( - "Packet received: " + JSON.stringify(packet) - ); - const status = this.success(targetForLogging); - onDone(status, DataTargetType.MQTT); - } - } finally { - client.end(); - } - } - ); - }); - } + client.once("connect", () => { + client.publish(config.topic, JSON.stringify(dto.payload), { qos: config.qos }, (err, packet) => { + try { + if (err) { + const status = this.failure(targetForLogging, err?.message, datatarget); + onDone(status, DataTargetType.MQTT); + } else { + this.logger.debug("Packet received: " + JSON.stringify(packet)); + const status = this.success(targetForLogging); + onDone(status, DataTargetType.MQTT); + } + } finally { + client.end(); + } + }); + }); + } } diff --git a/src/services/device-management/application.service.ts b/src/services/device-management/application.service.ts index 4ca13f0b..f3a0a547 100644 --- a/src/services/device-management/application.service.ts +++ b/src/services/device-management/application.service.ts @@ -29,457 +29,455 @@ import { IoTDevicesListToMapResponseDto } from "@dto/list-all-iot-devices-to-map @Injectable() export class ApplicationService { - constructor( - @InjectRepository(Application) - private applicationRepository: Repository, - @InjectRepository(IoTDevice) - private iotDeviceRepository: Repository, - @Inject(forwardRef(() => OrganizationService)) - private organizationService: OrganizationService, - private multicastService: MulticastService, - private chirpstackDeviceService: ChirpstackDeviceService, - @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService, - @Inject(forwardRef(() => DataTargetService)) - private dataTargetService: DataTargetService, - private chirpstackApplicationService: ApplicationChirpstackService - ) {} - - async findAndCountInList( - query?: ListAllEntitiesDto, - whitelist?: number[], - allFromOrgs?: number[] - ): Promise { - const sorting = this.getSortingForApplications(query); - const orgCondition = - allFromOrgs != null ? { id: In(whitelist), belongsTo: In(allFromOrgs) } : { id: In(whitelist) }; - const [result, total] = await this.applicationRepository.findAndCount({ - where: orgCondition, - take: query.limit, - skip: query.offset, - relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes"], - order: sorting, - }); - - return { - data: result, - count: total, - }; - } - - async findAndCountApplicationInWhitelistOrOrganization( - query: ListAllApplicationsDto, - allowedApplications: number[], - organizationIds: number[] - ): Promise { - const [result, total] = await this.applicationRepository.findAndCount({ - where: - organizationIds.length > 0 - ? { id: In(allowedApplications), belongsTo: In(organizationIds) } - : { id: In(allowedApplications) }, - take: query.limit, - skip: query.offset, - relations: ["iotDevices"], - order: { id: query.sort }, - }); - - return { - data: result, - count: total, - }; - } - - async findAndCountWithPagination( - query?: ListAllEntitiesDto, - allowedOrganisations?: number[] - ): Promise { - const sorting = this.getSortingForApplications(query); - const [result, total] = await this.applicationRepository.findAndCount({ - where: allowedOrganisations != null ? { belongsTo: In(allowedOrganisations) } : {}, - take: +query.limit, - skip: +query.offset, - relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes"], - order: sorting, - }); - - this.externalSortResult(query, result); - - return { - data: result, - count: total, - }; + constructor( + @InjectRepository(Application) + private applicationRepository: Repository, + @InjectRepository(IoTDevice) + private iotDeviceRepository: Repository, + @Inject(forwardRef(() => OrganizationService)) + private organizationService: OrganizationService, + private multicastService: MulticastService, + private chirpstackDeviceService: ChirpstackDeviceService, + @Inject(forwardRef(() => PermissionService)) + private permissionService: PermissionService, + @Inject(forwardRef(() => DataTargetService)) + private dataTargetService: DataTargetService, + private chirpstackApplicationService: ApplicationChirpstackService + ) {} + + async findAndCountInList( + query?: ListAllEntitiesDto, + whitelist?: number[], + allFromOrgs?: number[] + ): Promise { + const sorting = this.getSortingForApplications(query); + const orgCondition = + allFromOrgs != null ? { id: In(whitelist), belongsTo: In(allFromOrgs) } : { id: In(whitelist) }; + const [result, total] = await this.applicationRepository.findAndCount({ + where: orgCondition, + take: query.limit, + skip: query.offset, + relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes"], + order: sorting, + }); + + return { + data: result, + count: total, + }; + } + + async findAndCountApplicationInWhitelistOrOrganization( + query: ListAllApplicationsDto, + allowedApplications: number[], + organizationIds: number[] + ): Promise { + const [result, total] = await this.applicationRepository.findAndCount({ + where: + organizationIds.length > 0 + ? { id: In(allowedApplications), belongsTo: In(organizationIds) } + : { id: In(allowedApplications) }, + take: query.limit, + skip: query.offset, + relations: ["iotDevices"], + order: { id: query.sort }, + }); + + return { + data: result, + count: total, + }; + } + + async findAndCountWithPagination( + query?: ListAllEntitiesDto, + allowedOrganisations?: number[] + ): Promise { + const sorting = this.getSortingForApplications(query); + const [result, total] = await this.applicationRepository.findAndCount({ + where: allowedOrganisations != null ? { belongsTo: In(allowedOrganisations) } : {}, + take: +query.limit, + skip: +query.offset, + relations: ["iotDevices", "dataTargets", "controlledProperties", "deviceTypes"], + order: sorting, + }); + + this.externalSortResult(query, result); + + return { + data: result, + count: total, + }; + } + + // Some sorting fields can't be done in the database + private externalSortResult(query: ListAllEntitiesDto, result: Application[]) { + // Since openDataDkEnabled is not a database attribute sorting has to be done manually after reading + if (query.orderOn === "openDataDkEnabled") { + result.sort( + (a, b) => + (query.sort.toLowerCase() === "asc" ? -1 : 1) * + (Number(!!a.dataTargets.find(t => t.type === DataTargetType.OpenDataDK)) - + Number(!!b.dataTargets.find(t => t.type === DataTargetType.OpenDataDK))) + ); } - - // Some sorting fields can't be done in the database - private externalSortResult(query: ListAllEntitiesDto, result: Application[]) { - // Since openDataDkEnabled is not a database attribute sorting has to be done manually after reading - if (query.orderOn === "openDataDkEnabled") { - result.sort( - (a, b) => - (query.sort.toLowerCase() === "asc" ? -1 : 1) * - (Number(!!a.dataTargets.find(t => t.type === DataTargetType.OpenDataDK)) - - Number(!!b.dataTargets.find(t => t.type === DataTargetType.OpenDataDK))) - ); - } - if (query.orderOn === "devices") { - result.sort( - (a, b) => (query.sort.toLowerCase() === "asc" ? 1 : -1) * (a.iotDevices.length - b.iotDevices.length) - ); - } - if (query.orderOn === "dataTargets") { - result.sort( - (a, b) => (query.sort.toLowerCase() === "asc" ? 1 : -1) * (a.dataTargets.length - b.dataTargets.length) - ); - } + if (query.orderOn === "devices") { + result.sort( + (a, b) => (query.sort.toLowerCase() === "asc" ? 1 : -1) * (a.iotDevices.length - b.iotDevices.length) + ); } - - async getApplicationsOnPermissionId( - permissionId: number, - query: ListAllApplicationsDto - ): Promise { - let orderBy = `application.id`; - if ( - query.orderOn != null && - (query.orderOn === "id" || query.orderOn === "name" || query.orderOn === "updatedAt") - ) { - orderBy = `application.${query.orderOn}`; - } - const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC"; - const [result, total] = await this.applicationRepository - .createQueryBuilder("application") - .innerJoin("application.permissions", "perm") - .leftJoinAndSelect("application.iotDevices", "iotdevices") - .leftJoinAndSelect("application.dataTargets", "datatargets") - .leftJoinAndSelect("application.controlledProperties", "controlledproperties") - .leftJoinAndSelect("application.deviceTypes", "devicetypes") - .where("perm.id = :permId", { permId: permissionId }) - .take(+query.limit) - .skip(+query.offset) - .orderBy(orderBy, order) - .getManyAndCount(); - - return { - data: result, - count: total, - }; + if (query.orderOn === "dataTargets") { + result.sort( + (a, b) => (query.sort.toLowerCase() === "asc" ? 1 : -1) * (a.dataTargets.length - b.dataTargets.length) + ); } - - async findOneWithoutRelations(id: number): Promise { - return await this.applicationRepository.findOneByOrFail({ id }); + } + + async getApplicationsOnPermissionId( + permissionId: number, + query: ListAllApplicationsDto + ): Promise { + let orderBy = `application.id`; + if ( + query.orderOn != null && + (query.orderOn === "id" || query.orderOn === "name" || query.orderOn === "updatedAt") + ) { + orderBy = `application.${query.orderOn}`; } - - async findOneWithOrganisation(id: number): Promise { - return await this.applicationRepository.findOneOrFail({ - where: { id }, - relations: ["belongsTo"], - }); - } - - async findManyWithOrganisation(ids: number[]): Promise { - return await this.applicationRepository.find({ - where: { id: In(ids) }, - relations: ["belongsTo"], - }); + const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC"; + const [result, total] = await this.applicationRepository + .createQueryBuilder("application") + .innerJoin("application.permissions", "perm") + .leftJoinAndSelect("application.iotDevices", "iotdevices") + .leftJoinAndSelect("application.dataTargets", "datatargets") + .leftJoinAndSelect("application.controlledProperties", "controlledproperties") + .leftJoinAndSelect("application.deviceTypes", "devicetypes") + .where("perm.id = :permId", { permId: permissionId }) + .take(+query.limit) + .skip(+query.offset) + .orderBy(orderBy, order) + .getManyAndCount(); + + return { + data: result, + count: total, + }; + } + + async findOneWithoutRelations(id: number): Promise { + return await this.applicationRepository.findOneByOrFail({ id }); + } + + async findOneWithOrganisation(id: number): Promise { + return await this.applicationRepository.findOneOrFail({ + where: { id }, + relations: ["belongsTo"], + }); + } + + async findManyWithOrganisation(ids: number[]): Promise { + return await this.applicationRepository.find({ + where: { id: In(ids) }, + relations: ["belongsTo"], + }); + } + + async findOne(id: number): Promise { + const app = await this.applicationRepository.findOneOrFail({ + where: { id }, + relations: [ + "iotDevices", + "belongsTo", + nameof("controlledProperties"), + nameof("deviceTypes"), + "permissions", + ], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + + return app; + } + + async findManyByIds(ids: number[]): Promise { + if (ids == null || ids?.length === 0) { + return []; } - - async findOne(id: number): Promise { - const app = await this.applicationRepository.findOneOrFail({ - where: { id }, - relations: [ - "iotDevices", - "belongsTo", - nameof("controlledProperties"), - nameof("deviceTypes"), - "permissions", - ], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - - return app; + return await this.applicationRepository.findBy({ id: In(ids) }); + } + + async create(createApplicationDto: CreateApplicationDto, userId: number): Promise { + const application = new Application(); + + const mappedApplication = await this.mapApplicationDtoToApplication(createApplicationDto, application, userId); + mappedApplication.iotDevices = []; + mappedApplication.dataTargets = []; + mappedApplication.multicasts = []; + mappedApplication.createdBy = userId; + mappedApplication.updatedBy = userId; + + try { + mappedApplication.chirpstackId = await this.chirpstackApplicationService.createChirpstackApplication({ + application: { description: createApplicationDto.description, name: createApplicationDto.name }, + }); + const app = await this.applicationRepository.save(mappedApplication); + + await this.permissionService.autoAddPermissionsToApplication(app); + + return app; + } catch (e) { + throw new BadRequestException(ErrorCodes.InvalidPost); } - - async findManyByIds(ids: number[]): Promise { - if (ids == null || ids?.length === 0) { - return []; - } - return await this.applicationRepository.findBy({ id: In(ids) }); + } + + async update(id: number, updateApplicationDto: UpdateApplicationDto, userId: number): Promise { + const existingApplication = await this.applicationRepository.findOneOrFail({ + where: { id }, + relations: [ + nameof("iotDevices"), + nameof("dataTargets"), + nameof("controlledProperties"), + nameof("deviceTypes"), + ], + }); + + const mappedApplication = await this.mapApplicationDtoToApplication( + updateApplicationDto, + existingApplication, + userId + ); + + await this.chirpstackApplicationService.updateApplication(mappedApplication); + + mappedApplication.updatedBy = userId; + return this.applicationRepository.save(mappedApplication, {}); + } + + async delete(id: number): Promise { + const application = await this.applicationRepository.findOne({ + where: { id }, + relations: ["iotDevices", "multicasts", "dataTargets"], + }); + + // Don't allow delete if this application contains any sigfox devices. + if ( + application.iotDevices.some(iotDevice => { + return iotDevice.type === IoTDeviceType.SigFox; + }) + ) { + throw new ConflictException(ErrorCodes.DeleteNotAllowedHasSigfoxDevice); } - async create(createApplicationDto: CreateApplicationDto, userId: number): Promise { - const application = new Application(); - - const mappedApplication = await this.mapApplicationDtoToApplication(createApplicationDto, application, userId); - mappedApplication.iotDevices = []; - mappedApplication.dataTargets = []; - mappedApplication.multicasts = []; - mappedApplication.createdBy = userId; - mappedApplication.updatedBy = userId; - - try { - mappedApplication.chirpstackId = await this.chirpstackApplicationService.createChirpstackApplication({ - application: { description: createApplicationDto.description, name: createApplicationDto.name }, - }); - const app = await this.applicationRepository.save(mappedApplication); - - await this.permissionService.autoAddPermissionsToApplication(app); - - return app; - } catch (e) { - throw new BadRequestException(ErrorCodes.InvalidPost); - } + for (const dataTarget of application.dataTargets) { + await this.dataTargetService.delete(dataTarget.id); } - async update(id: number, updateApplicationDto: UpdateApplicationDto, userId: number): Promise { - const existingApplication = await this.applicationRepository.findOneOrFail({ - where: { id }, - relations: [ - nameof("iotDevices"), - nameof("dataTargets"), - nameof("controlledProperties"), - nameof("deviceTypes"), - ], - }); - - const mappedApplication = await this.mapApplicationDtoToApplication( - updateApplicationDto, - existingApplication, - userId - ); - - await this.chirpstackApplicationService.updateApplication(mappedApplication); + // Delete all LoRaWAN devices in ChirpStack + const loRaWANDevices = application.iotDevices.filter(device => device.type === IoTDeviceType.LoRaWAN); - mappedApplication.updatedBy = userId; - return this.applicationRepository.save(mappedApplication, {}); + for (const device of loRaWANDevices) { + const lwDevice = device as LoRaWANDevice; + await this.chirpstackDeviceService.deleteDevice(lwDevice.deviceEUI); } - async delete(id: number): Promise { - const application = await this.applicationRepository.findOne({ - where: { id }, - relations: ["iotDevices", "multicasts", "dataTargets"], - }); + //delete all multicats + const multicasts = application.multicasts; + for (const multicast of multicasts) { + const dbMulticast = await this.multicastService.findOne(multicast.id); - // Don't allow delete if this application contains any sigfox devices. - if ( - application.iotDevices.some(iotDevice => { - return iotDevice.type === IoTDeviceType.SigFox; - }) - ) { - throw new ConflictException(ErrorCodes.DeleteNotAllowedHasSigfoxDevice); - } - - for (const dataTarget of application.dataTargets) { - await this.dataTargetService.delete(dataTarget.id); - } - - // Delete all LoRaWAN devices in ChirpStack - const loRaWANDevices = application.iotDevices.filter(device => device.type === IoTDeviceType.LoRaWAN); - - for (const device of loRaWANDevices) { - const lwDevice = device as LoRaWANDevice; - await this.chirpstackDeviceService.deleteDevice(lwDevice.deviceEUI); - } - - //delete all multicats - const multicasts = application.multicasts; - for (const multicast of multicasts) { - const dbMulticast = await this.multicastService.findOne(multicast.id); - - await this.multicastService.deleteMulticastChirpstack( - dbMulticast.lorawanMulticastDefinition.chirpstackGroupId - ); - } - if (application.chirpstackId) { - await this.chirpstackApplicationService.deleteApplication(application.chirpstackId); - } - - return this.applicationRepository.delete(id); + await this.multicastService.deleteMulticastChirpstack(dbMulticast.lorawanMulticastDefinition.chirpstackGroupId); } - - async isNameValidAndNotUsed(name: string, id?: number): Promise { - if (name) { - const applicationsWithName = await this.applicationRepository.findBy({ - name, - }); - - if (id) { - // If id is given then this id is allowed to have the name already (i.e. it's being changed) - return applicationsWithName.every(app => { - return app.id === id; - }); - } else { - return applicationsWithName.length === 0; - } - } - - return false; + if (application.chirpstackId) { + await this.chirpstackApplicationService.deleteApplication(application.chirpstackId); } - private async mapApplicationDtoToApplication( - applicationDto: CreateApplicationDto | UpdateApplicationDto, - application: Application, - userId: number - ): Promise { - application.name = applicationDto.name; - application.description = applicationDto.description; - application.belongsTo = await this.organizationService.findById(applicationDto.organizationId); - application.status = applicationDto.status; - // Setting a date to 'undefined' will set it to today in the database - application.startDate = applicationDto.startDate ?? null; - application.endDate = applicationDto.endDate ?? null; - application.category = applicationDto.category; - application.owner = applicationDto.owner; - application.contactPerson = applicationDto.contactPerson; - application.contactEmail = applicationDto.contactEmail; - application.contactPhone = applicationDto.contactPhone; - application.personalData = applicationDto.personalData; - application.hardware = applicationDto.hardware; - application.permissions = await this.permissionService.findManyByIds(applicationDto.permissionIds); - - // Set metadata dependencies - application.controlledProperties = applicationDto.controlledProperties - ? this.buildControlledPropertyDeviceType( - ControlledPropertyTypes, - applicationDto.controlledProperties, - userId, - ControlledProperty - ) - : undefined; - application.deviceTypes = applicationDto.deviceTypes - ? this.buildControlledPropertyDeviceType( - ApplicationDeviceTypes, - applicationDto.deviceTypes, - userId, - ApplicationDeviceType - ) - : undefined; - - return application; - } - - buildControlledPropertyDeviceType< - T extends Record, - Entity extends ControlledProperty | ApplicationDeviceType - >(validKeys: T, clientTypes: string[], userId: number, entity: { new (): Entity }): Entity[] { - // Filter out invalid client values - const matchingValues = findValuesInRecord(validKeys, clientTypes); + return this.applicationRepository.delete(id); + } - return matchingValues.map(type => { - const newEntity = new entity(); - newEntity.createdBy = userId; - newEntity.updatedBy = userId; - newEntity.type = type as ControlledPropertyTypes | ApplicationDeviceTypeUnion; + async isNameValidAndNotUsed(name: string, id?: number): Promise { + if (name) { + const applicationsWithName = await this.applicationRepository.findBy({ + name, + }); - return newEntity; + if (id) { + // If id is given then this id is allowed to have the name already (i.e. it's being changed) + return applicationsWithName.every(app => { + return app.id === id; }); + } else { + return applicationsWithName.length === 0; + } } - async findDevicesForApplication(appId: number, query: ListAllEntitiesDto): Promise { - const orderByColumn = this.getSortingForIoTDevices(query); - const direction = query?.sort?.toUpperCase() === "DESC" ? "DESC" : "ASC"; - const nullsOrder = query?.sort?.toUpperCase() === "DESC" ? "NULLS LAST" : "NULLS FIRST"; - - const [data, count] = await this.iotDeviceRepository - .createQueryBuilder("iot_device") - .where('"iot_device"."applicationId" = :id', { id: appId }) - .leftJoinAndSelect("iot_device.latestReceivedMessage", "metadata") - .leftJoinAndSelect("iot_device.deviceModel", "deviceModel") - .leftJoinAndSelect("iot_device.connections", "connections") - .skip(query?.offset ? +query.offset : 0) - .take(query?.limit ? +query.limit : 100) - .orderBy(orderByColumn, direction, nullsOrder) - .getManyAndCount(); - - if (query.orderOn === "dataTargets") { - data.sort( - (a, b) => (query.sort.toLowerCase() === "asc" ? 1 : -1) * (a.connections.length - b.connections.length) - ); - } - - // Fetch LoRa details one by one to get battery status. The LoRa API doesn't support query by multiple deveui's to reduce the calls. - // Reduce calls by pre-fetching service profile ids by application id. The applications is usually the same - // TODO: Remove - const loraDevices = data.filter( - device => device.type === IoTDeviceType.LoRaWAN - ) as LoRaWANDeviceWithChirpstackDataDto[]; - - for (const device of loraDevices) { - await this.chirpstackDeviceService.enrichLoRaWANDevice(device); - } - - return { - data, - count, - }; + return false; + } + + private async mapApplicationDtoToApplication( + applicationDto: CreateApplicationDto | UpdateApplicationDto, + application: Application, + userId: number + ): Promise { + application.name = applicationDto.name; + application.description = applicationDto.description; + application.belongsTo = await this.organizationService.findById(applicationDto.organizationId); + application.status = applicationDto.status; + // Setting a date to 'undefined' will set it to today in the database + application.startDate = applicationDto.startDate ?? null; + application.endDate = applicationDto.endDate ?? null; + application.category = applicationDto.category; + application.owner = applicationDto.owner; + application.contactPerson = applicationDto.contactPerson; + application.contactEmail = applicationDto.contactEmail; + application.contactPhone = applicationDto.contactPhone; + application.personalData = applicationDto.personalData; + application.hardware = applicationDto.hardware; + application.permissions = await this.permissionService.findManyByIds(applicationDto.permissionIds); + + // Set metadata dependencies + application.controlledProperties = applicationDto.controlledProperties + ? this.buildControlledPropertyDeviceType( + ControlledPropertyTypes, + applicationDto.controlledProperties, + userId, + ControlledProperty + ) + : undefined; + application.deviceTypes = applicationDto.deviceTypes + ? this.buildControlledPropertyDeviceType( + ApplicationDeviceTypes, + applicationDto.deviceTypes, + userId, + ApplicationDeviceType + ) + : undefined; + + return application; + } + + buildControlledPropertyDeviceType< + T extends Record, + Entity extends ControlledProperty | ApplicationDeviceType + >(validKeys: T, clientTypes: string[], userId: number, entity: { new (): Entity }): Entity[] { + // Filter out invalid client values + const matchingValues = findValuesInRecord(validKeys, clientTypes); + + return matchingValues.map(type => { + const newEntity = new entity(); + newEntity.createdBy = userId; + newEntity.updatedBy = userId; + newEntity.type = type as ControlledPropertyTypes | ApplicationDeviceTypeUnion; + + return newEntity; + }); + } + + async findDevicesForApplication(appId: number, query: ListAllEntitiesDto): Promise { + const orderByColumn = this.getSortingForIoTDevices(query); + const direction = query?.sort?.toUpperCase() === "DESC" ? "DESC" : "ASC"; + const nullsOrder = query?.sort?.toUpperCase() === "DESC" ? "NULLS LAST" : "NULLS FIRST"; + + const [data, count] = await this.iotDeviceRepository + .createQueryBuilder("iot_device") + .where('"iot_device"."applicationId" = :id', { id: appId }) + .leftJoinAndSelect("iot_device.latestReceivedMessage", "metadata") + .leftJoinAndSelect("iot_device.deviceModel", "deviceModel") + .leftJoinAndSelect("iot_device.connections", "connections") + .skip(query?.offset ? +query.offset : 0) + .take(query?.limit ? +query.limit : 100) + .orderBy(orderByColumn, direction, nullsOrder) + .getManyAndCount(); + + if (query.orderOn === "dataTargets") { + data.sort( + (a, b) => (query.sort.toLowerCase() === "asc" ? 1 : -1) * (a.connections.length - b.connections.length) + ); } - async findDevicesForApplicationMap(appId: number): Promise { - const [data] = await this.iotDeviceRepository - .createQueryBuilder("iot_device") - .where('"iot_device"."applicationId" = :id', { id: appId }) - .leftJoinAndSelect("iot_device.latestReceivedMessage", "metadata") - .getManyAndCount(); - - const deviceList: IoTDevicesListToMapResponseDto[] = data.map(device => { - return { - id: device.id, - name: device.name, - type: device.type, - latestSentMessage: device.latestReceivedMessage?.sentTime, - location: device.location, - }; - }); + // Fetch LoRa details one by one to get battery status. The LoRa API doesn't support query by multiple deveui's to reduce the calls. + // Reduce calls by pre-fetching service profile ids by application id. The applications is usually the same + // TODO: Remove + const loraDevices = data.filter( + device => device.type === IoTDeviceType.LoRaWAN + ) as LoRaWANDeviceWithChirpstackDataDto[]; - return deviceList; + for (const device of loraDevices) { + await this.chirpstackDeviceService.enrichLoRaWANDevice(device); } - private getSortingForIoTDevices(query: ListAllEntitiesDto) { - let orderBy = `iot_device.id`; - if ( - (query?.orderOn != null && query.orderOn === "id") || - query.orderOn === "name" || - query.orderOn === "active" || - query.orderOn === "rssi" || - query.orderOn === "snr" || - query.orderOn === "deviceModel" || - query.orderOn === "dataTargets" || - query.orderOn === "commentOnLocation" - ) { - if (query.orderOn === "active") { - orderBy = `metadata.sentTime`; - } else if (query.orderOn === "rssi" || query.orderOn === "snr") { - orderBy = `metadata.${query.orderOn}`; - } else if (query.orderOn === "deviceModel") { - orderBy = "deviceModel.body"; - } else if (query.orderOn === "dataTargets") { - orderBy = "connections.id"; - } else { - orderBy = `iot_device.${query.orderOn}`; - } - } - return orderBy; + return { + data, + count, + }; + } + + async findDevicesForApplicationMap(appId: number): Promise { + const [data] = await this.iotDeviceRepository + .createQueryBuilder("iot_device") + .where('"iot_device"."applicationId" = :id', { id: appId }) + .leftJoinAndSelect("iot_device.latestReceivedMessage", "metadata") + .getManyAndCount(); + + const deviceList: IoTDevicesListToMapResponseDto[] = data.map(device => { + return { + id: device.id, + name: device.name, + type: device.type, + latestSentMessage: device.latestReceivedMessage?.sentTime, + location: device.location, + }; + }); + + return deviceList; + } + + private getSortingForIoTDevices(query: ListAllEntitiesDto) { + let orderBy = `iot_device.id`; + if ( + (query?.orderOn != null && query.orderOn === "id") || + query.orderOn === "name" || + query.orderOn === "active" || + query.orderOn === "rssi" || + query.orderOn === "snr" || + query.orderOn === "deviceModel" || + query.orderOn === "dataTargets" || + query.orderOn === "commentOnLocation" + ) { + if (query.orderOn === "active") { + orderBy = `metadata.sentTime`; + } else if (query.orderOn === "rssi" || query.orderOn === "snr") { + orderBy = `metadata.${query.orderOn}`; + } else if (query.orderOn === "deviceModel") { + orderBy = "deviceModel.body"; + } else if (query.orderOn === "dataTargets") { + orderBy = "connections.id"; + } else { + orderBy = `iot_device.${query.orderOn}`; + } } - - private getSortingForApplications(query: ListAllEntitiesDto): Record { - const sorting: Record = {}; - if ( - // TODO: Make this nicer - query.orderOn != null && - (query.orderOn === "id" || - query.orderOn === "name" || - query.orderOn === "updatedAt" || - query.orderOn === "status" || - query.orderOn === "startDate" || - query.orderOn === "endDate" || - query.orderOn === "owner" || - query.orderOn === "contactPerson" || - query.orderOn === "personalData") - ) { - sorting[query.orderOn] = query.sort.toLocaleUpperCase(); - } else { - sorting["id"] = "ASC"; - } - return sorting; + return orderBy; + } + + private getSortingForApplications(query: ListAllEntitiesDto): Record { + const sorting: Record = {}; + if ( + // TODO: Make this nicer + query.orderOn != null && + (query.orderOn === "id" || + query.orderOn === "name" || + query.orderOn === "updatedAt" || + query.orderOn === "status" || + query.orderOn === "startDate" || + query.orderOn === "endDate" || + query.orderOn === "owner" || + query.orderOn === "contactPerson" || + query.orderOn === "personalData") + ) { + sorting[query.orderOn] = query.sort.toLocaleUpperCase(); + } else { + sorting["id"] = "ASC"; } + return sorting; + } } diff --git a/src/services/device-management/device-model.service.ts b/src/services/device-management/device-model.service.ts index f917e1a5..eae58b10 100644 --- a/src/services/device-management/device-model.service.ts +++ b/src/services/device-management/device-model.service.ts @@ -12,119 +12,115 @@ import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; @Injectable() export class DeviceModelService { - constructor( - @InjectRepository(DeviceModel) - private repository: Repository, - private organizationService: OrganizationService - ) { - this.avj = new AJV({ allErrors: true, missingRefs: "ignore", verbose: false }); - this.avj.addSchema(deviceModelSchema, this.SCHEMA_NAME); - } + constructor( + @InjectRepository(DeviceModel) + private repository: Repository, + private organizationService: OrganizationService + ) { + this.avj = new AJV({ allErrors: true, missingRefs: "ignore", verbose: false }); + this.avj.addSchema(deviceModelSchema, this.SCHEMA_NAME); + } - private avj: AJV.Ajv; - private readonly SCHEMA_NAME = "device-model"; + private avj: AJV.Ajv; + private readonly SCHEMA_NAME = "device-model"; - private getSorting(query: ListAllEntitiesDto) { - const sorting: { [id: string]: string | number } = {}; - if (query?.orderOn != null && query.orderOn == "id") { - sorting[query.orderOn] = query.sort.toLocaleUpperCase(); - } else { - sorting["id"] = "ASC"; - } - return sorting; + private getSorting(query: ListAllEntitiesDto) { + const sorting: { [id: string]: string | number } = {}; + if (query?.orderOn != null && query.orderOn == "id") { + sorting[query.orderOn] = query.sort.toLocaleUpperCase(); + } else { + sorting["id"] = "ASC"; } + return sorting; + } - async getAllDeviceModelsByOrgIds( - orgIds: number[], - query?: ListAllEntitiesDto - ): Promise { - if (orgIds.length == 0) { - return { - data: [], - count: 0, - }; - } + async getAllDeviceModelsByOrgIds( + orgIds: number[], + query?: ListAllEntitiesDto + ): Promise { + if (orgIds.length == 0) { + return { + data: [], + count: 0, + }; + } - const [data, count] = await this.repository.findAndCount({ - where: { - belongsTo: { id: In(orgIds) }, - }, - take: query?.limit ? +query.limit : 100, - skip: query?.offset ? +query.offset : 0, - order: this.getSorting(query), - }); + const [data, count] = await this.repository.findAndCount({ + where: { + belongsTo: { id: In(orgIds) }, + }, + take: query?.limit ? +query.limit : 100, + skip: query?.offset ? +query.offset : 0, + order: this.getSorting(query), + }); - return { - data: data, - count: count, - }; - } + return { + data: data, + count: count, + }; + } - async getById(id: number): Promise { - return this.repository.findOne({ - where: { id }, - loadRelationIds: { - relations: ["belongsTo", "createdBy", "updatedBy"], - }, - }); - } + async getById(id: number): Promise { + return this.repository.findOne({ + where: { id }, + loadRelationIds: { + relations: ["belongsTo", "createdBy", "updatedBy"], + }, + }); + } - async getByIdWithRelations(id: number): Promise { - return this.repository.findOne({ - where: { id }, - relations: ["belongsTo"], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - } + async getByIdWithRelations(id: number): Promise { + return this.repository.findOne({ + where: { id }, + relations: ["belongsTo"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } - async getByIdsWithRelations(ids: number[]): Promise { - return this.repository.find({ - where: { id: In(ids) }, - relations: ["belongsTo"], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - } + async getByIdsWithRelations(ids: number[]): Promise { + return this.repository.find({ + where: { id: In(ids) }, + relations: ["belongsTo"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } - async create(dto: CreateDeviceModelDto, userId: number): Promise { - const deviceModel = new DeviceModel(); - deviceModel.belongsTo = await this.organizationService.findById(dto.belongsToId); - deviceModel.createdBy = userId; - return this.update(deviceModel, dto, userId); - } + async create(dto: CreateDeviceModelDto, userId: number): Promise { + const deviceModel = new DeviceModel(); + deviceModel.belongsTo = await this.organizationService.findById(dto.belongsToId); + deviceModel.createdBy = userId; + return this.update(deviceModel, dto, userId); + } - async update( - deviceModel: DeviceModel, - dto: UpdateDeviceModelDto, - userId: number - ): Promise { - this.validateModel(dto.body); + async update(deviceModel: DeviceModel, dto: UpdateDeviceModelDto, userId: number): Promise { + this.validateModel(dto.body); - deviceModel.body = dto.body; - deviceModel.updatedBy = userId; - return this.repository.save(deviceModel); - } + deviceModel.body = dto.body; + deviceModel.updatedBy = userId; + return this.repository.save(deviceModel); + } - async delete(id: number): Promise { - return this.repository.delete(id); - } + async delete(id: number): Promise { + return this.repository.delete(id); + } - private validateModel(body: JSON): void { - const valid = this.avj.validate(this.SCHEMA_NAME, body); - if (!valid) { - // Construct error messages like class-validator ... - const messages = this.avj.errors.map(x => { - return { - property: (x?.params as AJV.RequiredParams)?.missingProperty, - constraints: { - required: x.message, - }, - }; - }); - throw new BadRequestException(messages); - } + private validateModel(body: JSON): void { + const valid = this.avj.validate(this.SCHEMA_NAME, body); + if (!valid) { + // Construct error messages like class-validator ... + const messages = this.avj.errors.map(x => { + return { + property: (x?.params as AJV.RequiredParams)?.missingProperty, + constraints: { + required: x.message, + }, + }; + }); + throw new BadRequestException(messages); } + } } diff --git a/src/services/device-management/iot-device-downlink.service.ts b/src/services/device-management/iot-device-downlink.service.ts index 355062ab..d3c8fc18 100644 --- a/src/services/device-management/iot-device-downlink.service.ts +++ b/src/services/device-management/iot-device-downlink.service.ts @@ -9,84 +9,84 @@ import { SigFoxApiDeviceTypeService } from "@services/sigfox/sigfox-api-device-t import { SigFoxGroupService } from "@services/sigfox/sigfox-group.service"; import { IoTDeviceService } from "@services/device-management/iot-device.service"; import { - CreateChirpstackDeviceQueueItemDto, - CreateChirpstackDeviceQueueItemResponse, + CreateChirpstackDeviceQueueItemDto, + CreateChirpstackDeviceQueueItemResponse, } from "@dto/chirpstack/create-chirpstack-device-queue-item.dto"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; import { IdResponse } from "@interfaces/chirpstack-id-response.interface"; @Injectable() export class IoTDeviceDownlinkService { - constructor( - private sigfoxApiDeviceTypeService: SigFoxApiDeviceTypeService, - private sigfoxGroupService: SigFoxGroupService, - private iotDeviceService: IoTDeviceService, - private chirpstackDeviceService: ChirpstackDeviceService - ) {} - private readonly logger = new Logger(IoTDeviceDownlinkService.name); - private readonly SIGFOX_DOWNLINK_LENGTH_EXACT = 16; + constructor( + private sigfoxApiDeviceTypeService: SigFoxApiDeviceTypeService, + private sigfoxGroupService: SigFoxGroupService, + private iotDeviceService: IoTDeviceService, + private chirpstackDeviceService: ChirpstackDeviceService + ) {} + private readonly logger = new Logger(IoTDeviceDownlinkService.name); + private readonly SIGFOX_DOWNLINK_LENGTH_EXACT = 16; - async createDownlink(dto: CreateIoTDeviceDownlinkDto, device: IoTDevice): Promise { - if (device.type === IoTDeviceType.LoRaWAN) { - const cast = device; - return await this.createLoraDownlink(dto, cast); - } else if (device.type === IoTDeviceType.SigFox) { - const cast = device; - return await this.createSigfoxDownlink(dto, cast); - } else { - throw new BadRequestException(ErrorCodes.DownlinkNotSupportedForDeviceType); - } + async createDownlink(dto: CreateIoTDeviceDownlinkDto, device: IoTDevice): Promise { + if (device.type === IoTDeviceType.LoRaWAN) { + const cast = device; + return await this.createLoraDownlink(dto, cast); + } else if (device.type === IoTDeviceType.SigFox) { + const cast = device; + return await this.createSigfoxDownlink(dto, cast); + } else { + throw new BadRequestException(ErrorCodes.DownlinkNotSupportedForDeviceType); } + } - private async createSigfoxDownlink(dto: CreateIoTDeviceDownlinkDto, cast: SigFoxDevice): Promise { - this.validateSigfoxPayload(dto); - this.logger.debug(`Creating downlink for device(${cast.id}) sigfoxId(${cast.deviceId})`); - cast.downlinkPayload = dto.data; - await this.iotDeviceService.save(cast); - await this.updateSigFoxDeviceTypeDownlink(cast); - } + private async createSigfoxDownlink(dto: CreateIoTDeviceDownlinkDto, cast: SigFoxDevice): Promise { + this.validateSigfoxPayload(dto); + this.logger.debug(`Creating downlink for device(${cast.id}) sigfoxId(${cast.deviceId})`); + cast.downlinkPayload = dto.data; + await this.iotDeviceService.save(cast); + await this.updateSigFoxDeviceTypeDownlink(cast); + } - private async updateSigFoxDeviceTypeDownlink(cast: SigFoxDevice) { - const sigfoxGroup = await this.sigfoxGroupService.findOneByGroupId(cast.groupId); - await this.sigfoxApiDeviceTypeService.addOrUpdateCallback(sigfoxGroup, cast.deviceTypeId); - } + private async updateSigFoxDeviceTypeDownlink(cast: SigFoxDevice) { + const sigfoxGroup = await this.sigfoxGroupService.findOneByGroupId(cast.groupId); + await this.sigfoxApiDeviceTypeService.addOrUpdateCallback(sigfoxGroup, cast.deviceTypeId); + } - private validateSigfoxPayload(dto: CreateIoTDeviceDownlinkDto) { - if (dto.data.length !== this.SIGFOX_DOWNLINK_LENGTH_EXACT) { - throw new BadRequestException(ErrorCodes.DownlinkLengthWrongForSigfox); - } + private validateSigfoxPayload(dto: CreateIoTDeviceDownlinkDto) { + if (dto.data.length !== this.SIGFOX_DOWNLINK_LENGTH_EXACT) { + throw new BadRequestException(ErrorCodes.DownlinkLengthWrongForSigfox); } + } - private async createLoraDownlink(dto: CreateIoTDeviceDownlinkDto, cast: LoRaWANDevice): Promise { - const csDto: CreateChirpstackDeviceQueueItemDto = { - deviceQueueItem: { - fPort: dto.port, - devEUI: cast.deviceEUI, - confirmed: dto.confirmed, - data: this.hexBytesToBase64(dto.data), - }, - }; + private async createLoraDownlink(dto: CreateIoTDeviceDownlinkDto, cast: LoRaWANDevice): Promise { + const csDto: CreateChirpstackDeviceQueueItemDto = { + deviceQueueItem: { + fPort: dto.port, + devEUI: cast.deviceEUI, + confirmed: dto.confirmed, + data: this.hexBytesToBase64(dto.data), + }, + }; - try { - return this.chirpstackDeviceService.overwriteDownlink(csDto); - } catch (err) { - this.handleErrorsFromChirpstack(csDto, err); - } + try { + return this.chirpstackDeviceService.overwriteDownlink(csDto); + } catch (err) { + this.handleErrorsFromChirpstack(csDto, err); } + } - private handleErrorsFromChirpstack(csDto: CreateChirpstackDeviceQueueItemDto, err: any) { - this.logger.error( - `Error while trying to create downlink i chirpstack. DTO: '${JSON.stringify( - csDto - )}'. Error: '${JSON.stringify(err?.data)}'` - ); - if (err.status == 400) { - throw new BadRequestException("Error 400 from Chirpstack" + JSON.stringify(err?.data)); - } - throw new InternalServerErrorException("Could not send to chirpstack, try again later."); + private handleErrorsFromChirpstack(csDto: CreateChirpstackDeviceQueueItemDto, err: any) { + this.logger.error( + `Error while trying to create downlink i chirpstack. DTO: '${JSON.stringify(csDto)}'. Error: '${JSON.stringify( + err?.data + )}'` + ); + if (err.status == 400) { + throw new BadRequestException("Error 400 from Chirpstack" + JSON.stringify(err?.data)); } + throw new InternalServerErrorException("Could not send to chirpstack, try again later."); + } - private hexBytesToBase64(hexBytes: string): string { - return Buffer.from(hexBytes, "hex").toString("base64"); - } + private hexBytesToBase64(hexBytes: string): string { + return Buffer.from(hexBytes, "hex").toString("base64"); + } } diff --git a/src/services/device-management/iot-device-payload-decoder-data-target-connection.service.ts b/src/services/device-management/iot-device-payload-decoder-data-target-connection.service.ts index c2bd7918..6357e96d 100644 --- a/src/services/device-management/iot-device-payload-decoder-data-target-connection.service.ts +++ b/src/services/device-management/iot-device-payload-decoder-data-target-connection.service.ts @@ -2,13 +2,7 @@ import { off } from "process"; import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; -import { - DeleteResult, - FindOptionsWhere, - In, - Repository, - SelectQueryBuilder, -} from "typeorm"; +import { DeleteResult, FindOptionsWhere, In, Repository, SelectQueryBuilder } from "typeorm"; import { CreateIoTDevicePayloadDecoderDataTargetConnectionDto } from "@dto/create-iot-device-payload-decoder-data-target-connection.dto"; import { ListAllConnectionsResponseDto } from "@dto/list-all-connections-response.dto"; @@ -23,326 +17,271 @@ import { IoTDeviceService } from "./iot-device.service"; @Injectable() export class IoTDevicePayloadDecoderDataTargetConnectionService { - constructor( - @InjectRepository(IoTDevicePayloadDecoderDataTargetConnection) - private repository: Repository, - private ioTDeviceService: IoTDeviceService, - private dataTargetService: DataTargetService, - private payloadDecoderService: PayloadDecoderService - ) {} + constructor( + @InjectRepository(IoTDevicePayloadDecoderDataTargetConnection) + private repository: Repository, + private ioTDeviceService: IoTDeviceService, + private dataTargetService: DataTargetService, + private payloadDecoderService: PayloadDecoderService + ) {} - async findAndCountWithPagination( - query?: ListAllEntitiesDto, - allowed?: number[] - ): Promise { - if (allowed != undefined) { - return await this.findAndCountWithPaginationAndWhitelist(query, allowed); - } else { - return await this.findAllWithWhereQueryBuilder( - this.genDefaultQuery(), - query.limit, - query.offset, - query.sort - ); - } + async findAndCountWithPagination( + query?: ListAllEntitiesDto, + allowed?: number[] + ): Promise { + if (allowed != undefined) { + return await this.findAndCountWithPaginationAndWhitelist(query, allowed); + } else { + return await this.findAllWithWhereQueryBuilder(this.genDefaultQuery(), query.limit, query.offset, query.sort); } + } - async findAndCountWithPaginationAndWhitelist( - query?: ListAllEntitiesDto, - allowed?: number[] - ): Promise { - if (allowed.length === 0) { - return { - data: [], - count: 0, - }; - } - const innerQuery = this.genDefaultQuery().where("d.application In(:...appIds)", { - appIds: allowed, - }); - return await this.findAllWithWhereQueryBuilder( - innerQuery, - query.limit, - query.offset, - query.sort - ); + async findAndCountWithPaginationAndWhitelist( + query?: ListAllEntitiesDto, + allowed?: number[] + ): Promise { + if (allowed.length === 0) { + return { + data: [], + count: 0, + }; } + const innerQuery = this.genDefaultQuery().where("d.application In(:...appIds)", { + appIds: allowed, + }); + return await this.findAllWithWhereQueryBuilder(innerQuery, query.limit, query.offset, query.sort); + } - private async findAllWithWhere( - where?: FindOptionsWhere, - limit?: number, - offset?: number, - sort?: "ASC" | "DESC" | 1 | -1 - ): Promise { - const [result, total] = await this.repository.findAndCount({ - where: where || {}, - take: limit || 1000, - skip: offset || 0, - relations: [ - "iotDevices", - "payloadDecoder", - "dataTarget", - "iotDevices.application", - "dataTarget.application", - ], - order: { id: sort }, - }); + private async findAllWithWhere( + where?: FindOptionsWhere, + limit?: number, + offset?: number, + sort?: "ASC" | "DESC" | 1 | -1 + ): Promise { + const [result, total] = await this.repository.findAndCount({ + where: where || {}, + take: limit || 1000, + skip: offset || 0, + relations: ["iotDevices", "payloadDecoder", "dataTarget", "iotDevices.application", "dataTarget.application"], + order: { id: sort }, + }); - return { - data: result, - count: total, - }; - } + return { + data: result, + count: total, + }; + } - private async findAllWithWhereQueryBuilder( - query?: SelectQueryBuilder, - limit?: number, - offset?: number, - sort?: "ASC" | "DESC" | 1 | -1 - ): Promise { - const [result, total] = await query - .limit(limit || 1000) - .skip(offset || 0) - .orderBy("connection.id") - .getManyAndCount(); + private async findAllWithWhereQueryBuilder( + query?: SelectQueryBuilder, + limit?: number, + offset?: number, + sort?: "ASC" | "DESC" | 1 | -1 + ): Promise { + const [result, total] = await query + .limit(limit || 1000) + .skip(offset || 0) + .orderBy("connection.id") + .getManyAndCount(); - return { - data: result, - count: total, - }; - } + return { + data: result, + count: total, + }; + } - private genDefaultQuery() { - return this.repository - .createQueryBuilder("connection") - .innerJoinAndSelect("connection.iotDevices", "d") - .leftJoinAndSelect("connection.payloadDecoder", "pd") - .innerJoinAndSelect("connection.dataTarget", "dt") - .innerJoinAndSelect("d.application", "deviceApp") - .innerJoinAndSelect("dt.application", "dataTargetApp"); - } + private genDefaultQuery() { + return this.repository + .createQueryBuilder("connection") + .innerJoinAndSelect("connection.iotDevices", "d") + .leftJoinAndSelect("connection.payloadDecoder", "pd") + .innerJoinAndSelect("connection.dataTarget", "dt") + .innerJoinAndSelect("d.application", "deviceApp") + .innerJoinAndSelect("dt.application", "dataTargetApp"); + } - async findAllByIoTDeviceIdWithDeviceModel( - id: number - ): Promise { - const query = this.genDefaultQuery() - .leftJoinAndSelect("d.deviceModel", "dm") - .where("d.id = :deviceId", { - deviceId: id, - }); - return await this.findAllWithWhereQueryBuilder(query); - } + async findAllByIoTDeviceIdWithDeviceModel(id: number): Promise { + const query = this.genDefaultQuery().leftJoinAndSelect("d.deviceModel", "dm").where("d.id = :deviceId", { + deviceId: id, + }); + return await this.findAllWithWhereQueryBuilder(query); + } - async findAllByIoTDeviceId( - id: number, - allowed?: number[] - ): Promise { - if (allowed != undefined) { - if (allowed.length === 0) { - return { - data: [], - count: 0, - }; - } - const query = this.genDefaultQuery().where( - "d.id = :deviceId and d.application In(:...appIds)", - { - deviceId: id, - appIds: allowed, - } - ); - return await this.findAllWithWhereQueryBuilder(query); - } else { - const query = this.genDefaultQuery().where("d.id = :deviceId", { - deviceId: id, - }); - return await this.findAllWithWhereQueryBuilder(query); - } + async findAllByIoTDeviceId(id: number, allowed?: number[]): Promise { + if (allowed != undefined) { + if (allowed.length === 0) { + return { + data: [], + count: 0, + }; + } + const query = this.genDefaultQuery().where("d.id = :deviceId and d.application In(:...appIds)", { + deviceId: id, + appIds: allowed, + }); + return await this.findAllWithWhereQueryBuilder(query); + } else { + const query = this.genDefaultQuery().where("d.id = :deviceId", { + deviceId: id, + }); + return await this.findAllWithWhereQueryBuilder(query); } + } - async findAllByPayloadDecoderId( - id: number, - allowedOrganisations?: number[] - ): Promise { - if (allowedOrganisations != undefined) { - if (allowedOrganisations.length === 0) { - return { - data: [], - count: 0, - }; - } - const query = this.genDefaultQuery() - .innerJoin("deviceApp.belongsTo", "deviceOrg") - .innerJoin("dataTargetApp.belongsTo", "dataTargetOrg") - .where( - 'pd.id = :payloadDecoderId and "deviceOrg"."id" In(:...orgIds) and "dataTargetOrg"."id" In(:...orgIds)', - { - payloadDecoderId: id, - orgIds: allowedOrganisations, - } - ); - return await this.findAllWithWhereQueryBuilder(query); - } else { - return await this.findAllWithWhere({ - payloadDecoder: { id: id }, - }); - } + async findAllByPayloadDecoderId(id: number, allowedOrganisations?: number[]): Promise { + if (allowedOrganisations != undefined) { + if (allowedOrganisations.length === 0) { + return { + data: [], + count: 0, + }; + } + const query = this.genDefaultQuery() + .innerJoin("deviceApp.belongsTo", "deviceOrg") + .innerJoin("dataTargetApp.belongsTo", "dataTargetOrg") + .where( + 'pd.id = :payloadDecoderId and "deviceOrg"."id" In(:...orgIds) and "dataTargetOrg"."id" In(:...orgIds)', + { + payloadDecoderId: id, + orgIds: allowedOrganisations, + } + ); + return await this.findAllWithWhereQueryBuilder(query); + } else { + return await this.findAllWithWhere({ + payloadDecoder: { id: id }, + }); } + } - async findAllByDataTargetId( - id: number, - allowed?: number[] - ): Promise { - if (allowed != undefined) { - if (allowed.length === 0) { - return { - data: [], - count: 0, - }; - } - return await this.findAllWithWhere({ - dataTarget: { - id, - application: { - id: In(allowed), - }, - }, - }); - } else { - return await this.findAllWithWhere({ - dataTarget: { - id, - }, - }); - } + async findAllByDataTargetId(id: number, allowed?: number[]): Promise { + if (allowed != undefined) { + if (allowed.length === 0) { + return { + data: [], + count: 0, + }; + } + return await this.findAllWithWhere({ + dataTarget: { + id, + application: { + id: In(allowed), + }, + }, + }); + } else { + return await this.findAllWithWhere({ + dataTarget: { + id, + }, + }); } + } - async findOne(id: number): Promise { - try { - return await this.repository.findOneOrFail({ - where: { id }, - relations: [ - "iotDevices", - "payloadDecoder", - "dataTarget", - "iotDevices.application", - ], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - } catch (err) { - throw new NotFoundException( - `Could not find IoTDevicePayloadDecoderDataTargetConnection by id: ${id}` - ); - } + async findOne(id: number): Promise { + try { + return await this.repository.findOneOrFail({ + where: { id }, + relations: ["iotDevices", "payloadDecoder", "dataTarget", "iotDevices.application"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } catch (err) { + throw new NotFoundException(`Could not find IoTDevicePayloadDecoderDataTargetConnection by id: ${id}`); } + } - async create( - createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto, - userId: number - ): Promise { - const connection = new IoTDevicePayloadDecoderDataTargetConnection(); - - const mapped = await this.mapDtoToConnection(connection, createConnectionDto); - mapped.createdBy = userId; - mapped.updatedBy = userId; - return await this.repository.save(mapped); - } + async create( + createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto, + userId: number + ): Promise { + const connection = new IoTDevicePayloadDecoderDataTargetConnection(); - async update( - id: number, - updateConnectionDto: UpdateIoTDevicePayloadDecoderDataTargetConnectionDto, - userId: number - ): Promise { - let connection; - try { - connection = await this.repository.findOneByOrFail({ id }); - } catch (err) { - throw new NotFoundException( - `Could not find IoTDevicePayloadDecoderDataTargetConnection by id: ${id}` - ); - } + const mapped = await this.mapDtoToConnection(connection, createConnectionDto); + mapped.createdBy = userId; + mapped.updatedBy = userId; + return await this.repository.save(mapped); + } - const mapped = await this.mapDtoToConnection(connection, updateConnectionDto); - mapped.updatedBy = userId; - return await this.repository.save(mapped); + async update( + id: number, + updateConnectionDto: UpdateIoTDevicePayloadDecoderDataTargetConnectionDto, + userId: number + ): Promise { + let connection; + try { + connection = await this.repository.findOneByOrFail({ id }); + } catch (err) { + throw new NotFoundException(`Could not find IoTDevicePayloadDecoderDataTargetConnection by id: ${id}`); } - async delete(id: number): Promise { - return await this.repository.delete(id); - } + const mapped = await this.mapDtoToConnection(connection, updateConnectionDto); + mapped.updatedBy = userId; + return await this.repository.save(mapped); + } - private async mapDtoToConnection( - connection: IoTDevicePayloadDecoderDataTargetConnection, - createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto - ): Promise { - await this.mapIoTDevices(connection, createConnectionDto); - await this.mapDataTarget(connection, createConnectionDto); - await this.mapPayloadDecoder(createConnectionDto, connection); + async delete(id: number): Promise { + return await this.repository.delete(id); + } - if ( - connection.iotDevices.some( - x => x.application.id != connection.dataTarget.application.id - ) - ) { - throw new BadRequestException(ErrorCodes.NotSameApplication); - } + private async mapDtoToConnection( + connection: IoTDevicePayloadDecoderDataTargetConnection, + createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto + ): Promise { + await this.mapIoTDevices(connection, createConnectionDto); + await this.mapDataTarget(connection, createConnectionDto); + await this.mapPayloadDecoder(createConnectionDto, connection); - return connection; + if (connection.iotDevices.some(x => x.application.id != connection.dataTarget.application.id)) { + throw new BadRequestException(ErrorCodes.NotSameApplication); } - private async mapPayloadDecoder( - createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto, - connection: IoTDevicePayloadDecoderDataTargetConnection - ) { - if (createConnectionDto.payloadDecoderId !== undefined) { - if (createConnectionDto.payloadDecoderId === null) { - connection.payloadDecoder = null; - } else { - try { - connection.payloadDecoder = await this.payloadDecoderService.findOne( - createConnectionDto.payloadDecoderId - ); - } catch (err) { - throw new BadRequestException( - `Could not find PayloadDecoder by id: '${createConnectionDto.payloadDecoderId}'` - ); - } - } - } - } + return connection; + } - private async mapDataTarget( - connection: IoTDevicePayloadDecoderDataTargetConnection, - createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto - ) { + private async mapPayloadDecoder( + createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto, + connection: IoTDevicePayloadDecoderDataTargetConnection + ) { + if (createConnectionDto.payloadDecoderId !== undefined) { + if (createConnectionDto.payloadDecoderId === null) { + connection.payloadDecoder = null; + } else { try { - connection.dataTarget = await this.dataTargetService.findOne( - createConnectionDto.dataTargetId - ); + connection.payloadDecoder = await this.payloadDecoderService.findOne(createConnectionDto.payloadDecoderId); } catch (err) { - throw new BadRequestException( - `Could not find DataTarget by id: '${createConnectionDto.dataTargetId}'` - ); + throw new BadRequestException( + `Could not find PayloadDecoder by id: '${createConnectionDto.payloadDecoderId}'` + ); } + } } + } - private async mapIoTDevices( - connection: IoTDevicePayloadDecoderDataTargetConnection, - createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto - ) { - try { - connection.iotDevices = await this.ioTDeviceService.findManyByIds( - createConnectionDto.iotDeviceIds - ); - } catch (err) { - throw new BadRequestException( - `Could not find IoT-Device by id: '${createConnectionDto.iotDeviceIds}'` - ); - } - if (connection.iotDevices.length === 0) { - throw new BadRequestException(`Must contain at least one IoTDevice`); - } + private async mapDataTarget( + connection: IoTDevicePayloadDecoderDataTargetConnection, + createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto + ) { + try { + connection.dataTarget = await this.dataTargetService.findOne(createConnectionDto.dataTargetId); + } catch (err) { + throw new BadRequestException(`Could not find DataTarget by id: '${createConnectionDto.dataTargetId}'`); + } + } + + private async mapIoTDevices( + connection: IoTDevicePayloadDecoderDataTargetConnection, + createConnectionDto: CreateIoTDevicePayloadDecoderDataTargetConnectionDto + ) { + try { + connection.iotDevices = await this.ioTDeviceService.findManyByIds(createConnectionDto.iotDeviceIds); + } catch (err) { + throw new BadRequestException(`Could not find IoT-Device by id: '${createConnectionDto.iotDeviceIds}'`); + } + if (connection.iotDevices.length === 0) { + throw new BadRequestException(`Must contain at least one IoTDevice`); } + } } diff --git a/src/services/device-management/iot-device.service.ts b/src/services/device-management/iot-device.service.ts index 769938c7..69f3cd1b 100644 --- a/src/services/device-management/iot-device.service.ts +++ b/src/services/device-management/iot-device.service.ts @@ -9,9 +9,9 @@ import { CreateIoTDeviceMapDto } from "@dto/iot-device/create-iot-device-map.dto import { IotDeviceBatchResponseDto } from "@dto/iot-device/iot-device-batch-response.dto"; import { UpdateIoTDeviceBatchDto } from "@dto/iot-device/update-iot-device-batch.dto"; import { - IoTDeviceMinimal, - IoTDeviceMinimalRaw, - ListAllIoTDevicesMinimalResponseDto, + IoTDeviceMinimal, + IoTDeviceMinimalRaw, + ListAllIoTDevicesMinimalResponseDto, } from "@dto/list-all-iot-devices-minimal-response.dto"; import { LoRaWANDeviceWithChirpstackDataDto } from "@dto/lorawan-device-with-chirpstack-data.dto"; import { SigFoxDeviceWithBackendDataDto } from "@dto/sigfox-device-with-backend-data.dto"; @@ -33,18 +33,18 @@ import { IoTDeviceType } from "@enum/device-type.enum"; import { ActivationType } from "@enum/lorawan-activation-type.enum"; import { subtractDays } from "@helpers/date.helper"; import { - filterValidIotDeviceMaps, - isValidIoTDeviceMap, - mapAllDevicesByProcessed, - validateMQTTInternalBroker, - validateMQTTExternalBroker, + filterValidIotDeviceMaps, + isValidIoTDeviceMap, + mapAllDevicesByProcessed, + validateMQTTInternalBroker, + validateMQTTExternalBroker, } from "@helpers/iot-device.helper"; import { - BadRequestException, - Injectable, - InternalServerErrorException, - Logger, - NotFoundException, + BadRequestException, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException, } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { ChirpstackDeviceService } from "@services/chirpstack/chirpstack-device.service"; @@ -73,1088 +73,1074 @@ import { DeviceProfileService } from "@services/chirpstack/device-profile.servic import { ApplicationChirpstackService } from "@services/chirpstack/chirpstack-application.service"; type IoTDeviceOrSpecialized = - | IoTDevice - | LoRaWANDeviceWithChirpstackDataDto - | SigFoxDeviceWithBackendDataDto - | MQTTInternalBrokerDeviceDTO; + | IoTDevice + | LoRaWANDeviceWithChirpstackDataDto + | SigFoxDeviceWithBackendDataDto + | MQTTInternalBrokerDeviceDTO; @Injectable() export class IoTDeviceService { - constructor( - @InjectRepository(GenericHTTPDevice) - private genericHTTPDeviceRepository: Repository, - @InjectRepository(SigFoxDevice) - private sigfoxRepository: Repository, - @InjectRepository(IoTDevice) - private iotDeviceRepository: Repository, - @InjectRepository(LoRaWANDevice) - private loRaWANDeviceRepository: Repository, - @InjectRepository(MQTTInternalBrokerDevice) - private mqttInternalBrokerDeviceRepository: Repository, - @InjectRepository(MQTTExternalBrokerDevice) - private mqttExternalBrokerDeviceRepository: Repository, - private entityManager: EntityManager, - private applicationService: ApplicationService, - private chirpstackDeviceService: ChirpstackDeviceService, - private applicationChirpstackService: ApplicationChirpstackService, - private sigfoxApiDeviceService: SigFoxApiDeviceService, - private sigfoxApiDeviceTypeService: SigFoxApiDeviceTypeService, - private sigfoxGroupService: SigFoxGroupService, - private deviceModelService: DeviceModelService, - private ioTLoRaWANDeviceService: IoTLoRaWANDeviceService, - private sigfoxMessagesService: SigFoxMessagesService, - private mqttService: MqttService, - private internalMqttClientListenerService: InternalMqttClientListenerService, - private encryptionHelperService: EncryptionHelperService, - private csvGeneratorService: CsvGeneratorService, - private deviceProfileService: DeviceProfileService - ) {} - - private readonly logger = new Logger(IoTDeviceService.name); - - async findOne(id: number): Promise { - return await this.iotDeviceRepository.findOneOrFail({ - where: { id }, - relations: ["application"], - }); + constructor( + @InjectRepository(GenericHTTPDevice) + private genericHTTPDeviceRepository: Repository, + @InjectRepository(SigFoxDevice) + private sigfoxRepository: Repository, + @InjectRepository(IoTDevice) + private iotDeviceRepository: Repository, + @InjectRepository(LoRaWANDevice) + private loRaWANDeviceRepository: Repository, + @InjectRepository(MQTTInternalBrokerDevice) + private mqttInternalBrokerDeviceRepository: Repository, + @InjectRepository(MQTTExternalBrokerDevice) + private mqttExternalBrokerDeviceRepository: Repository, + private entityManager: EntityManager, + private applicationService: ApplicationService, + private chirpstackDeviceService: ChirpstackDeviceService, + private applicationChirpstackService: ApplicationChirpstackService, + private sigfoxApiDeviceService: SigFoxApiDeviceService, + private sigfoxApiDeviceTypeService: SigFoxApiDeviceTypeService, + private sigfoxGroupService: SigFoxGroupService, + private deviceModelService: DeviceModelService, + private ioTLoRaWANDeviceService: IoTLoRaWANDeviceService, + private sigfoxMessagesService: SigFoxMessagesService, + private mqttService: MqttService, + private internalMqttClientListenerService: InternalMqttClientListenerService, + private encryptionHelperService: EncryptionHelperService, + private csvGeneratorService: CsvGeneratorService, + private deviceProfileService: DeviceProfileService + ) {} + + private readonly logger = new Logger(IoTDeviceService.name); + + async findOne(id: number): Promise { + return await this.iotDeviceRepository.findOneOrFail({ + where: { id }, + relations: ["application"], + }); + } + + async findAllSigFoxDevices(): Promise { + return await this.sigfoxRepository.find(); + } + + async findManyByIds(iotDeviceIds: number[]): Promise { + if (iotDeviceIds == null || iotDeviceIds?.length == 0) { + return []; } - - async findAllSigFoxDevices(): Promise { - return await this.sigfoxRepository.find(); + return await this.iotDeviceRepository.find({ + where: { id: In(iotDeviceIds) }, + relations: ["application"], + }); + } + + async findOneWithApplicationAndMetadata(id: number, enrich?: boolean): Promise { + // Repository syntax doesn't yet support ordering by relation: https://github.com/typeorm/typeorm/issues/2620 + // Therefore we use the QueryBuilder ... + const iotDevice = await this.queryDatabaseForIoTDevice(id); + + if (iotDevice == null) { + throw new NotFoundException(); } - - async findManyByIds(iotDeviceIds: number[]): Promise { - if (iotDeviceIds == null || iotDeviceIds?.length == 0) { - return []; - } - return await this.iotDeviceRepository.find({ - where: { id: In(iotDeviceIds) }, - relations: ["application"], - }); + if (enrich) { + if (iotDevice.type == IoTDeviceType.LoRaWAN) { + // Add more suplimental info about LoRaWAN devices. + return await this.chirpstackDeviceService.enrichLoRaWANDevice(iotDevice); + } else if (iotDevice.type == IoTDeviceType.SigFox) { + // Add more info about SigFox devices + return await this.enrichSigFoxDevice(iotDevice); + } else if (iotDevice.type === IoTDeviceType.MQTTInternalBroker) { + return await this.enrichMQTTInternalBrokerDevice(iotDevice); + } else if (iotDevice.type === IoTDeviceType.MQTTExternalBroker) { + return await this.enrichMQTTExternalBrokerDevice(iotDevice); + } } - async findOneWithApplicationAndMetadata(id: number, enrich?: boolean): Promise { - // Repository syntax doesn't yet support ordering by relation: https://github.com/typeorm/typeorm/issues/2620 - // Therefore we use the QueryBuilder ... - const iotDevice = await this.queryDatabaseForIoTDevice(id); - - if (iotDevice == null) { - throw new NotFoundException(); - } - if (enrich) { - if (iotDevice.type == IoTDeviceType.LoRaWAN) { - // Add more suplimental info about LoRaWAN devices. - return await this.chirpstackDeviceService.enrichLoRaWANDevice(iotDevice); - } else if (iotDevice.type == IoTDeviceType.SigFox) { - // Add more info about SigFox devices - return await this.enrichSigFoxDevice(iotDevice); - } else if (iotDevice.type === IoTDeviceType.MQTTInternalBroker) { - return await this.enrichMQTTInternalBrokerDevice(iotDevice); - } else if (iotDevice.type === IoTDeviceType.MQTTExternalBroker) { - return await this.enrichMQTTExternalBrokerDevice(iotDevice); - } - } - - return iotDevice; - } - - async findManyWithApplicationAndMetadata(ids: number[]): Promise { - return this.queryDatabaseForIoTDevices(ids); - } - - async enrichSigFoxDevice(iotDevice: IoTDevice): Promise { - const sigfoxDevice = iotDevice as SigFoxDeviceWithBackendDataDto; - - const application = await this.applicationService.findOneWithOrganisation(iotDevice.application.id); + return iotDevice; + } - const sigfoxGroup = await this.sigfoxGroupService.findOneByGroupId( - sigfoxDevice.groupId, - application.belongsTo.id - ); + async findManyWithApplicationAndMetadata(ids: number[]): Promise { + return this.queryDatabaseForIoTDevices(ids); + } - const thisDevice = await this.getDataFromSigFoxAboutDevice(sigfoxGroup, sigfoxDevice); - if (!thisDevice) { - throw new NotFoundException(ErrorCodes.SigfoxError); - } - sigfoxDevice.sigfoxSettings = await this.mapSigFoxBackendDataToDto(thisDevice, sigfoxGroup); + async enrichSigFoxDevice(iotDevice: IoTDevice): Promise { + const sigfoxDevice = iotDevice as SigFoxDeviceWithBackendDataDto; - return sigfoxDevice; - } + const application = await this.applicationService.findOneWithOrganisation(iotDevice.application.id); - async mapSigFoxBackendDataToDto( - thisDevice: SigFoxApiDeviceContent, - sigfoxGroup: SigFoxGroup - ): Promise { - return { - deviceId: thisDevice.id, - deviceTypeId: thisDevice.deviceType.id, - deviceTypeName: thisDevice.deviceType.name, - groupId: sigfoxGroup.id, - groupName: thisDevice.group.name, - connectToExistingDeviceInBackend: true, - pac: thisDevice.pac, - endProductCertificate: thisDevice.productCertificate.key, - prototype: thisDevice.prototype, - }; - } + const sigfoxGroup = await this.sigfoxGroupService.findOneByGroupId(sigfoxDevice.groupId, application.belongsTo.id); - async findAllByPayloadDecoder( - req: AuthenticatedRequest, - payloadDecoderId: number, - limit: number, - offset: number - ): Promise { - const data: Promise = this.getQueryForFindAllByPayloadDecoder(payloadDecoderId) - .addSelect('"application"."id"', "applicationId") - .addSelect('"application"."belongsToId"', "organizationId") - .limit(limit) - .offset(offset) - .getRawMany(); - - const count = this.getQueryForFindAllByPayloadDecoder(payloadDecoderId).getCount(); - - const transformedData: IoTDeviceMinimal[] = await this.mapToIoTDeviceMinimal(data, req); - - return { - data: transformedData, - count: await count, - }; + const thisDevice = await this.getDataFromSigFoxAboutDevice(sigfoxGroup, sigfoxDevice); + if (!thisDevice) { + throw new NotFoundException(ErrorCodes.SigfoxError); } - - private async mapToIoTDeviceMinimal( - data: Promise, - req: AuthenticatedRequest - ): Promise { - const applications = req.user.permissions.getAllApplicationsWithAtLeastRead(); - const organizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); - return (await data).map(x => { - return { - id: x.id, - name: x.name, - lastActiveTime: x.sentTime != null ? x.sentTime : null, - organizationId: x.organizationId, - applicationId: x.applicationId, - canRead: this.hasAccessToIoTDevice(x, applications, organizations, req), - }; - }); - } - - private hasAccessToIoTDevice( - x: IoTDeviceMinimalRaw, - apps: number[], - orgs: number[], - req: AuthenticatedRequest - ): boolean { - if (req.user.permissions.isGlobalAdmin) { - return true; - } else if (orgs.some(orgId => orgId == x.organizationId)) { - return true; - } else if (apps.some(appId => appId == x.applicationId)) { - return true; - } - return false; + sigfoxDevice.sigfoxSettings = await this.mapSigFoxBackendDataToDto(thisDevice, sigfoxGroup); + + return sigfoxDevice; + } + + async mapSigFoxBackendDataToDto( + thisDevice: SigFoxApiDeviceContent, + sigfoxGroup: SigFoxGroup + ): Promise { + return { + deviceId: thisDevice.id, + deviceTypeId: thisDevice.deviceType.id, + deviceTypeName: thisDevice.deviceType.name, + groupId: sigfoxGroup.id, + groupName: thisDevice.group.name, + connectToExistingDeviceInBackend: true, + pac: thisDevice.pac, + endProductCertificate: thisDevice.productCertificate.key, + prototype: thisDevice.prototype, + }; + } + + async findAllByPayloadDecoder( + req: AuthenticatedRequest, + payloadDecoderId: number, + limit: number, + offset: number + ): Promise { + const data: Promise = this.getQueryForFindAllByPayloadDecoder(payloadDecoderId) + .addSelect('"application"."id"', "applicationId") + .addSelect('"application"."belongsToId"', "organizationId") + .limit(limit) + .offset(offset) + .getRawMany(); + + const count = this.getQueryForFindAllByPayloadDecoder(payloadDecoderId).getCount(); + + const transformedData: IoTDeviceMinimal[] = await this.mapToIoTDeviceMinimal(data, req); + + return { + data: transformedData, + count: await count, + }; + } + + private async mapToIoTDeviceMinimal( + data: Promise, + req: AuthenticatedRequest + ): Promise { + const applications = req.user.permissions.getAllApplicationsWithAtLeastRead(); + const organizations = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); + return (await data).map(x => { + return { + id: x.id, + name: x.name, + lastActiveTime: x.sentTime != null ? x.sentTime : null, + organizationId: x.organizationId, + applicationId: x.applicationId, + canRead: this.hasAccessToIoTDevice(x, applications, organizations, req), + }; + }); + } + + private hasAccessToIoTDevice( + x: IoTDeviceMinimalRaw, + apps: number[], + orgs: number[], + req: AuthenticatedRequest + ): boolean { + if (req.user.permissions.isGlobalAdmin) { + return true; + } else if (orgs.some(orgId => orgId == x.organizationId)) { + return true; + } else if (apps.some(appId => appId == x.applicationId)) { + return true; } - - private getQueryForFindAllByPayloadDecoder(payloadDecoderId: number): SelectQueryBuilder { - return this.iotDeviceRepository - .createQueryBuilder("device") - .innerJoin("device.application", "application") - .innerJoin("device.connections", "connection") - .leftJoin("device.latestReceivedMessage", "receivedMessage") - .where('"connection"."payloadDecoderId" = :id', { id: payloadDecoderId }) - .orderBy("device.id") - .select(['"device"."id"', '"device"."name"', '"receivedMessage"."sentTime"']); + return false; + } + + private getQueryForFindAllByPayloadDecoder(payloadDecoderId: number): SelectQueryBuilder { + return this.iotDeviceRepository + .createQueryBuilder("device") + .innerJoin("device.application", "application") + .innerJoin("device.connections", "connection") + .leftJoin("device.latestReceivedMessage", "receivedMessage") + .where('"connection"."payloadDecoderId" = :id', { id: payloadDecoderId }) + .orderBy("device.id") + .select(['"device"."id"', '"device"."name"', '"receivedMessage"."sentTime"']); + } + + /** + * Avoid calling the endpoint /devices/:id at SigFox + * https://support.sigfox.com/docs/api-rate-limiting + */ + private async getDataFromSigFoxAboutDevice(sigfoxGroup: SigFoxGroup, sigfoxDevice: SigFoxDeviceWithBackendDataDto) { + const allDevices = await this.sigfoxApiDeviceService.getAllByGroupIds(sigfoxGroup, [sigfoxDevice.groupId]); + + const thisDevice = allDevices.data.find(x => x.id == sigfoxDevice.deviceId); + return thisDevice; + } + + private buildIoTDeviceWithRelationsQuery(): SelectQueryBuilder { + return this.iotDeviceRepository + .createQueryBuilder("iot_device") + .loadAllRelationIds({ relations: ["createdBy", "updatedBy"] }) + .innerJoinAndSelect("iot_device.application", "application", 'application.id = iot_device."applicationId"') + .leftJoinAndSelect("iot_device.receivedMessagesMetadata", "metadata", 'metadata."deviceId" = iot_device.id') + .leftJoinAndSelect( + "iot_device.latestReceivedMessage", + "receivedMessage", + '"receivedMessage"."deviceId" = iot_device.id' + ) + .leftJoinAndSelect("iot_device.deviceModel", "device_model", 'device_model.id = iot_device."deviceModelId"') + .orderBy('metadata."sentTime"', "DESC"); + } + + private async queryDatabaseForIoTDevice(id: number) { + return await this.buildIoTDeviceWithRelationsQuery().where("iot_device.id = :id", { id: id }).getOne(); + } + + private queryDatabaseForIoTDevices(ids: number[]) { + return this.buildIoTDeviceWithRelationsQuery().where("iot_device.id IN (:...ids)", { ids }).getMany(); + } + + async findGenericHttpDeviceByApiKey(key: string): Promise { + return await this.genericHTTPDeviceRepository.findOneBy({ apiKey: key }); + } + + async findSigFoxDeviceByDeviceIdAndDeviceTypeId(deviceId: string, apiKey: string): Promise { + return await this.sigfoxRepository.findOneBy({ + deviceId, + deviceTypeId: apiKey, + }); + } + + async findLoRaWANDeviceByDeviceEUI(deviceEUI: string): Promise { + return await this.loRaWANDeviceRepository.findOneBy({ + deviceEUI: ILike(deviceEUI), + }); + } + + async findNonEnrichedLoRaWANDevices(): Promise { + return await this.loRaWANDeviceRepository + .createQueryBuilder("iot_device") + .where('"OTAAapplicationKey" is null') + .getMany(); + } + + async updateLocalLoRaWANDevices(devices: LoRaWANDevice[]): Promise { + await this.loRaWANDeviceRepository.save(devices); + } + + async findMQTTDevice(id: number): Promise { + return await this.mqttInternalBrokerDeviceRepository.findOne({ + where: { id }, + }); + } + + async getMqttSuperUser(): Promise { + return await this.mqttInternalBrokerDeviceRepository.findOne({ + where: { permissions: MQTTPermissionLevel.superUser }, + }); + } + + async getAllValidMQTTExternalBrokers(): Promise { + return await this.mqttExternalBrokerDeviceRepository.find({ + where: { type: IoTDeviceType.MQTTExternalBroker, invalidMqttConfig: false }, + }); + } + + async create(createIoTDeviceDto: CreateIoTDeviceDto, userId: number): Promise { + // Reuse the same logic for creating multiple devices. + const iotDevice = await this.createMany([createIoTDeviceDto], userId); + + // We passed in 1 device, so we expect the output to contain 1 device as well + if (iotDevice[0].error) throw new BadRequestException(iotDevice[0].error); + return iotDevice[0].data; + } + + async createMany(createIoTDeviceDtos: CreateIoTDeviceDto[], userId: number): Promise { + const iotDevicesMaps: CreateIoTDeviceMapDto[] = []; + + // Translate each generic device to the specific type + createIoTDeviceDtos.forEach(createIotDevice => { + try { + const deviceType = iotDeviceTypeMap[createIotDevice.type]; + const iotDevice = new deviceType(); + iotDevicesMaps.push({ iotDeviceDto: createIotDevice, iotDevice }); + } catch (error) { + iotDevicesMaps.push({ iotDeviceDto: createIotDevice, error }); + } + }); + + await this.validateDtoAndCreateIoTDevice( + // Don't process any devices whose type couldn't be determined + filterValidIotDeviceMaps(iotDevicesMaps), + false + ); + // Filter any device which failed validation or couldn't be created + const validProcessedDevices = filterValidIotDeviceMaps(iotDevicesMaps); + + for (const mappedIotDevice of validProcessedDevices) { + if (mappedIotDevice.iotDevice) { + mappedIotDevice.iotDevice.createdBy = userId; + mappedIotDevice.iotDevice.updatedBy = userId; + } } - /** - * Avoid calling the endpoint /devices/:id at SigFox - * https://support.sigfox.com/docs/api-rate-limiting - */ - private async getDataFromSigFoxAboutDevice(sigfoxGroup: SigFoxGroup, sigfoxDevice: SigFoxDeviceWithBackendDataDto) { - const allDevices = await this.sigfoxApiDeviceService.getAllByGroupIds(sigfoxGroup, [sigfoxDevice.groupId]); - - const thisDevice = allDevices.data.find(x => x.id == sigfoxDevice.deviceId); - return thisDevice; - } - - private buildIoTDeviceWithRelationsQuery(): SelectQueryBuilder { - return this.iotDeviceRepository - .createQueryBuilder("iot_device") - .loadAllRelationIds({ relations: ["createdBy", "updatedBy"] }) - .innerJoinAndSelect("iot_device.application", "application", 'application.id = iot_device."applicationId"') - .leftJoinAndSelect("iot_device.receivedMessagesMetadata", "metadata", 'metadata."deviceId" = iot_device.id') - .leftJoinAndSelect( - "iot_device.latestReceivedMessage", - "receivedMessage", - '"receivedMessage"."deviceId" = iot_device.id' - ) - .leftJoinAndSelect("iot_device.deviceModel", "device_model", 'device_model.id = iot_device."deviceModelId"') - .orderBy('metadata."sentTime"', "DESC"); - } - - private async queryDatabaseForIoTDevice(id: number) { - return await this.buildIoTDeviceWithRelationsQuery().where("iot_device.id = :id", { id: id }).getOne(); - } - - private queryDatabaseForIoTDevices(ids: number[]) { - return this.buildIoTDeviceWithRelationsQuery().where("iot_device.id IN (:...ids)", { ids }).getMany(); - } - - async findGenericHttpDeviceByApiKey(key: string): Promise { - return await this.genericHTTPDeviceRepository.findOneBy({ apiKey: key }); - } - - async findSigFoxDeviceByDeviceIdAndDeviceTypeId(deviceId: string, apiKey: string): Promise { - return await this.sigfoxRepository.findOneBy({ - deviceId, - deviceTypeId: apiKey, - }); - } - - async findLoRaWANDeviceByDeviceEUI(deviceEUI: string): Promise { - return await this.loRaWANDeviceRepository.findOneBy({ - deviceEUI: ILike(deviceEUI), - }); - } - - async findNonEnrichedLoRaWANDevices(): Promise { - return await this.loRaWANDeviceRepository - .createQueryBuilder("iot_device") - .where('"OTAAapplicationKey" is null') - .getMany(); - } - - async updateLocalLoRaWANDevices(devices: LoRaWANDevice[]): Promise { - await this.loRaWANDeviceRepository.save(devices); - } - - async findMQTTDevice(id: number): Promise { - return await this.mqttInternalBrokerDeviceRepository.findOne({ - where: { id }, - }); - } - - async getMqttSuperUser(): Promise { - return await this.mqttInternalBrokerDeviceRepository.findOne({ - where: { permissions: MQTTPermissionLevel.superUser }, - }); - } - - async getAllValidMQTTExternalBrokers(): Promise { - return await this.mqttExternalBrokerDeviceRepository.find({ - where: { type: IoTDeviceType.MQTTExternalBroker, invalidMqttConfig: false }, - }); - } - - async create(createIoTDeviceDto: CreateIoTDeviceDto, userId: number): Promise { - // Reuse the same logic for creating multiple devices. - const iotDevice = await this.createMany([createIoTDeviceDto], userId); - - // We passed in 1 device, so we expect the output to contain 1 device as well - if (iotDevice[0].error) throw new BadRequestException(iotDevice[0].error); - return iotDevice[0].data; + // Store or update valid devices on the database + const validIotDevices = validProcessedDevices.map(iotDeviceMap => iotDeviceMap.iotDevice); + const dbIotDevices = validIotDevices.length ? await this.iotDeviceRepository.save(validIotDevices) : []; + + // Set deviceId related values on new mqtt devices + await this.handleNewMQTTDevices(dbIotDevices); + + // Return a new list with all processed and failed devices + return iotDevicesMaps.map(mapAllDevicesByProcessed(dbIotDevices)); + } + + async save(iotDevice: IoTDevice): Promise { + return await this.iotDeviceRepository.save(iotDevice); + } + + async removeDownlink(sigfoxDevice: SigFoxDevice): Promise { + this.logger.log(`Removing downlink from device(${sigfoxDevice.id}) sigfoxId(${sigfoxDevice.deviceId})`); + sigfoxDevice.downlinkPayload = null; + return await this.iotDeviceRepository.save(sigfoxDevice); + } + + async getDownlinkForSigfox(device: SigFoxDevice): Promise { + if (device.downlinkPayload != null) { + return { + totalCount: 1, + deviceQueueItems: [ + { + data: device.downlinkPayload, + }, + ], + }; } - - async createMany(createIoTDeviceDtos: CreateIoTDeviceDto[], userId: number): Promise { - const iotDevicesMaps: CreateIoTDeviceMapDto[] = []; - - // Translate each generic device to the specific type - createIoTDeviceDtos.forEach(createIotDevice => { - try { - const deviceType = iotDeviceTypeMap[createIotDevice.type]; - const iotDevice = new deviceType(); - iotDevicesMaps.push({ iotDeviceDto: createIotDevice, iotDevice }); - } catch (error) { - iotDevicesMaps.push({ iotDeviceDto: createIotDevice, error }); - } - }); - - await this.validateDtoAndCreateIoTDevice( - // Don't process any devices whose type couldn't be determined - filterValidIotDeviceMaps(iotDevicesMaps), - false - ); - // Filter any device which failed validation or couldn't be created - const validProcessedDevices = filterValidIotDeviceMaps(iotDevicesMaps); - - for (const mappedIotDevice of validProcessedDevices) { - if (mappedIotDevice.iotDevice) { - mappedIotDevice.iotDevice.createdBy = userId; - mappedIotDevice.iotDevice.updatedBy = userId; - } - } - - // Store or update valid devices on the database - const validIotDevices = validProcessedDevices.map(iotDeviceMap => iotDeviceMap.iotDevice); - const dbIotDevices = validIotDevices.length ? await this.iotDeviceRepository.save(validIotDevices) : []; - - // Set deviceId related values on new mqtt devices - await this.handleNewMQTTDevices(dbIotDevices); - - // Return a new list with all processed and failed devices - return iotDevicesMaps.map(mapAllDevicesByProcessed(dbIotDevices)); - } - - async save(iotDevice: IoTDevice): Promise { - return await this.iotDeviceRepository.save(iotDevice); - } - - async removeDownlink(sigfoxDevice: SigFoxDevice): Promise { - this.logger.log(`Removing downlink from device(${sigfoxDevice.id}) sigfoxId(${sigfoxDevice.deviceId})`); - sigfoxDevice.downlinkPayload = null; - return await this.iotDeviceRepository.save(sigfoxDevice); - } - - async getDownlinkForSigfox(device: SigFoxDevice): Promise { - if (device.downlinkPayload != null) { - return { - totalCount: 1, - deviceQueueItems: [ - { - data: device.downlinkPayload, - }, - ], - }; - } - return { - totalCount: 0, - deviceQueueItems: [], - }; - } - - async update(id: number, updateDto: UpdateIoTDeviceDto, userId: number): Promise { - const existingIoTDevice = await this.iotDeviceRepository.findOneByOrFail({ id }); - const iotDeviceDtoMap: CreateIoTDeviceMapDto[] = [{ iotDevice: existingIoTDevice, iotDeviceDto: updateDto }]; - await this.validateDtoAndCreateIoTDevice(iotDeviceDtoMap, true); - - const mappedIoTDevice = iotDeviceDtoMap[0].iotDevice; - mappedIoTDevice.updatedBy = userId; - const res = await this.iotDeviceRepository.save(mappedIoTDevice); - - return res; - } - - async updateMany(updateDto: UpdateIoTDeviceBatchDto, userId: number): Promise { - // Fetch existing devices from db and map them - const existingDevices = await this.iotDeviceRepository.findBy({ - id: In(updateDto.data.map(device => device.id)), - }); - const iotDeviceMaps: CreateIoTDeviceMapDto[] = updateDto.data.map(updateDevice => ({ - iotDeviceDto: updateDevice, - iotDevice: existingDevices.find(existingDevice => existingDevice.id === updateDevice.id), - })); - await this.validateDtoAndCreateIoTDevice(iotDeviceMaps, true); - - const validDevices = iotDeviceMaps.reduce((res: IoTDevice[], currentMap) => { - if (isValidIoTDeviceMap(currentMap)) { - currentMap.iotDevice.updatedBy = userId; - res.push(currentMap.iotDevice); - } - - return res; - }, []); - const dbIotDevices = await this.iotDeviceRepository.save(validDevices); - - // Return a new list with all processed and failed devices - return iotDeviceMaps.map(mapAllDevicesByProcessed(dbIotDevices)); - } - - async delete(device: IoTDevice): Promise { - let deleteResult: DeleteResult = { - raw: null, - affected: 0, - }; - - // Run these operations inside a TypeORM transaction to make sure if operations on chirpstack fail, we - // roll back any changes on the database to ensure consistency between devices in the OS2iot database - // and the Chirpstack database. - await this.entityManager.transaction(async transactionManager => { - // Find and delete m-n relations with the device first - await this.deleteMulticastsFromDevice(transactionManager, device); - const iotDeviceRepository = transactionManager.getRepository(IoTDevice); - - // Remove all data target connections before deleting the device. We can't do the same thing - // with multicasts as the relation isn't on the IoT device entity (bug) - device.connections = []; - await iotDeviceRepository.save(device); - deleteResult = await transactionManager.delete(IoTDevice, device.id); - - // Now we can safely perform any actions against Chirpstack - if (device.type === IoTDeviceType.LoRaWAN) { - const lorawanDevice = device as LoRaWANDevice; - this.logger.debug( - `Deleting LoRaWANDevice ${lorawanDevice.id} / ${lorawanDevice.deviceEUI} in Chirpstack ...` - ); - - await this.chirpstackDeviceService.deleteDevice(lorawanDevice.deviceEUI); - } else if (device.type === IoTDeviceType.MQTTExternalBroker) { - this.internalMqttClientListenerService.removeMQTTClient(device as MQTTExternalBrokerDevice); - } - }); - - return deleteResult; - } - - async markMqttExternalBrokerAsInvalid(device: MQTTExternalBrokerDevice) { - device.invalidMqttConfig = true; - await this.mqttExternalBrokerDeviceRepository.save(device); - } - - private async deleteMulticastsFromDevice(manager: EntityManager, device: IoTDevice) { - const multicastRepository = manager.getRepository(Multicast); - // Take only the multicasts with references to the device. - // Filtering by device id won't return multicasts with all devices - const _multicasts = await multicastRepository - .createQueryBuilder("multicast") - .innerJoinAndSelect("multicast.iotDevices", "iot_device") - .getMany(); - - // Filter multicasts without the device out to avoid updating them - const multicastsWithDevice = _multicasts.filter(multicast => - multicast.iotDevices.some(iotDevice => iotDevice.id === device.id) - ); - // Remove device from the mappings. It's important that each existing mapping has been fetched - // as the new mappings will replace the existing ones. - multicastsWithDevice.forEach( - multicast => (multicast.iotDevices = multicast.iotDevices?.filter(iotDevice => iotDevice.id !== device.id)) - ); - await multicastRepository.save(multicastsWithDevice); - } - - async deleteMany(ids: number[]): Promise { - return this.iotDeviceRepository.delete(ids); - } - - async findStats(device: IoTDevice): Promise { - const toDate = new Date(); - const fromDate = subtractDays(toDate, 30); - - switch (device.type) { - case IoTDeviceType.LoRaWAN: - const loraData = await this.chirpstackDeviceService.getStats((device as LoRaWANDevice).deviceEUI); - - return loraData.result.map(loraStat => ({ - timestamp: loraStat.timestamp, - rssi: loraStat.gwRssi, - snr: loraStat.gwSnr, - rxPacketsPerDr: loraStat.rxPacketsPerDr, - })); - case IoTDeviceType.SigFox: - const sigFoxData = await this.sigfoxMessagesService.getMessageSignals(device.id, fromDate, toDate); - - // SigFox data might contain data points on the same day. They have to be averaged - const sortedStats = sigFoxData - .map(data => ({ - timestamp: data.sentTime.toISOString(), - rssi: data.rssi, - snr: data.snr, - })) - .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); - const averagedStats = this.averageStatsForSameDay(sortedStats); - return averagedStats; - default: - return null; - } - } - - private averageStatsForSameDay(stats: DeviceStatsResponseDto[]) { - const statsSummed = stats.reduce( - (res: Record, item) => { - // Assume that the date is ISO formatted and extract only the date. - const dateWithoutTime = item.timestamp.split("T")[0]; - res[dateWithoutTime] = res.hasOwnProperty(dateWithoutTime) - ? { - count: res[dateWithoutTime].count + 1, - timestamp: item.timestamp, - rssi: res[dateWithoutTime].rssi + item.rssi, - snr: res[dateWithoutTime].snr + item.snr, - } - : { - count: 1, - timestamp: item.timestamp, - rssi: item.rssi, - snr: item.snr, - }; - return res; - }, - {} - ); - - const averagedStats: DeviceStatsResponseDto[] = Object.entries(statsSummed).map(([_key, item]) => ({ - timestamp: item.timestamp, - rssi: item.rssi / item.count, - snr: item.snr / item.count, + return { + totalCount: 0, + deviceQueueItems: [], + }; + } + + async update(id: number, updateDto: UpdateIoTDeviceDto, userId: number): Promise { + const existingIoTDevice = await this.iotDeviceRepository.findOneByOrFail({ id }); + const iotDeviceDtoMap: CreateIoTDeviceMapDto[] = [{ iotDevice: existingIoTDevice, iotDeviceDto: updateDto }]; + await this.validateDtoAndCreateIoTDevice(iotDeviceDtoMap, true); + + const mappedIoTDevice = iotDeviceDtoMap[0].iotDevice; + mappedIoTDevice.updatedBy = userId; + const res = await this.iotDeviceRepository.save(mappedIoTDevice); + + return res; + } + + async updateMany(updateDto: UpdateIoTDeviceBatchDto, userId: number): Promise { + // Fetch existing devices from db and map them + const existingDevices = await this.iotDeviceRepository.findBy({ + id: In(updateDto.data.map(device => device.id)), + }); + const iotDeviceMaps: CreateIoTDeviceMapDto[] = updateDto.data.map(updateDevice => ({ + iotDeviceDto: updateDevice, + iotDevice: existingDevices.find(existingDevice => existingDevice.id === updateDevice.id), + })); + await this.validateDtoAndCreateIoTDevice(iotDeviceMaps, true); + + const validDevices = iotDeviceMaps.reduce((res: IoTDevice[], currentMap) => { + if (isValidIoTDeviceMap(currentMap)) { + currentMap.iotDevice.updatedBy = userId; + res.push(currentMap.iotDevice); + } + + return res; + }, []); + const dbIotDevices = await this.iotDeviceRepository.save(validDevices); + + // Return a new list with all processed and failed devices + return iotDeviceMaps.map(mapAllDevicesByProcessed(dbIotDevices)); + } + + async delete(device: IoTDevice): Promise { + let deleteResult: DeleteResult = { + raw: null, + affected: 0, + }; + + // Run these operations inside a TypeORM transaction to make sure if operations on chirpstack fail, we + // roll back any changes on the database to ensure consistency between devices in the OS2iot database + // and the Chirpstack database. + await this.entityManager.transaction(async transactionManager => { + // Find and delete m-n relations with the device first + await this.deleteMulticastsFromDevice(transactionManager, device); + const iotDeviceRepository = transactionManager.getRepository(IoTDevice); + + // Remove all data target connections before deleting the device. We can't do the same thing + // with multicasts as the relation isn't on the IoT device entity (bug) + device.connections = []; + await iotDeviceRepository.save(device); + deleteResult = await transactionManager.delete(IoTDevice, device.id); + + // Now we can safely perform any actions against Chirpstack + if (device.type === IoTDeviceType.LoRaWAN) { + const lorawanDevice = device as LoRaWANDevice; + this.logger.debug(`Deleting LoRaWANDevice ${lorawanDevice.id} / ${lorawanDevice.deviceEUI} in Chirpstack ...`); + + await this.chirpstackDeviceService.deleteDevice(lorawanDevice.deviceEUI); + } else if (device.type === IoTDeviceType.MQTTExternalBroker) { + this.internalMqttClientListenerService.removeMQTTClient(device as MQTTExternalBrokerDevice); + } + }); + + return deleteResult; + } + + async markMqttExternalBrokerAsInvalid(device: MQTTExternalBrokerDevice) { + device.invalidMqttConfig = true; + await this.mqttExternalBrokerDeviceRepository.save(device); + } + + private async deleteMulticastsFromDevice(manager: EntityManager, device: IoTDevice) { + const multicastRepository = manager.getRepository(Multicast); + // Take only the multicasts with references to the device. + // Filtering by device id won't return multicasts with all devices + const _multicasts = await multicastRepository + .createQueryBuilder("multicast") + .innerJoinAndSelect("multicast.iotDevices", "iot_device") + .getMany(); + + // Filter multicasts without the device out to avoid updating them + const multicastsWithDevice = _multicasts.filter(multicast => + multicast.iotDevices.some(iotDevice => iotDevice.id === device.id) + ); + // Remove device from the mappings. It's important that each existing mapping has been fetched + // as the new mappings will replace the existing ones. + multicastsWithDevice.forEach( + multicast => (multicast.iotDevices = multicast.iotDevices?.filter(iotDevice => iotDevice.id !== device.id)) + ); + await multicastRepository.save(multicastsWithDevice); + } + + async deleteMany(ids: number[]): Promise { + return this.iotDeviceRepository.delete(ids); + } + + async findStats(device: IoTDevice): Promise { + const toDate = new Date(); + const fromDate = subtractDays(toDate, 30); + + switch (device.type) { + case IoTDeviceType.LoRaWAN: + const loraData = await this.chirpstackDeviceService.getStats((device as LoRaWANDevice).deviceEUI); + + return loraData.result.map(loraStat => ({ + timestamp: loraStat.timestamp, + rssi: loraStat.gwRssi, + snr: loraStat.gwSnr, + rxPacketsPerDr: loraStat.rxPacketsPerDr, })); - + case IoTDeviceType.SigFox: + const sigFoxData = await this.sigfoxMessagesService.getMessageSignals(device.id, fromDate, toDate); + + // SigFox data might contain data points on the same day. They have to be averaged + const sortedStats = sigFoxData + .map(data => ({ + timestamp: data.sentTime.toISOString(), + rssi: data.rssi, + snr: data.snr, + })) + .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); + const averagedStats = this.averageStatsForSameDay(sortedStats); return averagedStats; + default: + return null; } - - /** - * Validate and map info. from the dto onto an IoT device. This device is then created or updated - * as one of the final steps. I.e. valid chirpstack devices will be created in Chirpstack - * @param iotDeviceMaps - * @param isUpdate - */ - private async validateDtoAndCreateIoTDevice( - iotDeviceMaps: CreateIoTDeviceMapDto[], - isUpdate: boolean - ): Promise { - const applicationIds = iotDeviceMaps.reduce((res: number[], { iotDeviceDto }) => { - if (iotDeviceDto.applicationId) { - res.push(iotDeviceDto.applicationId); - } - - return res; - }, []); - // Pre-fetch applications - const applications = await this.getApplicationsByIds(applicationIds); - - // Populate all IoT devices. Any which fail will be added to the response as failed devices - for (const map of iotDeviceMaps) { - const { iotDevice, iotDeviceDto } = map; - try { - const application = applications.find(app => app.id === iotDeviceDto.applicationId); - iotDevice.name = iotDeviceDto.name; - iotDevice.application = application; - - if (iotDeviceDto.longitude != null && iotDeviceDto.latitude != null) { - iotDevice.location = { - type: "Point", - coordinates: [iotDeviceDto.longitude, iotDeviceDto.latitude], - }; - } else { - iotDevice.location = null; - } - - iotDevice.comment = iotDeviceDto.comment; - iotDevice.commentOnLocation = iotDeviceDto.commentOnLocation; - iotDevice.metadata = iotDeviceDto.metadata; - } catch (error) { - map.error = error; + } + + private averageStatsForSameDay(stats: DeviceStatsResponseDto[]) { + const statsSummed = stats.reduce( + (res: Record, item) => { + // Assume that the date is ISO formatted and extract only the date. + const dateWithoutTime = item.timestamp.split("T")[0]; + res[dateWithoutTime] = res.hasOwnProperty(dateWithoutTime) + ? { + count: res[dateWithoutTime].count + 1, + timestamp: item.timestamp, + rssi: res[dateWithoutTime].rssi + item.rssi, + snr: res[dateWithoutTime].snr + item.snr, } + : { + count: 1, + timestamp: item.timestamp, + rssi: item.rssi, + snr: item.snr, + }; + return res; + }, + {} + ); + + const averagedStats: DeviceStatsResponseDto[] = Object.entries(statsSummed).map(([_key, item]) => ({ + timestamp: item.timestamp, + rssi: item.rssi / item.count, + snr: item.snr / item.count, + })); + + return averagedStats; + } + + /** + * Validate and map info. from the dto onto an IoT device. This device is then created or updated + * as one of the final steps. I.e. valid chirpstack devices will be created in Chirpstack + * @param iotDeviceMaps + * @param isUpdate + */ + private async validateDtoAndCreateIoTDevice( + iotDeviceMaps: CreateIoTDeviceMapDto[], + isUpdate: boolean + ): Promise { + const applicationIds = iotDeviceMaps.reduce((res: number[], { iotDeviceDto }) => { + if (iotDeviceDto.applicationId) { + res.push(iotDeviceDto.applicationId); + } + + return res; + }, []); + // Pre-fetch applications + const applications = await this.getApplicationsByIds(applicationIds); + + // Populate all IoT devices. Any which fail will be added to the response as failed devices + for (const map of iotDeviceMaps) { + const { iotDevice, iotDeviceDto } = map; + try { + const application = applications.find(app => app.id === iotDeviceDto.applicationId); + iotDevice.name = iotDeviceDto.name; + iotDevice.application = application; + + if (iotDeviceDto.longitude != null && iotDeviceDto.latitude != null) { + iotDevice.location = { + type: "Point", + coordinates: [iotDeviceDto.longitude, iotDeviceDto.latitude], + }; + } else { + iotDevice.location = null; } - // Set and validate properties on each IoT device - // Filter devices whose properties couldn't be set - await this.mapDeviceModels(filterValidIotDeviceMaps(iotDeviceMaps)); - // Filter devices which didn't have a valid device model - await this.mapChildDtoToIoTDevice(filterValidIotDeviceMaps(iotDeviceMaps), isUpdate); - } - - async mapDeviceModels(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { - // Pre-fetch device models - const deviceModelIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { - if (dto.iotDeviceDto.deviceModelId) { - ids.push(dto.iotDeviceDto.deviceModelId); - } - - return ids; - }, []); - - const deviceModels = await this.deviceModelService.getByIdsWithRelations(deviceModelIds); - - const applicationIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { - if (dto.iotDeviceDto.applicationId) { - ids.push(dto.iotDeviceDto.applicationId); - } - return ids; - }, []); - - const applications = await this.applicationService.findManyWithOrganisation(applicationIds); - - // Ensure that each device model is assignable - this.setDeviceModel(iotDevicesDtoMap, applications, deviceModels); + iotDevice.comment = iotDeviceDto.comment; + iotDevice.commentOnLocation = iotDeviceDto.commentOnLocation; + iotDevice.metadata = iotDeviceDto.metadata; + } catch (error) { + map.error = error; + } } - private setDeviceModel( - iotDevicesDtoMap: CreateIoTDeviceMapDto[], - applications: Application[], - deviceModels: DeviceModel[] - ) { - for (const map of iotDevicesDtoMap) { - const applicationMatch = applications.find(application => application.id === map.iotDevice.application.id); - - if (!applicationMatch) { - map.error = { - message: ErrorCodes.ApplicationDoesNotExist, - }; - continue; - } - - // Validate DeviceModel if set - if (map.iotDeviceDto.deviceModelId) { - const deviceModelMatch = deviceModels.find(model => model.id === map.iotDeviceDto.deviceModelId); - - if (!deviceModelMatch) { - map.error = { message: ErrorCodes.DeviceModelDoesNotExist }; - continue; - } + // Set and validate properties on each IoT device + // Filter devices whose properties couldn't be set + await this.mapDeviceModels(filterValidIotDeviceMaps(iotDeviceMaps)); + // Filter devices which didn't have a valid device model + await this.mapChildDtoToIoTDevice(filterValidIotDeviceMaps(iotDeviceMaps), isUpdate); + } + + async mapDeviceModels(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { + // Pre-fetch device models + const deviceModelIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { + if (dto.iotDeviceDto.deviceModelId) { + ids.push(dto.iotDeviceDto.deviceModelId); + } + + return ids; + }, []); + + const deviceModels = await this.deviceModelService.getByIdsWithRelations(deviceModelIds); + + const applicationIds = iotDevicesDtoMap.reduce((ids: number[], dto) => { + if (dto.iotDeviceDto.applicationId) { + ids.push(dto.iotDeviceDto.applicationId); + } + return ids; + }, []); + + const applications = await this.applicationService.findManyWithOrganisation(applicationIds); + + // Ensure that each device model is assignable + this.setDeviceModel(iotDevicesDtoMap, applications, deviceModels); + } + + private setDeviceModel( + iotDevicesDtoMap: CreateIoTDeviceMapDto[], + applications: Application[], + deviceModels: DeviceModel[] + ) { + for (const map of iotDevicesDtoMap) { + const applicationMatch = applications.find(application => application.id === map.iotDevice.application.id); + + if (!applicationMatch) { + map.error = { + message: ErrorCodes.ApplicationDoesNotExist, + }; + continue; + } - if (deviceModelMatch.belongsTo.id !== applicationMatch.belongsTo.id) { - map.error = { - message: ErrorCodes.DeviceModelOrganizationDoesNotMatch, - }; - continue; - } + // Validate DeviceModel if set + if (map.iotDeviceDto.deviceModelId) { + const deviceModelMatch = deviceModels.find(model => model.id === map.iotDeviceDto.deviceModelId); - map.iotDevice.deviceModel = deviceModelMatch; - } + if (!deviceModelMatch) { + map.error = { message: ErrorCodes.DeviceModelDoesNotExist }; + continue; } - } - resetHttpDeviceApiKey(httpDevice: GenericHTTPDevice): Promise { - httpDevice.apiKey = uuidv4(); - return this.iotDeviceRepository.save(httpDevice); - } - - private async getApplicationsByIds(applicationIds: number[]) { - return applicationIds.length ? await this.applicationService.findManyByIds(applicationIds) : []; - } - - public async mapChildDtoToIoTDevice(iotDevicesDtoMap: CreateIoTDeviceMapDto[], isUpdate: boolean): Promise { - // Pre-fetch lorawan settings, if any - const loraDeviceEuis = await this.getLorawanDeviceEuis(iotDevicesDtoMap); - const loraOrganizationId = await this.chirpstackDeviceService.getDefaultOrganizationId(); - const loraApplications = await this.chirpstackDeviceService.getAllApplicationsWithPagination( - loraOrganizationId - ); - - // Populate each IoT device with the specific device type metadata - for (const map of iotDevicesDtoMap) { - try { - if (map.iotDevice.constructor.name === LoRaWANDevice.name) { - const cast = map.iotDevice as LoRaWANDevice; - map.iotDevice = await this.mapAndCreateLoRaWANDevice( - map.iotDeviceDto, - cast, - isUpdate, - loraDeviceEuis, - loraApplications - ); - } else if (map.iotDevice.constructor.name === SigFoxDevice.name) { - const cast = map.iotDevice as SigFoxDevice; - map.iotDevice = await this.mapSigFoxDevice(map.iotDeviceDto, cast); - } else if (map.iotDevice.constructor.name === MQTTInternalBrokerDevice.name) { - const cast = map.iotDevice as MQTTInternalBrokerDevice; - map.iotDevice = await this.mapMQTTInternalBrokerDevice(map.iotDeviceDto, cast); - } else if (map.iotDevice.constructor.name === MQTTExternalBrokerDevice.name) { - const cast = map.iotDevice as MQTTExternalBrokerDevice; - map.iotDevice = await this.mapMQTTExternalBrokerDevice(map.iotDeviceDto, cast, isUpdate); - } - } catch (error) { - map.error = { - message: (error as Error)?.message ?? ErrorCodes.FailedToCreateOrUpdateIotDevice, - }; - } + if (deviceModelMatch.belongsTo.id !== applicationMatch.belongsTo.id) { + map.error = { + message: ErrorCodes.DeviceModelOrganizationDoesNotMatch, + }; + continue; } - } - private async getLorawanDeviceEuis(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { - const iotLorawanDevices = iotDevicesDtoMap.reduce((res: string[], { iotDevice, iotDeviceDto }) => { - if (iotDevice.constructor.name === LoRaWANDevice.name && iotDeviceDto.lorawanSettings) { - res.push(iotDeviceDto.lorawanSettings.devEUI); - } - - return res; - }, []); - - const loraDeviceEuis = !iotLorawanDevices.length - ? [] - : // Fetch from the database instead of from Chirpstack to free up load - await this.ioTLoRaWANDeviceService.getDeviceEUIsByIds(iotLorawanDevices); - - return loraDeviceEuis.map(loraDevice => ({ devEUI: loraDevice.deviceEUI })); + map.iotDevice.deviceModel = deviceModelMatch; + } } - - private async mapSigFoxDevice(dto: CreateIoTDeviceDto, cast: SigFoxDevice): Promise { - cast.deviceId = dto?.sigfoxSettings?.deviceId; - cast.deviceTypeId = dto?.sigfoxSettings?.deviceTypeId; - - const sigfoxGroup = await this.sigfoxGroupService.findOneWithPassword(dto.sigfoxSettings.groupId); - cast.groupId = sigfoxGroup.sigfoxGroupId; - await this.createOrUpdateSigFoxDevice(dto, sigfoxGroup, cast); - - await this.sigfoxApiDeviceTypeService.addOrUpdateCallback(sigfoxGroup, cast.deviceTypeId); - - return cast; - } - - private async createOrUpdateSigFoxDevice(dto: CreateIoTDeviceDto, sigfoxGroup: SigFoxGroup, cast: SigFoxDevice) { - if (dto?.sigfoxSettings?.connectToExistingDeviceInBackend == false) { - // Create device in sigfox backend - const res = await this.createInSigfoxBackend(dto, sigfoxGroup); - cast.deviceId = res.id; - } else { - // Ensure that the device exists - try { - const res = await this.sigfoxApiDeviceService.getByIdSimple(sigfoxGroup, cast.deviceId); - cast.deviceId = res.id; - cast.deviceTypeId = res.deviceType.id; - await this.doEditInSigFoxBackend(res, dto, sigfoxGroup, cast); - } catch (err) { - if (err?.status == 429) { - throw err; - } - throw new BadRequestException(ErrorCodes.DeviceDoesNotExistInSigFoxForGroup); - } + } + + resetHttpDeviceApiKey(httpDevice: GenericHTTPDevice): Promise { + httpDevice.apiKey = uuidv4(); + return this.iotDeviceRepository.save(httpDevice); + } + + private async getApplicationsByIds(applicationIds: number[]) { + return applicationIds.length ? await this.applicationService.findManyByIds(applicationIds) : []; + } + + public async mapChildDtoToIoTDevice(iotDevicesDtoMap: CreateIoTDeviceMapDto[], isUpdate: boolean): Promise { + // Pre-fetch lorawan settings, if any + const loraDeviceEuis = await this.getLorawanDeviceEuis(iotDevicesDtoMap); + const loraOrganizationId = await this.chirpstackDeviceService.getDefaultOrganizationId(); + const loraApplications = await this.chirpstackDeviceService.getAllApplicationsWithPagination(loraOrganizationId); + + // Populate each IoT device with the specific device type metadata + for (const map of iotDevicesDtoMap) { + try { + if (map.iotDevice.constructor.name === LoRaWANDevice.name) { + const cast = map.iotDevice as LoRaWANDevice; + map.iotDevice = await this.mapAndCreateLoRaWANDevice( + map.iotDeviceDto, + cast, + isUpdate, + loraDeviceEuis, + loraApplications + ); + } else if (map.iotDevice.constructor.name === SigFoxDevice.name) { + const cast = map.iotDevice as SigFoxDevice; + map.iotDevice = await this.mapSigFoxDevice(map.iotDeviceDto, cast); + } else if (map.iotDevice.constructor.name === MQTTInternalBrokerDevice.name) { + const cast = map.iotDevice as MQTTInternalBrokerDevice; + map.iotDevice = await this.mapMQTTInternalBrokerDevice(map.iotDeviceDto, cast); + } else if (map.iotDevice.constructor.name === MQTTExternalBrokerDevice.name) { + const cast = map.iotDevice as MQTTExternalBrokerDevice; + map.iotDevice = await this.mapMQTTExternalBrokerDevice(map.iotDeviceDto, cast, isUpdate); } + } catch (error) { + map.error = { + message: (error as Error)?.message ?? ErrorCodes.FailedToCreateOrUpdateIotDevice, + }; + } } - - async getAllSigfoxDevicesByGroup(group: SigFoxGroup, removeExisting: boolean): Promise { - const devices = await this.sigfoxApiDeviceService.getAllByGroupIds(group, [group.sigfoxGroupId]); - - if (removeExisting) { - const sigfoxDeviceIdsInUse = await this.sigfoxRepository.find({ - select: ["deviceId"], - }); - const filtered = devices.data.filter(x => { - return !sigfoxDeviceIdsInUse.some(y => y.deviceId == x.id); - }); - return { - data: filtered, - }; + } + + private async getLorawanDeviceEuis(iotDevicesDtoMap: CreateIoTDeviceMapDto[]): Promise { + const iotLorawanDevices = iotDevicesDtoMap.reduce((res: string[], { iotDevice, iotDeviceDto }) => { + if (iotDevice.constructor.name === LoRaWANDevice.name && iotDeviceDto.lorawanSettings) { + res.push(iotDeviceDto.lorawanSettings.devEUI); + } + + return res; + }, []); + + const loraDeviceEuis = !iotLorawanDevices.length + ? [] + : // Fetch from the database instead of from Chirpstack to free up load + await this.ioTLoRaWANDeviceService.getDeviceEUIsByIds(iotLorawanDevices); + + return loraDeviceEuis.map(loraDevice => ({ devEUI: loraDevice.deviceEUI })); + } + + private async mapSigFoxDevice(dto: CreateIoTDeviceDto, cast: SigFoxDevice): Promise { + cast.deviceId = dto?.sigfoxSettings?.deviceId; + cast.deviceTypeId = dto?.sigfoxSettings?.deviceTypeId; + + const sigfoxGroup = await this.sigfoxGroupService.findOneWithPassword(dto.sigfoxSettings.groupId); + cast.groupId = sigfoxGroup.sigfoxGroupId; + await this.createOrUpdateSigFoxDevice(dto, sigfoxGroup, cast); + + await this.sigfoxApiDeviceTypeService.addOrUpdateCallback(sigfoxGroup, cast.deviceTypeId); + + return cast; + } + + private async createOrUpdateSigFoxDevice(dto: CreateIoTDeviceDto, sigfoxGroup: SigFoxGroup, cast: SigFoxDevice) { + if (dto?.sigfoxSettings?.connectToExistingDeviceInBackend == false) { + // Create device in sigfox backend + const res = await this.createInSigfoxBackend(dto, sigfoxGroup); + cast.deviceId = res.id; + } else { + // Ensure that the device exists + try { + const res = await this.sigfoxApiDeviceService.getByIdSimple(sigfoxGroup, cast.deviceId); + cast.deviceId = res.id; + cast.deviceTypeId = res.deviceType.id; + await this.doEditInSigFoxBackend(res, dto, sigfoxGroup, cast); + } catch (err) { + if (err?.status == 429) { + throw err; } - - return devices; + throw new BadRequestException(ErrorCodes.DeviceDoesNotExistInSigFoxForGroup); + } } - - async getDevicesMetadataCsv(applicationId: number) { - const iotDevices = await this.iotDeviceRepository - .createQueryBuilder("device") - .leftJoinAndSelect("device.deviceModel", "deviceModel") - .where("device.applicationId = :applicationId", { applicationId }) - .getMany(); - - for (const d of iotDevices) { - if (d.type !== IoTDeviceType.LoRaWAN) { - continue; - } - await this.chirpstackDeviceService.enrichLoRaWANDevice(d); - } - - const csvString = this.csvGeneratorService.generateDeviceMetadataCsv(iotDevices); - return Buffer.from(csvString); + } + + async getAllSigfoxDevicesByGroup(group: SigFoxGroup, removeExisting: boolean): Promise { + const devices = await this.sigfoxApiDeviceService.getAllByGroupIds(group, [group.sigfoxGroupId]); + + if (removeExisting) { + const sigfoxDeviceIdsInUse = await this.sigfoxRepository.find({ + select: ["deviceId"], + }); + const filtered = devices.data.filter(x => { + return !sigfoxDeviceIdsInUse.some(y => y.deviceId == x.id); + }); + return { + data: filtered, + }; } - private async doEditInSigFoxBackend( - currentSigFoxSettings: SigFoxApiDeviceContent, - dto: CreateIoTDeviceDto, - sigfoxGroup: SigFoxGroup, - sigfoxDevice: SigFoxDevice - ) { - await Promise.all([ - this.updateSigFoxDevice(currentSigFoxSettings, dto, sigfoxGroup, sigfoxDevice), - this.changeDeviceTypeIfNeeded(currentSigFoxSettings, dto, sigfoxGroup, sigfoxDevice), - ]); + return devices; + } + + async getDevicesMetadataCsv(applicationId: number) { + const iotDevices = await this.iotDeviceRepository + .createQueryBuilder("device") + .leftJoinAndSelect("device.deviceModel", "deviceModel") + .where("device.applicationId = :applicationId", { applicationId }) + .getMany(); + + for (const d of iotDevices) { + if (d.type !== IoTDeviceType.LoRaWAN) { + continue; + } + await this.chirpstackDeviceService.enrichLoRaWANDevice(d); } - private async updateSigFoxDevice( - currentSigFoxSettings: SigFoxApiDeviceContent, - dto: CreateIoTDeviceDto, - sigfoxGroup: SigFoxGroup, - sigfoxDevice: SigFoxDevice + const csvString = this.csvGeneratorService.generateDeviceMetadataCsv(iotDevices); + return Buffer.from(csvString); + } + + private async doEditInSigFoxBackend( + currentSigFoxSettings: SigFoxApiDeviceContent, + dto: CreateIoTDeviceDto, + sigfoxGroup: SigFoxGroup, + sigfoxDevice: SigFoxDevice + ) { + await Promise.all([ + this.updateSigFoxDevice(currentSigFoxSettings, dto, sigfoxGroup, sigfoxDevice), + this.changeDeviceTypeIfNeeded(currentSigFoxSettings, dto, sigfoxGroup, sigfoxDevice), + ]); + } + + private async updateSigFoxDevice( + currentSigFoxSettings: SigFoxApiDeviceContent, + dto: CreateIoTDeviceDto, + sigfoxGroup: SigFoxGroup, + sigfoxDevice: SigFoxDevice + ) { + const updateDto: UpdateSigFoxApiDeviceRequestDto = { + activable: true, + automaticRenewal: currentSigFoxSettings.automaticRenewal, + lat: dto.latitude, + lng: dto.longitude, + name: dto.name, + }; + + await this.sigfoxApiDeviceService.update(sigfoxGroup, sigfoxDevice.deviceId, updateDto); + } + + private async changeDeviceTypeIfNeeded( + currentSigFoxSettings: SigFoxApiDeviceContent, + dto: CreateIoTDeviceDto, + sigfoxGroup: SigFoxGroup, + sigfoxDevice: SigFoxDevice + ) { + if ( + dto.sigfoxSettings.deviceTypeId != null && + currentSigFoxSettings.deviceType.id != dto.sigfoxSettings.deviceTypeId ) { - const updateDto: UpdateSigFoxApiDeviceRequestDto = { - activable: true, - automaticRenewal: currentSigFoxSettings.automaticRenewal, - lat: dto.latitude, - lng: dto.longitude, - name: dto.name, - }; - - await this.sigfoxApiDeviceService.update(sigfoxGroup, sigfoxDevice.deviceId, updateDto); + this.logger.log( + `Changing deviceType from ${currentSigFoxSettings.deviceType.id} to ${dto.sigfoxSettings.deviceTypeId}` + ); + await this.sigfoxApiDeviceService.changeDeviceType( + sigfoxGroup, + sigfoxDevice.deviceId, + dto.sigfoxSettings.deviceTypeId + ); } + } - private async changeDeviceTypeIfNeeded( - currentSigFoxSettings: SigFoxApiDeviceContent, - dto: CreateIoTDeviceDto, - sigfoxGroup: SigFoxGroup, - sigfoxDevice: SigFoxDevice - ) { - if ( - dto.sigfoxSettings.deviceTypeId != null && - currentSigFoxSettings.deviceType.id != dto.sigfoxSettings.deviceTypeId - ) { - this.logger.log( - `Changing deviceType from ${currentSigFoxSettings.deviceType.id} to ${dto.sigfoxSettings.deviceTypeId}` - ); - await this.sigfoxApiDeviceService.changeDeviceType( - sigfoxGroup, - sigfoxDevice.deviceId, - dto.sigfoxSettings.deviceTypeId - ); - } - } - - private async createInSigfoxBackend(dto: CreateIoTDeviceDto, sigfoxGroup: SigFoxGroup) { - const sigfoxDto: CreateSigFoxApiDeviceRequestDto = this.mapToSigFoxDto(dto); + private async createInSigfoxBackend(dto: CreateIoTDeviceDto, sigfoxGroup: SigFoxGroup) { + const sigfoxDto: CreateSigFoxApiDeviceRequestDto = this.mapToSigFoxDto(dto); - try { - return await this.sigfoxApiDeviceService.create(sigfoxGroup, sigfoxDto); - } catch (err) { - this.logger.error(`Error creating sigfox device`); - throw err; - } + try { + return await this.sigfoxApiDeviceService.create(sigfoxGroup, sigfoxDto); + } catch (err) { + this.logger.error(`Error creating sigfox device`); + throw err; } - - private mapToSigFoxDto(dto: CreateIoTDeviceDto) { - const sigfoxDto: CreateSigFoxApiDeviceRequestDto = { - id: dto.sigfoxSettings.deviceId, - name: dto.name, - pac: dto.sigfoxSettings.pac, - deviceTypeId: dto.sigfoxSettings.deviceTypeId, - activable: true, - automaticRenewal: true, - lat: dto.latitude, - lng: dto.longitude, - prototype: dto.sigfoxSettings.prototype, - }; - - if (!sigfoxDto.prototype) { - sigfoxDto.productCertificate = { - key: dto.sigfoxSettings.endProductCertificate, - }; - } - return sigfoxDto; + } + + private mapToSigFoxDto(dto: CreateIoTDeviceDto) { + const sigfoxDto: CreateSigFoxApiDeviceRequestDto = { + id: dto.sigfoxSettings.deviceId, + name: dto.name, + pac: dto.sigfoxSettings.pac, + deviceTypeId: dto.sigfoxSettings.deviceTypeId, + activable: true, + automaticRenewal: true, + lat: dto.latitude, + lng: dto.longitude, + prototype: dto.sigfoxSettings.prototype, + }; + + if (!sigfoxDto.prototype) { + sigfoxDto.productCertificate = { + key: dto.sigfoxSettings.endProductCertificate, + }; } - - private async mapAndCreateLoRaWANDevice( - dto: CreateIoTDeviceDto, - lorawanDevice: LoRaWANDevice, - isUpdate: boolean, - lorawanDeviceEuis: ChirpstackDeviceId[] = null, - loraApplications: ListAllChirpstackApplicationsResponseDto = null - ): Promise { - lorawanDevice.deviceEUI = dto.lorawanSettings.devEUI; - if ( - !isUpdate && - (await this.chirpstackDeviceService.isDeviceAlreadyCreated(dto.lorawanSettings.devEUI, lorawanDeviceEuis)) - ) { - throw new BadRequestException(ErrorCodes.IdInvalidOrAlreadyInUse); - } - try { - const chirpstackDeviceDto = this.chirpstackDeviceService.makeCreateChirpstackDeviceDto( - dto.lorawanSettings, - dto.name - ); - - const applicationId = await this.applicationChirpstackService.findOrCreateDefaultApplication( - loraApplications, - lorawanDevice - ); - lorawanDevice.chirpstackApplicationId = applicationId; - chirpstackDeviceDto.device.applicationID = applicationId; - // Create or update the LoRa device against Chirpstack API - const success = await this.chirpstackDeviceService.createOrUpdateDevice( - chirpstackDeviceDto, - lorawanDeviceEuis - ); - if (success) { - lorawanDeviceEuis.push(chirpstackDeviceDto.device); - await this.doActivation(dto, isUpdate); - lorawanDevice.OTAAapplicationKey = dto.lorawanSettings.OTAAapplicationKey; - const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById( - dto.lorawanSettings.deviceProfileID - ); - lorawanDevice.deviceProfileName = deviceProfile.deviceProfile.name; - } else { - throw new BadRequestException(ErrorCodes.InvalidPost); - } - } catch (err) { - this.logger.error(err); - - // This will also be thrown if a chirpstack device with the same name already exists - if (err?.response?.data?.error == "object already exists") { - throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); - } - throw err; - } - return lorawanDevice; + return sigfoxDto; + } + + private async mapAndCreateLoRaWANDevice( + dto: CreateIoTDeviceDto, + lorawanDevice: LoRaWANDevice, + isUpdate: boolean, + lorawanDeviceEuis: ChirpstackDeviceId[] = null, + loraApplications: ListAllChirpstackApplicationsResponseDto = null + ): Promise { + lorawanDevice.deviceEUI = dto.lorawanSettings.devEUI; + if ( + !isUpdate && + (await this.chirpstackDeviceService.isDeviceAlreadyCreated(dto.lorawanSettings.devEUI, lorawanDeviceEuis)) + ) { + throw new BadRequestException(ErrorCodes.IdInvalidOrAlreadyInUse); } - - private async doActivation(dto: CreateIoTDeviceDto, isUpdate: boolean): Promise { - if (dto.lorawanSettings.activationType == ActivationType.OTAA) { - // OTAA Activate if key is provided - await this.doActivationByOTAA(dto, isUpdate); - } else if (dto.lorawanSettings.activationType === ActivationType.ABP) { - await this.doActivationByABP(dto); - } + try { + const chirpstackDeviceDto = this.chirpstackDeviceService.makeCreateChirpstackDeviceDto( + dto.lorawanSettings, + dto.name + ); + + const applicationId = await this.applicationChirpstackService.findOrCreateDefaultApplication( + loraApplications, + lorawanDevice + ); + lorawanDevice.chirpstackApplicationId = applicationId; + chirpstackDeviceDto.device.applicationID = applicationId; + // Create or update the LoRa device against Chirpstack API + const success = await this.chirpstackDeviceService.createOrUpdateDevice(chirpstackDeviceDto, lorawanDeviceEuis); + if (success) { + lorawanDeviceEuis.push(chirpstackDeviceDto.device); + await this.doActivation(dto, isUpdate); + lorawanDevice.OTAAapplicationKey = dto.lorawanSettings.OTAAapplicationKey; + const deviceProfile = await this.deviceProfileService.findOneDeviceProfileById( + dto.lorawanSettings.deviceProfileID + ); + lorawanDevice.deviceProfileName = deviceProfile.deviceProfile.name; + } else { + throw new BadRequestException(ErrorCodes.InvalidPost); + } + } catch (err) { + this.logger.error(err); + + // This will also be thrown if a chirpstack device with the same name already exists + if (err?.response?.data?.error == "object already exists") { + throw new BadRequestException(ErrorCodes.NameInvalidOrAlreadyInUse); + } + throw err; } - - private async doActivationByOTAA(dto: CreateIoTDeviceDto, isUpdate: boolean) { - if (dto.lorawanSettings.OTAAapplicationKey) { - await this.chirpstackDeviceService.activateDeviceWithOTAA( - dto.lorawanSettings.devEUI, - dto.lorawanSettings.OTAAapplicationKey, - isUpdate - ); - } else { - throw new BadRequestException(ErrorCodes.MissingOTAAInfo); - } + return lorawanDevice; + } + + private async doActivation(dto: CreateIoTDeviceDto, isUpdate: boolean): Promise { + if (dto.lorawanSettings.activationType == ActivationType.OTAA) { + // OTAA Activate if key is provided + await this.doActivationByOTAA(dto, isUpdate); + } else if (dto.lorawanSettings.activationType === ActivationType.ABP) { + await this.doActivationByABP(dto); } - - private async doActivationByABP(dto: CreateIoTDeviceDto) { - if ( - dto.lorawanSettings.devAddr && - dto.lorawanSettings.fCntUp != null && - dto.lorawanSettings.nFCntDown != null && - dto.lorawanSettings.networkSessionKey && - dto.lorawanSettings.applicationSessionKey - ) { - await this.chirpstackDeviceService.activateDeviceWithABP( - dto.lorawanSettings.devEUI, - dto.lorawanSettings.devAddr, - dto.lorawanSettings.fCntUp, - dto.lorawanSettings.nFCntDown, - dto.lorawanSettings.networkSessionKey, - dto.lorawanSettings.applicationSessionKey - ); - } else { - throw new BadRequestException(ErrorCodes.MissingABPInfo); - } + } + + private async doActivationByOTAA(dto: CreateIoTDeviceDto, isUpdate: boolean) { + if (dto.lorawanSettings.OTAAapplicationKey) { + await this.chirpstackDeviceService.activateDeviceWithOTAA( + dto.lorawanSettings.devEUI, + dto.lorawanSettings.OTAAapplicationKey, + isUpdate + ); + } else { + throw new BadRequestException(ErrorCodes.MissingOTAAInfo); } - - private async mapMQTTInternalBrokerDevice( - iotDeviceDto: CreateIoTDeviceDto, - cast: MQTTInternalBrokerDevice - ): Promise { - const settings = iotDeviceDto.mqttInternalBrokerSettings; - validateMQTTInternalBroker(settings); - cast.authenticationType = settings.authenticationType; - switch (cast.authenticationType) { - case AuthenticationType.PASSWORD: - cast.mqttpasswordhash = this.mqttService.hashPassword(settings.mqttpassword); - cast.mqttpassword = this.encryptionHelperService.basicEncrypt(settings.mqttpassword); - cast.mqttusername = settings.mqttusername; - break; - case AuthenticationType.CERTIFICATE: - if (!cast.deviceCertificate) { - const certificateDetails = await this.mqttService.generateCertificate(cast.name); - cast.deviceCertificate = certificateDetails.deviceCertificate; - cast.deviceCertificateKey = this.encryptionHelperService.basicEncrypt( - certificateDetails.deviceCertificateKey - ); - cast.caCertificate = certificateDetails.ca; - cast.mqttusername = cast.name; - cast.mqttpassword = undefined; - cast.mqttpasswordhash = undefined; - } - break; - } - - const topicDetails = await this.mqttService.createTopic(cast); - cast.mqttURL = topicDetails.uRL; - cast.mqttPort = topicDetails.port; - cast.mqtttopicname = topicDetails.topicName; - cast.permissions = MQTTPermissionLevel.write; // Hardcoded 'write' permission for now, as communication is oneway - - return cast; + } + + private async doActivationByABP(dto: CreateIoTDeviceDto) { + if ( + dto.lorawanSettings.devAddr && + dto.lorawanSettings.fCntUp != null && + dto.lorawanSettings.nFCntDown != null && + dto.lorawanSettings.networkSessionKey && + dto.lorawanSettings.applicationSessionKey + ) { + await this.chirpstackDeviceService.activateDeviceWithABP( + dto.lorawanSettings.devEUI, + dto.lorawanSettings.devAddr, + dto.lorawanSettings.fCntUp, + dto.lorawanSettings.nFCntDown, + dto.lorawanSettings.networkSessionKey, + dto.lorawanSettings.applicationSessionKey + ); + } else { + throw new BadRequestException(ErrorCodes.MissingABPInfo); } - - private async mapMQTTExternalBrokerDevice( - iotDeviceDto: CreateIoTDeviceDto, - cast: MQTTExternalBrokerDevice, - isUpdate = false - ): Promise { - const settings = iotDeviceDto.mqttExternalBrokerSettings; - validateMQTTExternalBroker(settings); - cast.authenticationType = settings.authenticationType; - switch (cast.authenticationType) { - case AuthenticationType.PASSWORD: - cast.mqttpassword = this.encryptionHelperService.basicEncrypt(settings.mqttpassword); - cast.mqttusername = settings.mqttusername; - break; - case AuthenticationType.CERTIFICATE: - cast.caCertificate = settings.caCertificate; - cast.deviceCertificate = settings.deviceCertificate; - cast.deviceCertificateKey = this.encryptionHelperService.basicEncrypt(settings.deviceCertificateKey); - break; + } + + private async mapMQTTInternalBrokerDevice( + iotDeviceDto: CreateIoTDeviceDto, + cast: MQTTInternalBrokerDevice + ): Promise { + const settings = iotDeviceDto.mqttInternalBrokerSettings; + validateMQTTInternalBroker(settings); + cast.authenticationType = settings.authenticationType; + switch (cast.authenticationType) { + case AuthenticationType.PASSWORD: + cast.mqttpasswordhash = this.mqttService.hashPassword(settings.mqttpassword); + cast.mqttpassword = this.encryptionHelperService.basicEncrypt(settings.mqttpassword); + cast.mqttusername = settings.mqttusername; + break; + case AuthenticationType.CERTIFICATE: + if (!cast.deviceCertificate) { + const certificateDetails = await this.mqttService.generateCertificate(cast.name); + cast.deviceCertificate = certificateDetails.deviceCertificate; + cast.deviceCertificateKey = this.encryptionHelperService.basicEncrypt( + certificateDetails.deviceCertificateKey + ); + cast.caCertificate = certificateDetails.ca; + cast.mqttusername = cast.name; + cast.mqttpassword = undefined; + cast.mqttpasswordhash = undefined; } - - cast.mqttURL = settings.mqttURL; - cast.mqttPort = settings.mqttPort; - cast.mqtttopicname = settings.mqtttopicname; - - if (isUpdate) { - await this.internalMqttClientListenerService.removeMQTTClient(cast); - await this.createNewMQTTClients([cast]); - cast.invalidMqttConfig = false; - } - - return cast; + break; } - private async enrichMQTTInternalBrokerDevice(iotDevice: IoTDevice) { - const device = iotDevice as MQTTInternalBrokerDeviceDTO; - device.mqttInternalBrokerSettings = { - authenticationType: device.authenticationType, - caCertificate: device.caCertificate ?? fs.readFileSync(caCertPath).toString(), - deviceCertificate: device.deviceCertificate, - deviceCertificateKey: this.encryptionHelperService.basicDecrypt(device.deviceCertificateKey), - mqtttopicname: device.mqtttopicname, - mqttURL: device.mqttURL, - mqttPort: device.mqttPort, - mqttusername: device.mqttusername, - mqttpassword: this.encryptionHelperService.basicDecrypt(device.mqttpassword), - permissions: device.permissions, - }; - return device; + const topicDetails = await this.mqttService.createTopic(cast); + cast.mqttURL = topicDetails.uRL; + cast.mqttPort = topicDetails.port; + cast.mqtttopicname = topicDetails.topicName; + cast.permissions = MQTTPermissionLevel.write; // Hardcoded 'write' permission for now, as communication is oneway + + return cast; + } + + private async mapMQTTExternalBrokerDevice( + iotDeviceDto: CreateIoTDeviceDto, + cast: MQTTExternalBrokerDevice, + isUpdate = false + ): Promise { + const settings = iotDeviceDto.mqttExternalBrokerSettings; + validateMQTTExternalBroker(settings); + cast.authenticationType = settings.authenticationType; + switch (cast.authenticationType) { + case AuthenticationType.PASSWORD: + cast.mqttpassword = this.encryptionHelperService.basicEncrypt(settings.mqttpassword); + cast.mqttusername = settings.mqttusername; + break; + case AuthenticationType.CERTIFICATE: + cast.caCertificate = settings.caCertificate; + cast.deviceCertificate = settings.deviceCertificate; + cast.deviceCertificateKey = this.encryptionHelperService.basicEncrypt(settings.deviceCertificateKey); + break; } - private async enrichMQTTExternalBrokerDevice(iotDevice: IoTDevice) { - const device = iotDevice as MQTTExternalBrokerDeviceDTO; - device.mqttExternalBrokerSettings = { - authenticationType: device.authenticationType, - caCertificate: device.caCertificate, - deviceCertificate: device.deviceCertificate, - deviceCertificateKey: this.encryptionHelperService.basicDecrypt(device.deviceCertificateKey), - mqtttopicname: device.mqtttopicname, - mqttURL: device.mqttURL, - mqttPort: device.mqttPort, - mqttusername: device.mqttusername, - mqttpassword: this.encryptionHelperService.basicDecrypt(device.mqttpassword), - permissions: MQTTPermissionLevel.read, - invalidMqttConfig: device.invalidMqttConfig, - }; - return device; - } + cast.mqttURL = settings.mqttURL; + cast.mqttPort = settings.mqttPort; + cast.mqtttopicname = settings.mqtttopicname; - private async handleNewMQTTDevices(dbIotDevices: IoTDevice[]) { - await this.fixMQTTInternalBrokerTopics(dbIotDevices); - await this.createNewMQTTClients(dbIotDevices); + if (isUpdate) { + await this.internalMqttClientListenerService.removeMQTTClient(cast); + await this.createNewMQTTClients([cast]); + cast.invalidMqttConfig = false; } - private async fixMQTTInternalBrokerTopics(dbIotDevices: IoTDevice[]) { - const newMQTTInternalBrokers = dbIotDevices.filter( - (d: MQTTInternalBrokerDevice) => - d.type === IoTDeviceType.MQTTInternalBroker && d.mqtttopicname.includes("undefined") - ); - const remappedMQTT = []; - for (const iotDevice of newMQTTInternalBrokers) { - const mqttInternalBrokerDevice = iotDevice as MQTTInternalBrokerDevice; - mqttInternalBrokerDevice.mqtttopicname = ( - await this.mqttService.createTopic(mqttInternalBrokerDevice) - ).topicName; - remappedMQTT.push(mqttInternalBrokerDevice); - } - await this.iotDeviceRepository.save(remappedMQTT); + return cast; + } + + private async enrichMQTTInternalBrokerDevice(iotDevice: IoTDevice) { + const device = iotDevice as MQTTInternalBrokerDeviceDTO; + device.mqttInternalBrokerSettings = { + authenticationType: device.authenticationType, + caCertificate: device.caCertificate ?? fs.readFileSync(caCertPath).toString(), + deviceCertificate: device.deviceCertificate, + deviceCertificateKey: this.encryptionHelperService.basicDecrypt(device.deviceCertificateKey), + mqtttopicname: device.mqtttopicname, + mqttURL: device.mqttURL, + mqttPort: device.mqttPort, + mqttusername: device.mqttusername, + mqttpassword: this.encryptionHelperService.basicDecrypt(device.mqttpassword), + permissions: device.permissions, + }; + return device; + } + + private async enrichMQTTExternalBrokerDevice(iotDevice: IoTDevice) { + const device = iotDevice as MQTTExternalBrokerDeviceDTO; + device.mqttExternalBrokerSettings = { + authenticationType: device.authenticationType, + caCertificate: device.caCertificate, + deviceCertificate: device.deviceCertificate, + deviceCertificateKey: this.encryptionHelperService.basicDecrypt(device.deviceCertificateKey), + mqtttopicname: device.mqtttopicname, + mqttURL: device.mqttURL, + mqttPort: device.mqttPort, + mqttusername: device.mqttusername, + mqttpassword: this.encryptionHelperService.basicDecrypt(device.mqttpassword), + permissions: MQTTPermissionLevel.read, + invalidMqttConfig: device.invalidMqttConfig, + }; + return device; + } + + private async handleNewMQTTDevices(dbIotDevices: IoTDevice[]) { + await this.fixMQTTInternalBrokerTopics(dbIotDevices); + await this.createNewMQTTClients(dbIotDevices); + } + + private async fixMQTTInternalBrokerTopics(dbIotDevices: IoTDevice[]) { + const newMQTTInternalBrokers = dbIotDevices.filter( + (d: MQTTInternalBrokerDevice) => + d.type === IoTDeviceType.MQTTInternalBroker && d.mqtttopicname.includes("undefined") + ); + const remappedMQTT = []; + for (const iotDevice of newMQTTInternalBrokers) { + const mqttInternalBrokerDevice = iotDevice as MQTTInternalBrokerDevice; + mqttInternalBrokerDevice.mqtttopicname = (await this.mqttService.createTopic(mqttInternalBrokerDevice)).topicName; + remappedMQTT.push(mqttInternalBrokerDevice); } - - private async createNewMQTTClients(dbIotDevices: IoTDevice[]) { - const newMQTTExternalBrokers = dbIotDevices.filter( - (d: MQTTExternalBrokerDevice) => d.type === IoTDeviceType.MQTTExternalBroker - ); - try { - this.internalMqttClientListenerService.createMQTTClients( - newMQTTExternalBrokers as MQTTExternalBrokerDevice[] - ); - // If something goes wrong delete the devices again. - } catch (e) { - await this.iotDeviceRepository.remove(dbIotDevices); - throw new InternalServerErrorException( - e, - "Something went wrong when creating MQTT external broker client. Device removed" - ); - } + await this.iotDeviceRepository.save(remappedMQTT); + } + + private async createNewMQTTClients(dbIotDevices: IoTDevice[]) { + const newMQTTExternalBrokers = dbIotDevices.filter( + (d: MQTTExternalBrokerDevice) => d.type === IoTDeviceType.MQTTExternalBroker + ); + try { + this.internalMqttClientListenerService.createMQTTClients(newMQTTExternalBrokers as MQTTExternalBrokerDevice[]); + // If something goes wrong delete the devices again. + } catch (e) { + await this.iotDeviceRepository.remove(dbIotDevices); + throw new InternalServerErrorException( + e, + "Something went wrong when creating MQTT external broker client. Device removed" + ); } + } } diff --git a/src/services/device-management/iot-lorawan-device.service.ts b/src/services/device-management/iot-lorawan-device.service.ts index 468e5d9b..b119525a 100644 --- a/src/services/device-management/iot-lorawan-device.service.ts +++ b/src/services/device-management/iot-lorawan-device.service.ts @@ -6,17 +6,17 @@ import { In, Repository } from "typeorm"; @Injectable() export class IoTLoRaWANDeviceService { - constructor( - @InjectRepository(LoRaWANDevice) - private iotLorawanDeviceRepository: Repository - ) {} + constructor( + @InjectRepository(LoRaWANDevice) + private iotLorawanDeviceRepository: Repository + ) {} - private readonly logger = new Logger(IoTLoRaWANDeviceService.name, { timestamp: true }); + private readonly logger = new Logger(IoTLoRaWANDeviceService.name, { timestamp: true }); - public getDeviceEUIsByIds(deviceEUIs: string[]): Promise { - return this.iotLorawanDeviceRepository.find({ - select: ["deviceEUI"], - where: { deviceEUI: In(deviceEUIs) }, - }); - } + public getDeviceEUIsByIds(deviceEUIs: string[]): Promise { + return this.iotLorawanDeviceRepository.find({ + select: ["deviceEUI"], + where: { deviceEUI: In(deviceEUIs) }, + }); + } } diff --git a/src/services/device-management/lorawan-device-database-enrich-job.ts b/src/services/device-management/lorawan-device-database-enrich-job.ts index b3d244c7..c1db53b0 100644 --- a/src/services/device-management/lorawan-device-database-enrich-job.ts +++ b/src/services/device-management/lorawan-device-database-enrich-job.ts @@ -16,127 +16,118 @@ import configuration from "@config/configuration"; @Injectable() export class LorawanDeviceDatabaseEnrichJob { - constructor( - private chirpstackDeviceService: ChirpstackDeviceService, - private gatewayService: ChirpstackGatewayService, - private iotDeviceService: IoTDeviceService, - private organizationService: OrganizationService, - @InjectRepository(Gateway) - private gatewayRepository: Repository - ) {} - baseUrlGRPC = `${configuration()["chirpstack"]["hostname"]}:${configuration()["chirpstack"]["port"]}`; - private gatewayClient = new GatewayServiceClient(this.baseUrlGRPC, credentials.createInsecure()); + constructor( + private chirpstackDeviceService: ChirpstackDeviceService, + private gatewayService: ChirpstackGatewayService, + private iotDeviceService: IoTDeviceService, + private organizationService: OrganizationService, + @InjectRepository(Gateway) + private gatewayRepository: Repository + ) {} + baseUrlGRPC = `${configuration()["chirpstack"]["hostname"]}:${configuration()["chirpstack"]["port"]}`; + private gatewayClient = new GatewayServiceClient(this.baseUrlGRPC, credentials.createInsecure()); - private readonly logger = new Logger(LorawanDeviceDatabaseEnrichJob.name, { timestamp: true }); + private readonly logger = new Logger(LorawanDeviceDatabaseEnrichJob.name, { timestamp: true }); - @Cron(CronExpression.EVERY_MINUTE) - async fetchStatusForGateway() { - // Select all gateways from our database and chirpstack (Cheaper than individual calls) - const gateways = await this.gatewayService.getAll(); - const chirpstackGateways = await this.gatewayService.getAllGatewaysFromChirpstack(); + @Cron(CronExpression.EVERY_MINUTE) + async fetchStatusForGateway() { + // Select all gateways from our database and chirpstack (Cheaper than individual calls) + const gateways = await this.gatewayService.getAll(); + const chirpstackGateways = await this.gatewayService.getAllGatewaysFromChirpstack(); - // Setup batched fetching of status (Only for today) - await BluebirdPromise.all( - BluebirdPromise.map( - gateways.resultList, - async gateway => { - try { - const fromTime = new Date(); - const fromUtc = new Date( - Date.UTC(fromTime.getUTCFullYear(), fromTime.getUTCMonth(), fromTime.getUTCDate()) - ); + // Setup batched fetching of status (Only for today) + await BluebirdPromise.all( + BluebirdPromise.map( + gateways.resultList, + async gateway => { + try { + const fromTime = new Date(); + const fromUtc = new Date( + Date.UTC(fromTime.getUTCFullYear(), fromTime.getUTCMonth(), fromTime.getUTCDate()) + ); - const statsToday = await this.gatewayService.getGatewayStats( - gateway.gatewayId, - fromUtc, - new Date() - ); - // Save that to our database - const stats = statsToday[0]; - const chirpstackGateway = chirpstackGateways.resultList.find( - g => g.gatewayId === gateway.gatewayId - ); + const statsToday = await this.gatewayService.getGatewayStats(gateway.gatewayId, fromUtc, new Date()); + // Save that to our database + const stats = statsToday[0]; + const chirpstackGateway = chirpstackGateways.resultList.find(g => g.gatewayId === gateway.gatewayId); - await this.gatewayService.updateGatewayStats( - gateway.gatewayId, - stats.rxPacketsReceived, - stats.txPacketsEmitted, - gateway.updatedAt, - chirpstackGateway.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined - ); - } catch (err) { - this.logger.error(`Gateway status fetch failed with: ${JSON.stringify(err)}`, err); - } - }, - { - concurrency: 20, - } - ) - ); - } + await this.gatewayService.updateGatewayStats( + gateway.gatewayId, + stats.rxPacketsReceived, + stats.txPacketsEmitted, + gateway.updatedAt, + chirpstackGateway.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined + ); + } catch (err) { + this.logger.error(`Gateway status fetch failed with: ${JSON.stringify(err)}`, err); + } + }, + { + concurrency: 20, + } + ) + ); + } - @Timeout(30000) - async enrichLoRaWANDeviceDatabase() { - // Select lora devices without appId in the database - const devices = await this.iotDeviceService.findNonEnrichedLoRaWANDevices(); + @Timeout(30000) + async enrichLoRaWANDeviceDatabase() { + // Select lora devices without appId in the database + const devices = await this.iotDeviceService.findNonEnrichedLoRaWANDevices(); - // Enrich from chirpstack batched - await BluebirdPromise.all( - BluebirdPromise.map( - devices, - async device => { - try { - const enrichedDevice = await this.chirpstackDeviceService.enrichLoRaWANDevice(device); - await this.iotDeviceService.updateLocalLoRaWANDevices([enrichedDevice]); - } catch (err) { - this.logger.error(`Database sync of lora devices failed with: ${JSON.stringify(err)}`, err); - } - }, - { - concurrency: 10, - } - ) - ); - } + // Enrich from chirpstack batched + await BluebirdPromise.all( + BluebirdPromise.map( + devices, + async device => { + try { + const enrichedDevice = await this.chirpstackDeviceService.enrichLoRaWANDevice(device); + await this.iotDeviceService.updateLocalLoRaWANDevices([enrichedDevice]); + } catch (err) { + this.logger.error(`Database sync of lora devices failed with: ${JSON.stringify(err)}`, err); + } + }, + { + concurrency: 10, + } + ) + ); + } - // This is run once on startup and will create any gateways that exist in chirpstack but not our database - @Timeout(10000) - async importChirpstackGateways() { - const chirpstackGateways = await this.gatewayService.getAllGatewaysFromChirpstack(); - const dbGateways = await this.gatewayService.getAll(); - // Filter for gateways not existing in our database - const unknownGateways = chirpstackGateways.resultList.filter( - g => dbGateways.resultList.findIndex(dbGateway => dbGateway.gatewayId === g.gatewayId) === -1 - ); - await BluebirdPromise.all( - BluebirdPromise.map( - unknownGateways, - async x => { - try { - const req = new GetGatewayRequest(); - req.setGatewayId(x.gatewayId); + // This is run once on startup and will create any gateways that exist in chirpstack but not our database + @Timeout(10000) + async importChirpstackGateways() { + const chirpstackGateways = await this.gatewayService.getAllGatewaysFromChirpstack(); + const dbGateways = await this.gatewayService.getAll(); + // Filter for gateways not existing in our database + const unknownGateways = chirpstackGateways.resultList.filter( + g => dbGateways.resultList.findIndex(dbGateway => dbGateway.gatewayId === g.gatewayId) === -1 + ); + await BluebirdPromise.all( + BluebirdPromise.map( + unknownGateways, + async x => { + try { + const req = new GetGatewayRequest(); + req.setGatewayId(x.gatewayId); - const gwResponse = await this.gatewayService.get( - `gateways/${x.gatewayId}`, - this.gatewayClient, - req - ); - const chirpstackGateway = gwResponse.getGateway(); - const organizationId = +chirpstackGateway.getTagsMap().get("internalOrganizationId"); - const gateway = this.gatewayService.mapChirpstackGatewayToDatabaseGateway( - chirpstackGateway, - gwResponse - ); - gateway.organization = await this.organizationService.findById(organizationId); - await this.gatewayRepository.save(gateway); - } catch (err) { - this.logger.error(`Database sync of gateways failed with: ${JSON.stringify(err)}`, err); - } - }, - { - concurrency: 10, - } - ) - ); - } + const gwResponse = await this.gatewayService.get( + `gateways/${x.gatewayId}`, + this.gatewayClient, + req + ); + const chirpstackGateway = gwResponse.getGateway(); + const organizationId = +chirpstackGateway.getTagsMap().get("internalOrganizationId"); + const gateway = this.gatewayService.mapChirpstackGatewayToDatabaseGateway(chirpstackGateway, gwResponse); + gateway.organization = await this.organizationService.findById(organizationId); + await this.gatewayRepository.save(gateway); + } catch (err) { + this.logger.error(`Database sync of gateways failed with: ${JSON.stringify(err)}`, err); + } + }, + { + concurrency: 10, + } + ) + ); + } } diff --git a/src/services/encryption-helper.service.ts b/src/services/encryption-helper.service.ts index 20ffd847..3d94d3c2 100644 --- a/src/services/encryption-helper.service.ts +++ b/src/services/encryption-helper.service.ts @@ -1,21 +1,21 @@ -import {Injectable} from "@nestjs/common"; -import {AES, enc} from "crypto-js"; +import { Injectable } from "@nestjs/common"; +import { AES, enc } from "crypto-js"; @Injectable() export class EncryptionHelperService { - private encryptionKey; - constructor() { - this.encryptionKey = process.env.ENCRYPTION_SYMMETRIC_KEY || "SecretKey"; - } + private encryptionKey; + constructor() { + this.encryptionKey = process.env.ENCRYPTION_SYMMETRIC_KEY || "SecretKey"; + } - public basicEncrypt(input: string): string { - return AES.encrypt(input, this.encryptionKey).toString(); - } + public basicEncrypt(input: string): string { + return AES.encrypt(input, this.encryptionKey).toString(); + } - public basicDecrypt(encryptedInput: string): string { - if (!encryptedInput) { - return undefined; - } - return AES.decrypt(encryptedInput, this.encryptionKey).toString(enc.Utf8); + public basicDecrypt(encryptedInput: string): string { + if (!encryptedInput) { + return undefined; } + return AES.decrypt(encryptedInput, this.encryptionKey).toString(enc.Utf8); + } } diff --git a/src/services/health/health-check.service.ts b/src/services/health/health-check.service.ts index df7190a6..ca8af254 100644 --- a/src/services/health/health-check.service.ts +++ b/src/services/health/health-check.service.ts @@ -2,21 +2,18 @@ import { Injectable, Logger } from "@nestjs/common"; @Injectable() export class HealthCheckService { - private readonly logger = new Logger(HealthCheckService.name); + private readonly logger = new Logger(HealthCheckService.name); - // start at startup time ... - public lastHeartbeat: number = new Date().valueOf(); + // start at startup time ... + public lastHeartbeat: number = new Date().valueOf(); - private readonly MAX_HEARTBEAT_DELAY = 60000; + private readonly MAX_HEARTBEAT_DELAY = 60000; - isKafkaOk(): boolean { - const isOk = - new Date().valueOf() - this.lastHeartbeat <= this.MAX_HEARTBEAT_DELAY; - if (!isOk) { - this.logger.error( - `Healthcheck not OK. Last heartbeat from Kafka was at ${this.lastHeartbeat}` - ); - } - return isOk; + isKafkaOk(): boolean { + const isOk = new Date().valueOf() - this.lastHeartbeat <= this.MAX_HEARTBEAT_DELAY; + if (!isOk) { + this.logger.error(`Healthcheck not OK. Last heartbeat from Kafka was at ${this.lastHeartbeat}`); } + return isOk; + } } diff --git a/src/services/kafka/kafka.abstract.consumer.ts b/src/services/kafka/kafka.abstract.consumer.ts index 8eb5d9b9..4af02c1f 100644 --- a/src/services/kafka/kafka.abstract.consumer.ts +++ b/src/services/kafka/kafka.abstract.consumer.ts @@ -3,19 +3,19 @@ import { OnModuleInit } from "@nestjs/common"; import { SUBSCRIBER_COMBINED_REF_MAP } from "./kafka.decorator"; export abstract class AbstractKafkaConsumer implements OnModuleInit { - protected abstract registerTopic(): void; + protected abstract registerTopic(): void; - public async onModuleInit(): Promise { - this.registerTopic(); - } + public async onModuleInit(): Promise { + this.registerTopic(); + } - protected addTopic(topicName: string, toReplce: string): void { - const subs = SUBSCRIBER_COMBINED_REF_MAP.get(topicName); - // Replace the string part of the tuple with the correct class instance (this) - subs.forEach(x => { - if (x[0] == toReplce) { - x[0] = this; - } - }); - } + protected addTopic(topicName: string, toReplce: string): void { + const subs = SUBSCRIBER_COMBINED_REF_MAP.get(topicName); + // Replace the string part of the tuple with the correct class instance (this) + subs.forEach(x => { + if (x[0] == toReplce) { + x[0] = this; + } + }); + } } diff --git a/src/services/kafka/kafka.decorator.ts b/src/services/kafka/kafka.decorator.ts index f9e2040c..bb4b77b1 100644 --- a/src/services/kafka/kafka.decorator.ts +++ b/src/services/kafka/kafka.decorator.ts @@ -6,23 +6,20 @@ export const SUBSCRIBER_COMBINED_REF_MAP = new Map>(); // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function CombinedSubscribeTo(topic: string, uniqueName: any) { - return ( - target: { [x: string]: any }, - propertyKey: string | number, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - descriptor: any - ): any => { - const originalMethod = target[propertyKey]; - // Create this if it doesn't exist - if (!SUBSCRIBER_COMBINED_REF_MAP.has(topic)) { - SUBSCRIBER_COMBINED_REF_MAP.set(topic, []); - } - const existing = SUBSCRIBER_COMBINED_REF_MAP.get(topic); - // Append the new subscriber to it - SUBSCRIBER_COMBINED_REF_MAP.set( - topic, - existing.concat([[uniqueName, originalMethod]]) - ); - return descriptor; - }; + return ( + target: { [x: string]: any }, + propertyKey: string | number, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + descriptor: any + ): any => { + const originalMethod = target[propertyKey]; + // Create this if it doesn't exist + if (!SUBSCRIBER_COMBINED_REF_MAP.has(topic)) { + SUBSCRIBER_COMBINED_REF_MAP.set(topic, []); + } + const existing = SUBSCRIBER_COMBINED_REF_MAP.get(topic); + // Append the new subscriber to it + SUBSCRIBER_COMBINED_REF_MAP.set(topic, existing.concat([[uniqueName, originalMethod]])); + return descriptor; + }; } diff --git a/src/services/kafka/kafka.message.ts b/src/services/kafka/kafka.message.ts index 074073cc..c03727cf 100644 --- a/src/services/kafka/kafka.message.ts +++ b/src/services/kafka/kafka.message.ts @@ -1,23 +1,23 @@ export class KafkaPayload { - public body: unknown; - public messageId: string; - public messageType: string; - public topicName: string; - public createdTime?: string; + public body: unknown; + public messageId: string; + public messageType: string; + public topicName: string; + public createdTime?: string; - create?( - messageId: string, - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - body: any, - messageType: string, - topicName: string - ): KafkaPayload { - return { - messageId, - body, - messageType, - topicName, - createdTime: new Date().toISOString(), - }; - } + create?( + messageId: string, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + body: any, + messageType: string, + topicName: string + ): KafkaPayload { + return { + messageId, + body, + messageType, + topicName, + createdTime: new Date().toISOString(), + }; + } } diff --git a/src/services/kafka/kafka.service.ts b/src/services/kafka/kafka.service.ts index c261b496..f9f044b0 100644 --- a/src/services/kafka/kafka.service.ts +++ b/src/services/kafka/kafka.service.ts @@ -1,14 +1,6 @@ import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from "@nestjs/common"; import { HealthCheckService } from "@services/health/health-check.service"; -import { - Consumer, - Kafka, - KafkaMessage, - Producer, - RecordMetadata, - logLevel, - KafkaConfig, -} from "kafkajs"; +import { Consumer, Kafka, KafkaMessage, Producer, RecordMetadata, logLevel, KafkaConfig } from "kafkajs"; import { SUBSCRIBER_COMBINED_REF_MAP } from "./kafka.decorator"; import { KafkaPayload } from "./kafka.message"; @@ -19,131 +11,111 @@ import { KafkaPayload } from "./kafka.message"; */ @Injectable() export class KafkaService implements OnModuleInit, OnModuleDestroy { - private kafka: Kafka; - private producer: Producer; - private consumer: Consumer; - private readonly consumerSuffix = "-" + Math.floor(Math.random() * 100000); - private readonly logger = new Logger(KafkaService.name); - private readonly GROUP_ID = process.env.KAFKA_GROUPID || "os2iot-backend"; + private kafka: Kafka; + private producer: Producer; + private consumer: Consumer; + private readonly consumerSuffix = "-" + Math.floor(Math.random() * 100000); + private readonly logger = new Logger(KafkaService.name); + private readonly GROUP_ID = process.env.KAFKA_GROUPID || "os2iot-backend"; - constructor(private healthCheckService: HealthCheckService) { - const kafkaConfig = { - clientId: process.env.KAFKA_CLIENTID || "os2iot-client", - brokers: [ - `${ process.env.KAFKA_HOSTNAME || "localhost" }:${ - process.env.KAFKA_PORT || "9093" - }`, - ], - retry: { - initialRetryTime: 500, - retries: 8, - }, - }; - this.kafka = new Kafka({ - ssl: false, - clientId: kafkaConfig.clientId, - brokers: kafkaConfig.brokers, - logLevel: logLevel.INFO, - retry: kafkaConfig.retry, - }); - this.producer = this.kafka.producer(); - this.consumer = this.kafka.consumer({ - groupId: this.GROUP_ID + this.consumerSuffix, - heartbeatInterval: 10000, - }); - } + constructor(private healthCheckService: HealthCheckService) { + const kafkaConfig = { + clientId: process.env.KAFKA_CLIENTID || "os2iot-client", + brokers: [`${process.env.KAFKA_HOSTNAME || "localhost"}:${process.env.KAFKA_PORT || "9093"}`], + retry: { + initialRetryTime: 500, + retries: 8, + }, + }; + this.kafka = new Kafka({ + ssl: false, + clientId: kafkaConfig.clientId, + brokers: kafkaConfig.brokers, + logLevel: logLevel.INFO, + retry: kafkaConfig.retry, + }); + this.producer = this.kafka.producer(); + this.consumer = this.kafka.consumer({ + groupId: this.GROUP_ID + this.consumerSuffix, + heartbeatInterval: 10000, + }); + } - async onModuleInit(): Promise { - await this.connect(); - SUBSCRIBER_COMBINED_REF_MAP.forEach(async (functionRef, topic) => { - await this.bindAllCombinedTopicToConsumer(topic); - }); + async onModuleInit(): Promise { + await this.connect(); + SUBSCRIBER_COMBINED_REF_MAP.forEach(async (functionRef, topic) => { + await this.bindAllCombinedTopicToConsumer(topic); + }); - await this.consumer.on(this.consumer.events.HEARTBEAT, ({ timestamp }) => { - // this.logger.debug("Heartbeat ... " + timestamp); - this.healthCheckService.lastHeartbeat = timestamp; - }); - await this.consumer.on(this.consumer.events.STOP, () => { - this.logger.debug("STOP ... "); - }); - await this.consumer.on(this.consumer.events.CRASH, ({ payload }) => { - this.logger.debug("CRASH ... " + payload); - }); - await this.consumer.on(this.consumer.events.DISCONNECT, () => { - this.logger.debug("DISCONNECT ... "); - }); - } + await this.consumer.on(this.consumer.events.HEARTBEAT, ({ timestamp }) => { + // this.logger.debug("Heartbeat ... " + timestamp); + this.healthCheckService.lastHeartbeat = timestamp; + }); + await this.consumer.on(this.consumer.events.STOP, () => { + this.logger.debug("STOP ... "); + }); + await this.consumer.on(this.consumer.events.CRASH, ({ payload }) => { + this.logger.debug("CRASH ... " + payload); + }); + await this.consumer.on(this.consumer.events.DISCONNECT, () => { + this.logger.debug("DISCONNECT ... "); + }); + } - async onModuleDestroy(): Promise { - await this.disconnect(); - } + async onModuleDestroy(): Promise { + await this.disconnect(); + } - async connect(): Promise { - await this.producer.connect(); - await this.consumer.connect(); - } + async connect(): Promise { + await this.producer.connect(); + await this.consumer.connect(); + } - async disconnect(): Promise { - this.logger.debug("Disconnecting from Kafka!"); - await this.producer.disconnect(); - await this.consumer.disconnect(); - } + async disconnect(): Promise { + this.logger.debug("Disconnecting from Kafka!"); + await this.producer.disconnect(); + await this.consumer.disconnect(); + } - async bindAllCombinedTopicToConsumer(_topic: string): Promise { - await this.consumer.subscribe({ topic: _topic, fromBeginning: false }); - await this.consumer - .run({ - autoCommit: true, - autoCommitThreshold: 1, - eachMessage: async ({ - topic, - message, - }: { - topic: string; - message: KafkaMessage; - }) => { - try { - const arr = SUBSCRIBER_COMBINED_REF_MAP.get(topic); - this.logger.debug( - `Got kafka message, have ${ arr.length } receivers ...` - ); - arr.forEach(async tuple => { - const object = tuple[0]; - const fn = tuple[1]; - this.logger.debug(`Calling method ...`); - // bind the subscribed functions to topic - const msg = JSON.parse( - message.value.toString() - ) as KafkaPayload; - await fn.apply(object, [msg]); - }); - } catch (err) { - this.logger.error(`Error occurred in eachMessage: ${ err }`); - this.logger.error(`${ JSON.stringify(err) }`); - } - }, - }) - .catch(err => { - this.logger.error("Kafkajs got error: " + err); + async bindAllCombinedTopicToConsumer(_topic: string): Promise { + await this.consumer.subscribe({ topic: _topic, fromBeginning: false }); + await this.consumer + .run({ + autoCommit: true, + autoCommitThreshold: 1, + eachMessage: async ({ topic, message }: { topic: string; message: KafkaMessage }) => { + try { + const arr = SUBSCRIBER_COMBINED_REF_MAP.get(topic); + this.logger.debug(`Got kafka message, have ${arr.length} receivers ...`); + arr.forEach(async tuple => { + const object = tuple[0]; + const fn = tuple[1]; + this.logger.debug(`Calling method ...`); + // bind the subscribed functions to topic + const msg = JSON.parse(message.value.toString()) as KafkaPayload; + await fn.apply(object, [msg]); }); - } + } catch (err) { + this.logger.error(`Error occurred in eachMessage: ${err}`); + this.logger.error(`${JSON.stringify(err)}`); + } + }, + }) + .catch(err => { + this.logger.error("Kafkajs got error: " + err); + }); + } - async sendMessage( - kafkaTopic: string, - kafkaMessage: KafkaPayload - ): Promise { - const message = { - topic: kafkaTopic, - messages: [{ value: JSON.stringify(kafkaMessage) }], - }; - const metadata = await this.producer - .send(message) - .catch(async e => { - await this.producer.connect() - this.logger.warn("Kafka failed sending message, retrying", e) - await this.producer.send(message).catch(e => this.logger.error(e.message, e) - ) - }); - return metadata; - } + async sendMessage(kafkaTopic: string, kafkaMessage: KafkaPayload): Promise { + const message = { + topic: kafkaTopic, + messages: [{ value: JSON.stringify(kafkaMessage) }], + }; + const metadata = await this.producer.send(message).catch(async e => { + await this.producer.connect(); + this.logger.warn("Kafka failed sending message, retrying", e); + await this.producer.send(message).catch(e => this.logger.error(e.message, e)); + }); + return metadata; + } } diff --git a/src/services/mqtt/mqtt.service.ts b/src/services/mqtt/mqtt.service.ts index 3fe8e998..a5155882 100644 --- a/src/services/mqtt/mqtt.service.ts +++ b/src/services/mqtt/mqtt.service.ts @@ -10,99 +10,90 @@ import { caCertPath, caKeyPath } from "@resources/resource-paths"; @Injectable() export class MqttService { - constructor(private applicationService: ApplicationService) {} + constructor(private applicationService: ApplicationService) {} - public async generateCertificate(deviceName: string): Promise { - const certificateDetails = new CertificateDetails(); - try { - createPrivateKey(2048, (err, { key }) => { - if (err) { - console.log("keyerr", err); - return; - } - certificateDetails.deviceCertificateKey = key; - createCSR( - { - commonName: deviceName, - clientKey: key, - }, - function (err, { csr }) { - if (err) throw err; - const caCert = fs.readFileSync(caCertPath); - const caKey = fs.readFileSync(caKeyPath).toString(); - createCertificate( - { - csr: csr, - serviceKey: caKey, - serviceCertificate: caCert, - serial: Date.now(), - days: 365, - serviceKeyPassword: - process.env.CA_KEY_PASSWORD || "os2iot", - }, - async function (err, cert) { - if (err) throw err; - - certificateDetails.deviceCertificate = cert.certificate; - certificateDetails.ca = caCert.toString(); - } - ); - } - ); - }); - - // createPrivateKey function behaves like an async function, but cannot be awaited. This ensures waiting till it's done - while (certificateDetails.deviceCertificate === undefined) { - const wait = new Promise(resolve => setTimeout(resolve, 100)); - await wait; - } - return certificateDetails; - } catch (err) { - console.error(err); + public async generateCertificate(deviceName: string): Promise { + const certificateDetails = new CertificateDetails(); + try { + createPrivateKey(2048, (err, { key }) => { + if (err) { + console.log("keyerr", err); + return; } - } + certificateDetails.deviceCertificateKey = key; + createCSR( + { + commonName: deviceName, + clientKey: key, + }, + function (err, { csr }) { + if (err) throw err; + const caCert = fs.readFileSync(caCertPath); + const caKey = fs.readFileSync(caKeyPath).toString(); + createCertificate( + { + csr: csr, + serviceKey: caKey, + serviceCertificate: caCert, + serial: Date.now(), + days: 365, + serviceKeyPassword: process.env.CA_KEY_PASSWORD || "os2iot", + }, + async function (err, cert) { + if (err) throw err; - public async createTopic(device: MQTTInternalBrokerDevice): Promise { - const application = await this.applicationService.findOneWithOrganisation( - device.application.id + certificateDetails.deviceCertificate = cert.certificate; + certificateDetails.ca = caCert.toString(); + } + ); + } ); - const port = - device.authenticationType === AuthenticationType.PASSWORD ? "8885" : "8884"; - return { - uRL: `mqtts://${process.env.MQTT_BROKER_HOSTNAME || "localhost"}`, - topicName: `devices/${application.belongsTo.id}/${device.application.id}/${device.id}`, - port: Number(port), - }; + }); + + // createPrivateKey function behaves like an async function, but cannot be awaited. This ensures waiting till it's done + while (certificateDetails.deviceCertificate === undefined) { + const wait = new Promise(resolve => setTimeout(resolve, 100)); + await wait; + } + return certificateDetails; + } catch (err) { + console.error(err); } + } - public hashPassword(password: string) { - const salt = random(16); - const iterations = 1000; - const keySize = 64; + public async createTopic(device: MQTTInternalBrokerDevice): Promise { + const application = await this.applicationService.findOneWithOrganisation(device.application.id); + const port = device.authenticationType === AuthenticationType.PASSWORD ? "8885" : "8884"; + return { + uRL: `mqtts://${process.env.MQTT_BROKER_HOSTNAME || "localhost"}`, + topicName: `devices/${application.belongsTo.id}/${device.application.id}/${device.id}`, + port: Number(port), + }; + } - const hashed = PBKDF2(password, salt, { - keySize, - iterations, - hasher: algo.SHA512, - }); + public hashPassword(password: string) { + const salt = random(16); + const iterations = 1000; + const keySize = 64; - return ( - "PBKDF2$sha512$1000$" + - salt.toString(enc.Base64) + - "$" + - hashed.toString(enc.Base64) - ); - } + const hashed = PBKDF2(password, salt, { + keySize, + iterations, + hasher: algo.SHA512, + }); + + return "PBKDF2$sha512$1000$" + salt.toString(enc.Base64) + "$" + hashed.toString(enc.Base64); + } } export interface TopicDetails { - uRL: string; - topicName: string; - port: number; + uRL: string; + topicName: string; + port: number; } class CertificateDetails { - ca: string; - deviceCertificate: string; - deviceCertificateKey: string; + ca: string; + deviceCertificate: string; + deviceCertificateKey: string; } diff --git a/src/services/os2iot-mail.service.ts b/src/services/os2iot-mail.service.ts index 1a4b3cbf..bd3608f2 100644 --- a/src/services/os2iot-mail.service.ts +++ b/src/services/os2iot-mail.service.ts @@ -7,73 +7,74 @@ import SMTPTransport from "nodemailer/lib/smtp-transport"; @Injectable() export class OS2IoTMail { - private readonly logger = new Logger(OS2IoTMail.name); + private readonly logger = new Logger(OS2IoTMail.name); - // Transporter is re-usable, so we hold on to a reference to it, after the first time we have created and verified it - private transporter?: nodemailer.Transporter; + // Transporter is re-usable, so we hold on to a reference to it, after the first time we have created and verified it + private transporter?: nodemailer.Transporter; - constructor(private configService: ConfigService) {} + constructor(private configService: ConfigService) {} - public sendMail = async ( - mailOptions: Mail.Options - ): Promise => { - await this.checkCreateBasicMailTransporter(); - if (!mailOptions.from) { - mailOptions.from = this.configService.get("email.from"); - } - try { - return await this.transporter.sendMail(mailOptions); - } catch (error) { - this.logger.error( - "Send mail failed. To: " + mailOptions?.to + - ", Subject: " + mailOptions?.subject + - ", Error: " + JSON.stringify(error) - ); - throw new BadRequestException(ErrorCodes.SendMailError); - } - }; + public sendMail = async (mailOptions: Mail.Options): Promise => { + await this.checkCreateBasicMailTransporter(); + if (!mailOptions.from) { + mailOptions.from = this.configService.get("email.from"); + } + try { + return await this.transporter.sendMail(mailOptions); + } catch (error) { + this.logger.error( + "Send mail failed. To: " + + mailOptions?.to + + ", Subject: " + + mailOptions?.subject + + ", Error: " + + JSON.stringify(error) + ); + throw new BadRequestException(ErrorCodes.SendMailError); + } + }; - public sendMailChecked = async ( - mailOptions: Mail.Options - ): Promise => { - const response = await this.sendMail(mailOptions); - const messageId = response?.messageId; - if (response?.response) { - this.logger.verbose( - "Send-mail (messageId: " + messageId + ") response: " + response.response - ); - } - if (response?.rejected?.length) { - this.logger.error( - "Not all mail-recipients were accepted by SMPT-server. To: " + mailOptions.to + - ", Subject: " + mailOptions.subject + - ", MessageId: " + messageId + - ", RejectedAddresses: " + response.rejected - ); - throw new BadRequestException(ErrorCodes.SendMailError); - } - console.log('SENT MAIL', response); - return response; - }; + public sendMailChecked = async (mailOptions: Mail.Options): Promise => { + const response = await this.sendMail(mailOptions); + const messageId = response?.messageId; + if (response?.response) { + this.logger.verbose("Send-mail (messageId: " + messageId + ") response: " + response.response); + } + if (response?.rejected?.length) { + this.logger.error( + "Not all mail-recipients were accepted by SMPT-server. To: " + + mailOptions.to + + ", Subject: " + + mailOptions.subject + + ", MessageId: " + + messageId + + ", RejectedAddresses: " + + response.rejected + ); + throw new BadRequestException(ErrorCodes.SendMailError); + } + console.log("SENT MAIL", response); + return response; + }; - private checkCreateBasicMailTransporter = async () => { - if (!this.transporter) { - const t = nodemailer.createTransport({ - host: this.configService.get("email.host"), - port: this.configService.get("email.port"), - auth: { - user: this.configService.get("email.user"), - pass: this.configService.get("email.pass"), - }, - }); + private checkCreateBasicMailTransporter = async () => { + if (!this.transporter) { + const t = nodemailer.createTransport({ + host: this.configService.get("email.host"), + port: this.configService.get("email.port"), + auth: { + user: this.configService.get("email.user"), + pass: this.configService.get("email.pass"), + }, + }); - try { - await t.verify(); - } catch (error) { - throw new BadRequestException(ErrorCodes.SendMailError); - } + try { + await t.verify(); + } catch (error) { + throw new BadRequestException(ErrorCodes.SendMailError); + } - this.transporter = t; - } - }; + this.transporter = t; + } + }; } diff --git a/src/services/sigfox/generic-sigfox-administation.service.ts b/src/services/sigfox/generic-sigfox-administation.service.ts index a3074d56..7d09fb6a 100644 --- a/src/services/sigfox/generic-sigfox-administation.service.ts +++ b/src/services/sigfox/generic-sigfox-administation.service.ts @@ -3,179 +3,162 @@ import { SigFoxGroup } from "@entities/sigfox-group.entity"; import { ErrorCodes } from "@enum/error-codes.enum"; import { HttpService } from "@nestjs/axios"; import { - BadRequestException, - HttpException, - HttpStatus, - Injectable, - Logger, - UnauthorizedException, + BadRequestException, + HttpException, + HttpStatus, + Injectable, + Logger, + UnauthorizedException, } from "@nestjs/common"; import { AxiosRequestConfig, Method } from "axios"; import { ISetupCache, setupCache } from "axios-cache-adapter"; @Injectable() export class GenericSigfoxAdministationService { - constructor(private httpService: HttpService) { - this.cache = setupCache({ - maxAge: 5 * 60 * 1000, // 5 minutes - }); + constructor(private httpService: HttpService) { + this.cache = setupCache({ + maxAge: 5 * 60 * 1000, // 5 minutes + }); + } + private cache: ISetupCache; + + BASE_URL = "https://api.sigfox.com/v2/"; + TIMEOUT_IN_MS = 30000; + + private readonly logger = new Logger(GenericSigfoxAdministationService.name); + + async get(path: string, sigfoxGroup: SigFoxGroup, useCache?: boolean): Promise { + return await this.doRequest({ + path: path, + sigfoxGroup: sigfoxGroup, + method: "GET", + useCache, + }); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async post(path: string, dto: any, sigfoxGroup: SigFoxGroup): Promise { + return await this.doRequest({ + path: path, + sigfoxGroup: sigfoxGroup, + method: "POST", + dto, + }); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async put(path: string, dto: any, sigfoxGroup: SigFoxGroup): Promise { + return await this.doRequest({ + path: path, + sigfoxGroup: sigfoxGroup, + method: "PUT", + dto, + }); + } + + async delete(path: string, sigfoxGroup: SigFoxGroup): Promise { + return await this.doRequest({ + path: path, + sigfoxGroup: sigfoxGroup, + method: "DELETE", + }); + } + + async testConnection(sigfoxGroup: SigFoxGroup): Promise { + try { + const apiUsers = await this.get("api-users", sigfoxGroup); + return apiUsers.data.length > 0; + } catch (err) { + return false; } - private cache: ISetupCache; - - BASE_URL = "https://api.sigfox.com/v2/"; - TIMEOUT_IN_MS = 30000; - - private readonly logger = new Logger(GenericSigfoxAdministationService.name); - - async get(path: string, sigfoxGroup: SigFoxGroup, useCache?: boolean): Promise { - return await this.doRequest({ - path: path, - sigfoxGroup: sigfoxGroup, - method: "GET", - useCache, - }); + } + + private async doRequest({ + path, + sigfoxGroup, + method, + dto = undefined, + useCache = false, + }: RequestParameters): Promise { + const config = await this.generateAxiosConfig(sigfoxGroup, method, path, dto, useCache); + try { + const result = await this.httpService.request(config).toPromise(); + this.logger.debug(`${method} '${path}' got status: '${result.status} ${result.statusText}' `); + return result.data; + } catch (err) { + this.handleError(method, path, dto, err); } + } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async post(path: string, dto: any, sigfoxGroup: SigFoxGroup): Promise { - return await this.doRequest({ - path: path, - sigfoxGroup: sigfoxGroup, - method: "POST", - dto, - }); + private handleError(method: string, path: string, dto: any, err: any) { + this.logger.warn(`${method} '${path}'` + (dto != null ? `: '${JSON.stringify(dto)}'` : "")); + const response = err?.response; + if (response?.status == 401) { + throw new UnauthorizedException(ErrorCodes.SigFoxBadLogin); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async put(path: string, dto: any, sigfoxGroup: SigFoxGroup): Promise { - return await this.doRequest({ - path: path, - sigfoxGroup: sigfoxGroup, - method: "PUT", - dto, - }); + if (response?.status == 400) { + this.logger.error(`Error from SigFox: ${JSON.stringify(response?.data)}`); + throw new BadRequestException(response?.data?.errors); } - async delete(path: string, sigfoxGroup: SigFoxGroup): Promise { - return await this.doRequest({ - path: path, - sigfoxGroup: sigfoxGroup, - method: "DELETE", - }); + this.handleSigFox429(response); + + this.logger.error(`Got unexpected error from SigFox (${response?.status} ${response?.statusText})'`); + throw err; + } + + private handleSigFox429(response: any) { + if (response?.status == 429) { + this.logger.error(`Request to '${response.request.method} ${response.request.path}' got 'Too many requsts' ...`); + throw new HttpException( + { + statusCode: HttpStatus.TOO_MANY_REQUESTS, + error: "Too Many Requests", + message: "SigFox requests are limited. " + response.data.message, + }, + 429 + ); } - - async testConnection(sigfoxGroup: SigFoxGroup): Promise { - try { - const apiUsers = await this.get( - "api-users", - sigfoxGroup - ); - return apiUsers.data.length > 0; - } catch (err) { - return false; - } + } + + private async generateAxiosConfig( + sigfoxGroup: SigFoxGroup, + method: Method, + path: string, + dto?: any, + useCache?: boolean + ): Promise { + const url = this.generateUrl(path); + const axiosConfig: AxiosRequestConfig = { + timeout: this.TIMEOUT_IN_MS, + headers: { "Content-Type": "application/json" }, + auth: { + username: sigfoxGroup.username, + password: sigfoxGroup.password, + }, + method: method, + url: url, + }; + if (dto) { + axiosConfig.data = dto; } - - private async doRequest({ - path, - sigfoxGroup, - method, - dto = undefined, - useCache = false, - }: RequestParameters): Promise { - const config = await this.generateAxiosConfig( - sigfoxGroup, - method, - path, - dto, - useCache - ); - try { - const result = await this.httpService.request(config).toPromise(); - this.logger.debug( - `${method} '${path}' got status: '${result.status} ${result.statusText}' ` - ); - return result.data; - } catch (err) { - this.handleError(method, path, dto, err); - } + if (useCache) { + axiosConfig.adapter = this.cache.adapter; } - private handleError(method: string, path: string, dto: any, err: any) { - this.logger.warn( - `${method} '${path}'` + (dto != null ? `: '${JSON.stringify(dto)}'` : "") - ); - const response = err?.response; - if (response?.status == 401) { - throw new UnauthorizedException(ErrorCodes.SigFoxBadLogin); - } - - if (response?.status == 400) { - this.logger.error(`Error from SigFox: ${JSON.stringify(response?.data)}`); - throw new BadRequestException(response?.data?.errors); - } - - this.handleSigFox429(response); - - this.logger.error( - `Got unexpected error from SigFox (${response?.status} ${response?.statusText})'` - ); - throw err; - } + return axiosConfig; + } - private handleSigFox429(response: any) { - if (response?.status == 429) { - this.logger.error( - `Request to '${response.request.method} ${response.request.path}' got 'Too many requsts' ...` - ); - throw new HttpException( - { - statusCode: HttpStatus.TOO_MANY_REQUESTS, - error: "Too Many Requests", - message: "SigFox requests are limited. " + response.data.message, - }, - 429 - ); - } - } - - private async generateAxiosConfig( - sigfoxGroup: SigFoxGroup, - method: Method, - path: string, - dto?: any, - useCache?: boolean - ): Promise { - const url = this.generateUrl(path); - const axiosConfig: AxiosRequestConfig = { - timeout: this.TIMEOUT_IN_MS, - headers: { "Content-Type": "application/json" }, - auth: { - username: sigfoxGroup.username, - password: sigfoxGroup.password, - }, - method: method, - url: url, - }; - if (dto) { - axiosConfig.data = dto; - } - if (useCache) { - axiosConfig.adapter = this.cache.adapter; - } - - return axiosConfig; - } - - private generateUrl(path: string): string { - return `${this.BASE_URL}${path}`; - } + private generateUrl(path: string): string { + return `${this.BASE_URL}${path}`; + } } interface RequestParameters { - path: string; - sigfoxGroup: SigFoxGroup; - method: Method; - dto?: any; - useCache?: boolean; + path: string; + sigfoxGroup: SigFoxGroup; + method: Method; + dto?: any; + useCache?: boolean; } diff --git a/src/services/sigfox/periodic-sigfox-cleanup.service.ts b/src/services/sigfox/periodic-sigfox-cleanup.service.ts index f51e9baf..dc55d56e 100644 --- a/src/services/sigfox/periodic-sigfox-cleanup.service.ts +++ b/src/services/sigfox/periodic-sigfox-cleanup.service.ts @@ -1,7 +1,4 @@ -import { - SigFoxApiDeviceContent, - SigFoxApiDeviceResponse, -} from "@dto/sigfox/external/sigfox-api-device-response.dto"; +import { SigFoxApiDeviceContent, SigFoxApiDeviceResponse } from "@dto/sigfox/external/sigfox-api-device-response.dto"; import { SigFoxDevice } from "@entities/sigfox-device.entity"; import { Injectable, Logger } from "@nestjs/common"; import { Cron, CronExpression } from "@nestjs/schedule"; @@ -12,57 +9,51 @@ import { SigFoxGroupService } from "./sigfox-group.service"; @Injectable() export class PeriodicSigFoxCleanupService { - constructor( - private iotDevicService: IoTDeviceService, - private sigfoxApiDeviceService: SigFoxApiDeviceService, - private sigfoxGroupService: SigFoxGroupService - ) {} - private readonly logger = new Logger(PeriodicSigFoxCleanupService.name); + constructor( + private iotDevicService: IoTDeviceService, + private sigfoxApiDeviceService: SigFoxApiDeviceService, + private sigfoxGroupService: SigFoxGroupService + ) {} + private readonly logger = new Logger(PeriodicSigFoxCleanupService.name); - @Cron(CronExpression.EVERY_5_MINUTES) - async cleanupDevicesRemovedFromSigFoxBackend(): Promise { - // get all sigfox devices in database - const sigfoxDevicesInDatabasePromise = this.iotDevicService.findAllSigFoxDevices(); - // get all sigfox groups - const flattenedSigFoxDevices = await this.getDevicesInSigFoxBackend(); - const sigfoxDevicesInDb = await sigfoxDevicesInDatabasePromise; - // Find all devices ONLY in database and not in sigfox backend - const devicesToRemove = _.differenceBy( - sigfoxDevicesInDb, - flattenedSigFoxDevices, - x => { - return this.getId(x); - } - ); - if (devicesToRemove.length > 0) { - // Delete them - const idsToDelete = devicesToRemove.map(x => x.id); - this.logger.log(`Will remove devices: ${idsToDelete.join(", ")}`); - await this.iotDevicService.deleteMany(idsToDelete); - } + @Cron(CronExpression.EVERY_5_MINUTES) + async cleanupDevicesRemovedFromSigFoxBackend(): Promise { + // get all sigfox devices in database + const sigfoxDevicesInDatabasePromise = this.iotDevicService.findAllSigFoxDevices(); + // get all sigfox groups + const flattenedSigFoxDevices = await this.getDevicesInSigFoxBackend(); + const sigfoxDevicesInDb = await sigfoxDevicesInDatabasePromise; + // Find all devices ONLY in database and not in sigfox backend + const devicesToRemove = _.differenceBy(sigfoxDevicesInDb, flattenedSigFoxDevices, x => { + return this.getId(x); + }); + if (devicesToRemove.length > 0) { + // Delete them + const idsToDelete = devicesToRemove.map(x => x.id); + this.logger.log(`Will remove devices: ${idsToDelete.join(", ")}`); + await this.iotDevicService.deleteMany(idsToDelete); } + } - private async getDevicesInSigFoxBackend() { - const sigfoxGroups = await this.sigfoxGroupService.findAll(); - const uniqueSigFoxGroups = _.uniqBy(sigfoxGroups, x => x.username); - // get all sigfox devices in sigfox backend - const sigfoxDevicesInBackend = await Promise.all( - uniqueSigFoxGroups.map( - async x => (await this.sigfoxApiDeviceService.getAllByGroupIds(x)).data - ) - ); - const flattenedSigFoxDevices = _.flatten(sigfoxDevicesInBackend); - return flattenedSigFoxDevices; - } + private async getDevicesInSigFoxBackend() { + const sigfoxGroups = await this.sigfoxGroupService.findAll(); + const uniqueSigFoxGroups = _.uniqBy(sigfoxGroups, x => x.username); + // get all sigfox devices in sigfox backend + const sigfoxDevicesInBackend = await Promise.all( + uniqueSigFoxGroups.map(async x => (await this.sigfoxApiDeviceService.getAllByGroupIds(x)).data) + ); + const flattenedSigFoxDevices = _.flatten(sigfoxDevicesInBackend); + return flattenedSigFoxDevices; + } - private getId(device: SigFoxDevice | SigFoxApiDeviceContent) { - if (this.isSigFoxDevice(device)) { - return device.deviceId; - } - return device.id; + private getId(device: SigFoxDevice | SigFoxApiDeviceContent) { + if (this.isSigFoxDevice(device)) { + return device.deviceId; } + return device.id; + } - private isSigFoxDevice(obj: any): obj is SigFoxDevice { - return obj.deviceId !== undefined; - } + private isSigFoxDevice(obj: any): obj is SigFoxDevice { + return obj.deviceId !== undefined; + } } diff --git a/src/services/sigfox/sigfox-api-contract.service.ts b/src/services/sigfox/sigfox-api-contract.service.ts index 3ece909a..3b5e75df 100644 --- a/src/services/sigfox/sigfox-api-contract.service.ts +++ b/src/services/sigfox/sigfox-api-contract.service.ts @@ -1,6 +1,6 @@ import { - SigFoxApiContractInfosContent, - SigFoxApiContractInfosResponseDto, + SigFoxApiContractInfosContent, + SigFoxApiContractInfosResponseDto, } from "@dto/sigfox/external/sigfox-api-contract-infos-response.dto"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; import { Injectable } from "@nestjs/common"; @@ -8,16 +8,11 @@ import { GenericSigfoxAdministationService } from "./generic-sigfox-administatio @Injectable() export class SigFoxApiContractService { - constructor(private genericService: GenericSigfoxAdministationService) {} + constructor(private genericService: GenericSigfoxAdministationService) {} - async getContractInfos( - sigfoxGroup: SigFoxGroup - ): Promise { - const res = await this.genericService.get( - "contract-infos", - sigfoxGroup - ); + async getContractInfos(sigfoxGroup: SigFoxGroup): Promise { + const res = await this.genericService.get("contract-infos", sigfoxGroup); - return res.data; - } + return res.data; + } } diff --git a/src/services/sigfox/sigfox-api-device-type.service.ts b/src/services/sigfox/sigfox-api-device-type.service.ts index 20509460..e7dbcee6 100644 --- a/src/services/sigfox/sigfox-api-device-type.service.ts +++ b/src/services/sigfox/sigfox-api-device-type.service.ts @@ -1,12 +1,12 @@ import { CreateSigFoxApiCallbackRequestDto } from "@dto/sigfox/external/create-sigfox-api-callback-request.dto"; import { CreateSigFoxApiDeviceTypeRequestDto } from "@dto/sigfox/external/create-sigfox-api-device-type-request.dto"; import { - SigFoxApiCallbackContent, - SigFoxApiCallbacksResponseDto, + SigFoxApiCallbackContent, + SigFoxApiCallbacksResponseDto, } from "@dto/sigfox/external/sigfox-api-callbacks-response.dto"; import { - SigFoxApiDeviceTypeContent, - SigFoxApiDeviceTypeResponse, + SigFoxApiDeviceTypeContent, + SigFoxApiDeviceTypeResponse, } from "@dto/sigfox/external/sigfox-api-device-type-response.dto"; import { SigFoxApiIdReferenceDto } from "@dto/sigfox/external/sigfox-api-id-reference.dto"; import { SigFoxGroup } from "@entities/sigfox-group.entity"; @@ -18,18 +18,17 @@ import { SigfoxApiUsersService } from "./sigfox-api-users.service"; @Injectable() export class SigFoxApiDeviceTypeService { - constructor( - private genericService: GenericSigfoxAdministationService, - private usersService: SigfoxApiUsersService, - configService: ConfigService - ) { - this.OS2IOT_BACKEND_URL = configService.get("backend.baseurl"); - } - - private OS2IOT_BACKEND_URL: string; - private readonly OS2IOT_BACKEND_SIGFOX_CALLBACK_PATH = - "/api/v1/sigfox-callback/data/bidir?apiKey={deviceTypeId}"; - private readonly CALLBACK_BODY_TEMPLATE = `{ + constructor( + private genericService: GenericSigfoxAdministationService, + private usersService: SigfoxApiUsersService, + configService: ConfigService + ) { + this.OS2IOT_BACKEND_URL = configService.get("backend.baseurl"); + } + + private OS2IOT_BACKEND_URL: string; + private readonly OS2IOT_BACKEND_SIGFOX_CALLBACK_PATH = "/api/v1/sigfox-callback/data/bidir?apiKey={deviceTypeId}"; + private readonly CALLBACK_BODY_TEMPLATE = `{ "time": {time}, "deviceTypeId": "{deviceTypeId}", "deviceId": "{device}", @@ -37,141 +36,107 @@ export class SigFoxApiDeviceTypeService { "seqNumber": {seqNumber}, "ack": {ack} }`; - private readonly CALLBACK_CONTENT_TYPE = "application/json"; - private readonly URL_BASE = "device-types"; - private readonly CALLBACK_CHANNEL = "URL"; - private readonly CALLBACK_TYPE = 0; // Data - private readonly CALLBACK_SUBTYPE = 3; // BIDIR - private readonly CALLBACK_HTTP_METHOD = "POST"; - private readonly CALLBACK_SEND_SNI = true; - private readonly CALLBACK_ENABLED = true; - - async getAllByGroupIds( - sigfoxGroup: SigFoxGroup, - groupIds?: string[] - ): Promise { - let url = this.URL_BASE; - if (groupIds.length > 0) { - url += "?groupIds=" + groupIds.join(","); - } - return await this.genericService.get(url, sigfoxGroup); - } - - async getById( - sigfoxGroup: SigFoxGroup, - id: string - ): Promise { - const url = `${this.URL_BASE}/${id}`; - return await this.genericService.get(url, sigfoxGroup); - } - - async create( - sigfoxGroup: SigFoxGroup, - dto: CreateSigFoxApiDeviceTypeRequestDto - ): Promise { - await this.setDefaults(dto, sigfoxGroup); - return await this.genericService.post(this.URL_BASE, dto, sigfoxGroup); - } - - async update( - group: SigFoxGroup, - id: string, - dto: CreateSigFoxApiDeviceTypeRequestDto - ): Promise { - const URL = `${this.URL_BASE}/${id}`; - await this.genericService.put(URL, dto, group); - } - - async delete(group: SigFoxGroup, id: string): Promise { - const url = `${this.URL_BASE}/${id}`; - await this.genericService.delete(url, group); - } - - async addOrUpdateCallback(group: SigFoxGroup, deviceTypeId: string): Promise { - const url = `${this.URL_BASE}/${deviceTypeId}/callbacks`; - const response = await this.genericService.get( - url, - group - ); - const callback = response.data.find(x => - x.url.startsWith(this.OS2IOT_BACKEND_URL) - ); - const dto: CreateSigFoxApiCallbackRequestDto = this.makeDto(); - let callbackId; - - if (callback) { - // Callback exists, make sure it's OK - if (this.isCallbackNotOk(callback)) { - await this.genericService.put(`${url}/${callback.id}`, dto, group); - } - callbackId = callback.id; - } else { - callbackId = ( - await this.genericService.post(url, dto, group) - ).id; - } - - // set active downlink to this - await this.setActiveDownlinkCallback(group, deviceTypeId, callbackId); - } - - private isCallbackNotOk(callback: SigFoxApiCallbackContent) { - return ( - callback.enabled != this.CALLBACK_ENABLED || - callback.sendSni != this.CALLBACK_SEND_SNI || - callback.bodyTemplate != this.CALLBACK_BODY_TEMPLATE || - callback.channel != this.CALLBACK_CHANNEL || - callback.callbackSubtype != this.CALLBACK_SUBTYPE || - callback.httpMethod != this.CALLBACK_HTTP_METHOD || - callback.contentType != this.CALLBACK_CONTENT_TYPE - ); + private readonly CALLBACK_CONTENT_TYPE = "application/json"; + private readonly URL_BASE = "device-types"; + private readonly CALLBACK_CHANNEL = "URL"; + private readonly CALLBACK_TYPE = 0; // Data + private readonly CALLBACK_SUBTYPE = 3; // BIDIR + private readonly CALLBACK_HTTP_METHOD = "POST"; + private readonly CALLBACK_SEND_SNI = true; + private readonly CALLBACK_ENABLED = true; + + async getAllByGroupIds(sigfoxGroup: SigFoxGroup, groupIds?: string[]): Promise { + let url = this.URL_BASE; + if (groupIds.length > 0) { + url += "?groupIds=" + groupIds.join(","); } - - private async setActiveDownlinkCallback( - group: SigFoxGroup, - deviceTypeId: string, - callbackId: string - ) { - await this.genericService.put( - `${this.URL_BASE}/${deviceTypeId}/callbacks/${callbackId}/downlink`, - {}, - group - ); - } - - private makeDto(): CreateSigFoxApiCallbackRequestDto { - return { - channel: this.CALLBACK_CHANNEL, - callbackType: this.CALLBACK_TYPE, - callbackSubtype: this.CALLBACK_SUBTYPE, - payloadConfig: "", - enabled: this.CALLBACK_ENABLED, - url: this.OS2IOT_BACKEND_URL + this.OS2IOT_BACKEND_SIGFOX_CALLBACK_PATH, - httpMethod: this.CALLBACK_HTTP_METHOD, - sendSni: this.CALLBACK_SEND_SNI, - bodyTemplate: this.CALLBACK_BODY_TEMPLATE, - contentType: this.CALLBACK_CONTENT_TYPE, - }; + return await this.genericService.get(url, sigfoxGroup); + } + + async getById(sigfoxGroup: SigFoxGroup, id: string): Promise { + const url = `${this.URL_BASE}/${id}`; + return await this.genericService.get(url, sigfoxGroup); + } + + async create(sigfoxGroup: SigFoxGroup, dto: CreateSigFoxApiDeviceTypeRequestDto): Promise { + await this.setDefaults(dto, sigfoxGroup); + return await this.genericService.post(this.URL_BASE, dto, sigfoxGroup); + } + + async update(group: SigFoxGroup, id: string, dto: CreateSigFoxApiDeviceTypeRequestDto): Promise { + const URL = `${this.URL_BASE}/${id}`; + await this.genericService.put(URL, dto, group); + } + + async delete(group: SigFoxGroup, id: string): Promise { + const url = `${this.URL_BASE}/${id}`; + await this.genericService.delete(url, group); + } + + async addOrUpdateCallback(group: SigFoxGroup, deviceTypeId: string): Promise { + const url = `${this.URL_BASE}/${deviceTypeId}/callbacks`; + const response = await this.genericService.get(url, group); + const callback = response.data.find(x => x.url.startsWith(this.OS2IOT_BACKEND_URL)); + const dto: CreateSigFoxApiCallbackRequestDto = this.makeDto(); + let callbackId; + + if (callback) { + // Callback exists, make sure it's OK + if (this.isCallbackNotOk(callback)) { + await this.genericService.put(`${url}/${callback.id}`, dto, group); + } + callbackId = callback.id; + } else { + callbackId = (await this.genericService.post(url, dto, group)).id; } - private async setDefaults( - dto: CreateSigFoxApiDeviceTypeRequestDto, - sigfoxGroup: SigFoxGroup - ) { - const sigfoxApiGroup = await this.usersService.getByUserId( - sigfoxGroup.username, - sigfoxGroup - ); - dto.groupId = sigfoxApiGroup.group.id; - - dto.downlinkMode = SigFoxDownlinkMode.CALLBACK; - dto.downlinkDataString = null; - - dto.payloadType = SigFoxPayloadType.RegularRawPayload; - dto.payloadConfig = null; - - dto.automaticRenewal = true; - - dto.geolocPayloadConfigId = null; - } + // set active downlink to this + await this.setActiveDownlinkCallback(group, deviceTypeId, callbackId); + } + + private isCallbackNotOk(callback: SigFoxApiCallbackContent) { + return ( + callback.enabled != this.CALLBACK_ENABLED || + callback.sendSni != this.CALLBACK_SEND_SNI || + callback.bodyTemplate != this.CALLBACK_BODY_TEMPLATE || + callback.channel != this.CALLBACK_CHANNEL || + callback.callbackSubtype != this.CALLBACK_SUBTYPE || + callback.httpMethod != this.CALLBACK_HTTP_METHOD || + callback.contentType != this.CALLBACK_CONTENT_TYPE + ); + } + + private async setActiveDownlinkCallback(group: SigFoxGroup, deviceTypeId: string, callbackId: string) { + await this.genericService.put(`${this.URL_BASE}/${deviceTypeId}/callbacks/${callbackId}/downlink`, {}, group); + } + + private makeDto(): CreateSigFoxApiCallbackRequestDto { + return { + channel: this.CALLBACK_CHANNEL, + callbackType: this.CALLBACK_TYPE, + callbackSubtype: this.CALLBACK_SUBTYPE, + payloadConfig: "", + enabled: this.CALLBACK_ENABLED, + url: this.OS2IOT_BACKEND_URL + this.OS2IOT_BACKEND_SIGFOX_CALLBACK_PATH, + httpMethod: this.CALLBACK_HTTP_METHOD, + sendSni: this.CALLBACK_SEND_SNI, + bodyTemplate: this.CALLBACK_BODY_TEMPLATE, + contentType: this.CALLBACK_CONTENT_TYPE, + }; + } + + private async setDefaults(dto: CreateSigFoxApiDeviceTypeRequestDto, sigfoxGroup: SigFoxGroup) { + const sigfoxApiGroup = await this.usersService.getByUserId(sigfoxGroup.username, sigfoxGroup); + dto.groupId = sigfoxApiGroup.group.id; + + dto.downlinkMode = SigFoxDownlinkMode.CALLBACK; + dto.downlinkDataString = null; + + dto.payloadType = SigFoxPayloadType.RegularRawPayload; + dto.payloadConfig = null; + + dto.automaticRenewal = true; + + dto.geolocPayloadConfigId = null; + } } diff --git a/src/services/sigfox/sigfox-api-device.service.ts b/src/services/sigfox/sigfox-api-device.service.ts index ea951003..03f0b02b 100644 --- a/src/services/sigfox/sigfox-api-device.service.ts +++ b/src/services/sigfox/sigfox-api-device.service.ts @@ -1,9 +1,6 @@ import { CreateSigFoxApiDeviceRequestDto } from "@dto/sigfox/external/create-sigfox-api-device-request.dto"; import { SigFoxApiBulkTransferRequestDto } from "@dto/sigfox/external/sigfox-api-bulk-transfer-request.dto"; -import { - SigFoxApiDeviceContent, - SigFoxApiDeviceResponse, -} from "@dto/sigfox/external/sigfox-api-device-response.dto"; +import { SigFoxApiDeviceContent, SigFoxApiDeviceResponse } from "@dto/sigfox/external/sigfox-api-device-response.dto"; import { SigFoxApiIdReferenceDto } from "@dto/sigfox/external/sigfox-api-id-reference.dto"; import { SigFoxApiSingleDeviceResponseDto } from "@dto/sigfox/external/sigfox-api-single-device-response.dto"; import { UpdateSigFoxApiDeviceRequestDto } from "@dto/sigfox/external/update-sigfox-api-device-request.dto"; @@ -13,95 +10,73 @@ import { GenericSigfoxAdministationService } from "./generic-sigfox-administatio @Injectable() export class SigFoxApiDeviceService { - constructor(private genericService: GenericSigfoxAdministationService) {} + constructor(private genericService: GenericSigfoxAdministationService) {} - private readonly URL_BASE = "devices"; + private readonly URL_BASE = "devices"; - private readonly logger = new Logger(SigFoxApiDeviceService.name); + private readonly logger = new Logger(SigFoxApiDeviceService.name); - async getAllByGroupIds( - sigfoxGroup: SigFoxGroup, - groupIds?: string[] - ): Promise { - let url = - this.URL_BASE + - "?fields=productCertificate(key),contract(name),group(name),deviceType(name)"; - if (groupIds?.length > 0) { - url += "&groupIds=" + groupIds.join(","); - } - return await this.genericService.get(url, sigfoxGroup); + async getAllByGroupIds(sigfoxGroup: SigFoxGroup, groupIds?: string[]): Promise { + let url = this.URL_BASE + "?fields=productCertificate(key),contract(name),group(name),deviceType(name)"; + if (groupIds?.length > 0) { + url += "&groupIds=" + groupIds.join(","); } + return await this.genericService.get(url, sigfoxGroup); + } - async getByIdSimple( - sigfoxGroup: SigFoxGroup, - id: string - ): Promise { - const devices = await this.getAllByGroupIds(sigfoxGroup); - return devices.data.find(x => x.id == id); - } + async getByIdSimple(sigfoxGroup: SigFoxGroup, id: string): Promise { + const devices = await this.getAllByGroupIds(sigfoxGroup); + return devices.data.find(x => x.id == id); + } - async getById( - sigfoxGroup: SigFoxGroup, - id: string - ): Promise { - const url = `${this.URL_BASE}/${id}`; - return await this.genericService.get(url, sigfoxGroup, true); - } + async getById(sigfoxGroup: SigFoxGroup, id: string): Promise { + const url = `${this.URL_BASE}/${id}`; + return await this.genericService.get(url, sigfoxGroup, true); + } - async create( - sigfoxGroup: SigFoxGroup, - dto: CreateSigFoxApiDeviceRequestDto - ): Promise { - return await this.genericService.post(this.URL_BASE, dto, sigfoxGroup); - } + async create(sigfoxGroup: SigFoxGroup, dto: CreateSigFoxApiDeviceRequestDto): Promise { + return await this.genericService.post(this.URL_BASE, dto, sigfoxGroup); + } - async update( - group: SigFoxGroup, - id: string, - dto: UpdateSigFoxApiDeviceRequestDto - ): Promise { - const URL = `${this.URL_BASE}/${id}`; - await this.genericService.put(URL, dto, group); - } + async update(group: SigFoxGroup, id: string, dto: UpdateSigFoxApiDeviceRequestDto): Promise { + const URL = `${this.URL_BASE}/${id}`; + await this.genericService.put(URL, dto, group); + } - async delete(group: SigFoxGroup, id: string): Promise { - const deleteUrl = `${this.URL_BASE}/${id}`; + async delete(group: SigFoxGroup, id: string): Promise { + const deleteUrl = `${this.URL_BASE}/${id}`; - // To delete a sigfox device, first unsubscribe, then delete - await this.unsubscribe(group, id); + // To delete a sigfox device, first unsubscribe, then delete + await this.unsubscribe(group, id); - await this.genericService.delete(deleteUrl, group); - } + await this.genericService.delete(deleteUrl, group); + } - async changeDeviceType( - group: SigFoxGroup, - id: string, - newDeviceType: string - ): Promise { - const dto: SigFoxApiBulkTransferRequestDto = { - deviceTypeId: newDeviceType, - data: [ - { - id: id, - keepHistory: true, - activable: true, - }, - ], - }; + async changeDeviceType(group: SigFoxGroup, id: string, newDeviceType: string): Promise { + const dto: SigFoxApiBulkTransferRequestDto = { + deviceTypeId: newDeviceType, + data: [ + { + id: id, + keepHistory: true, + activable: true, + }, + ], + }; - await this.genericService.post(`${this.URL_BASE}/bulk/transfer`, dto, group); - } + await this.genericService.post(`${this.URL_BASE}/bulk/transfer`, dto, group); + } - private async unsubscribe(group: SigFoxGroup, id: string) { - const unsubscribeUrl = `${this.URL_BASE}/${id}/unsubscribe`; - const unsubscribeDto = { - unsubscriptionTime: new Date().valueOf(), - }; - try { - await this.genericService.put(unsubscribeUrl, unsubscribeDto, group); - } catch (err) { - this.logger.error(`SigFox backend failed to unsubscribe device '${id}'`); - throw new BadRequestException(err); - } + private async unsubscribe(group: SigFoxGroup, id: string) { + const unsubscribeUrl = `${this.URL_BASE}/${id}/unsubscribe`; + const unsubscribeDto = { + unsubscriptionTime: new Date().valueOf(), + }; + try { + await this.genericService.put(unsubscribeUrl, unsubscribeDto, group); + } catch (err) { + this.logger.error(`SigFox backend failed to unsubscribe device '${id}'`); + throw new BadRequestException(err); } + } } diff --git a/src/services/sigfox/sigfox-api-group.service.ts b/src/services/sigfox/sigfox-api-group.service.ts index 7cd02dc0..06da9280 100644 --- a/src/services/sigfox/sigfox-api-group.service.ts +++ b/src/services/sigfox/sigfox-api-group.service.ts @@ -5,11 +5,11 @@ import { GenericSigfoxAdministationService } from "@services/sigfox/generic-sigf @Injectable() export class SigfoxApiGroupService { - constructor(private genericService: GenericSigfoxAdministationService) {} + constructor(private genericService: GenericSigfoxAdministationService) {} - private readonly BASE_URL = "groups"; + private readonly BASE_URL = "groups"; - async getGroups(credentials: SigFoxGroup): Promise { - return await this.genericService.get(`${this.BASE_URL}`, credentials); - } + async getGroups(credentials: SigFoxGroup): Promise { + return await this.genericService.get(`${this.BASE_URL}`, credentials); + } } diff --git a/src/services/sigfox/sigfox-api-users.service.ts b/src/services/sigfox/sigfox-api-users.service.ts index aa96d3cd..23d70750 100644 --- a/src/services/sigfox/sigfox-api-users.service.ts +++ b/src/services/sigfox/sigfox-api-users.service.ts @@ -5,14 +5,11 @@ import { GenericSigfoxAdministationService } from "./generic-sigfox-administatio @Injectable() export class SigfoxApiUsersService { - constructor(private genericService: GenericSigfoxAdministationService) {} + constructor(private genericService: GenericSigfoxAdministationService) {} - private readonly BASE_URL = "api-users"; + private readonly BASE_URL = "api-users"; - async getByUserId( - userId: string, - credentials: SigFoxGroup - ): Promise { - return await this.genericService.get(`${this.BASE_URL}/${userId}`, credentials); - } + async getByUserId(userId: string, credentials: SigFoxGroup): Promise { + return await this.genericService.get(`${this.BASE_URL}/${userId}`, credentials); + } } diff --git a/src/services/sigfox/sigfox-group.service.ts b/src/services/sigfox/sigfox-group.service.ts index 16a2d1b3..3e198763 100644 --- a/src/services/sigfox/sigfox-group.service.ts +++ b/src/services/sigfox/sigfox-group.service.ts @@ -1,10 +1,4 @@ -import { - BadRequestException, - Inject, - Injectable, - Logger, - UnauthorizedException, -} from "@nestjs/common"; +import { BadRequestException, Inject, Injectable, Logger, UnauthorizedException } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { FindOneOptions, Repository, FindOptionsWhere } from "typeorm"; @@ -19,171 +13,150 @@ import { GenericSigfoxAdministationService } from "./generic-sigfox-administatio @Injectable() export class SigFoxGroupService { - constructor( - @InjectRepository(SigFoxGroup) - private repository: Repository, - @Inject(OrganizationService) - private organizationService: OrganizationService, - private sigfoxApiGroupService: SigfoxApiGroupService, - private genericSigfoxAdministationService: GenericSigfoxAdministationService - ) {} - - private readonly logger = new Logger(SigFoxGroupService.name); - - async findAll(): Promise { - return await this.repository.find({ - select: ["username", "password"], - }); + constructor( + @InjectRepository(SigFoxGroup) + private repository: Repository, + @Inject(OrganizationService) + private organizationService: OrganizationService, + private sigfoxApiGroupService: SigfoxApiGroupService, + private genericSigfoxAdministationService: GenericSigfoxAdministationService + ) {} + + private readonly logger = new Logger(SigFoxGroupService.name); + + async findAll(): Promise { + return await this.repository.find({ + select: ["username", "password"], + }); + } + + async findAllForOrganization(organizationId: number): Promise { + const [data, count] = await this.repository.findAndCount({ + where: { + belongsTo: { + id: organizationId, + }, + }, + select: ["username", "password", "id"], + relations: ["belongsTo"], + }); + + // TODO: Find a better way to do this + // - Deduplicate lookups at least. + await this.addSigFoxDataToAllGroupsAndSave(data); + + return { + data: data, + count: count, + }; + } + + private async addSigFoxDataToAllGroupsAndSave(data: SigFoxGroup[]) { + await Promise.all(data.map(async x => await this.addSigFoxDataToGroupAndSave(x))); + } + + private async addSigFoxDataToGroupAndSave(group: SigFoxGroup) { + // if password was not included, then include it now. + if (group.password == null) { + group = await this.findOneWithPassword(group.id); } - - async findAllForOrganization( - organizationId: number - ): Promise { - const [data, count] = await this.repository.findAndCount({ - where: { - belongsTo: { - id: organizationId, - }, - }, - select: ["username", "password", "id"], - relations: ["belongsTo"], - }); - - // TODO: Find a better way to do this - // - Deduplicate lookups at least. - await this.addSigFoxDataToAllGroupsAndSave(data); - - return { - data: data, - count: count, - }; + let apiGroupResponse; + try { + apiGroupResponse = await this.sigfoxApiGroupService.getGroups(group); + } catch (err) { + this.logger.warn(`Got error from SigFox: ${err?.response?.error}`); + group.sigfoxGroupData = null; + return group; } - - private async addSigFoxDataToAllGroupsAndSave(data: SigFoxGroup[]) { - await Promise.all(data.map(async x => await this.addSigFoxDataToGroupAndSave(x))); + if (apiGroupResponse.data.length > 1) { + this.logger.warn(`API user ${group.id} has access to more than one group`); } - - private async addSigFoxDataToGroupAndSave(group: SigFoxGroup) { - // if password was not included, then include it now. - if (group.password == null) { - group = await this.findOneWithPassword(group.id); - } - let apiGroupResponse; - try { - apiGroupResponse = await this.sigfoxApiGroupService.getGroups(group); - } catch (err) { - this.logger.warn(`Got error from SigFox: ${err?.response?.error}`); - group.sigfoxGroupData = null; - return group; - } - if (apiGroupResponse.data.length > 1) { - this.logger.warn(`API user ${group.id} has access to more than one group`); - } - const firstGroup = apiGroupResponse.data[0]; - group.sigfoxGroupData = firstGroup; - group.sigfoxGroupId = group.sigfoxGroupData.id; - await this.repository.save(group); - // remove password again ... - group.password = undefined; - } - - async findOne(id: number): Promise { - const res = await this.findOneWithPassword(id); - - await this.addSigFoxDataToAllGroupsAndSave([res]); - - return res; + const firstGroup = apiGroupResponse.data[0]; + group.sigfoxGroupData = firstGroup; + group.sigfoxGroupId = group.sigfoxGroupData.id; + await this.repository.save(group); + // remove password again ... + group.password = undefined; + } + + async findOne(id: number): Promise { + const res = await this.findOneWithPassword(id); + + await this.addSigFoxDataToAllGroupsAndSave([res]); + + return res; + } + + async findOneForPermissionCheck(id: number): Promise { + return await this.repository.findOneOrFail({ + where: { id }, + relations: ["belongsTo"], + select: ["username", "sigfoxGroupId", "id"], + }); + } + + async findOneWithPassword(id: number): Promise { + return await this.repository.findOneOrFail({ + where: { id }, + relations: ["belongsTo"], + select: ["username", "password", "sigfoxGroupId", "id", "createdBy", "updatedBy"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } + + async findOneByGroupId(groupId: string, orgId?: number): Promise { + const conditions: FindOptionsWhere = { + sigfoxGroupId: groupId, + }; + + if (orgId) { + conditions.belongsTo = { + id: orgId, + }; } - async findOneForPermissionCheck(id: number): Promise { - return await this.repository.findOneOrFail({ - where: { id }, - relations: ["belongsTo"], - select: ["username", "sigfoxGroupId", "id"], - }); + const options: FindOneOptions = { + where: conditions, + relations: ["belongsTo"], + select: ["id", "username", "password", "sigfoxGroupId"], + }; + + return await this.repository.findOneOrFail(options); + } + + async create(query: CreateSigFoxGroupRequestDto, userId: number): Promise { + const sigfoxGroup = new SigFoxGroup(); + try { + sigfoxGroup.belongsTo = await this.organizationService.findById(query.organizationId); + } catch (err) { + throw new BadRequestException(ErrorCodes.OrganizationDoesNotExists); } - async findOneWithPassword(id: number): Promise { - return await this.repository.findOneOrFail({ - where: { id }, - relations: ["belongsTo"], - select: [ - "username", - "password", - "sigfoxGroupId", - "id", - "createdBy", - "updatedBy", - ], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); + const mappedSigfoxGroup = await this.map(sigfoxGroup, query); + mappedSigfoxGroup.createdBy = userId; + mappedSigfoxGroup.updatedBy = userId; + await this.addSigFoxDataToAllGroupsAndSave([mappedSigfoxGroup]); + return mappedSigfoxGroup; + } + + async update(sigfoxGroup: SigFoxGroup, query: UpdateSigFoxGroupRequestDto, userId: number): Promise { + const mappedSigfoxGroup = await this.map(sigfoxGroup, query); + mappedSigfoxGroup.updatedBy = userId; + await this.addSigFoxDataToAllGroupsAndSave([mappedSigfoxGroup]); + return mappedSigfoxGroup; + } + + private async map(sigfoxGroup: SigFoxGroup, query: UpdateSigFoxGroupRequestDto): Promise { + sigfoxGroup.username = query.username; + sigfoxGroup.password = query.password; + + // Test that new credentials are good. + if (!(await this.genericSigfoxAdministationService.testConnection(sigfoxGroup))) { + throw new UnauthorizedException(ErrorCodes.SigFoxBadLogin); } - async findOneByGroupId(groupId: string, orgId?: number): Promise { - const conditions: FindOptionsWhere = { - sigfoxGroupId: groupId, - }; - - if (orgId) { - conditions.belongsTo = { - id: orgId, - }; - } - - const options: FindOneOptions = { - where: conditions, - relations: ["belongsTo"], - select: ["id", "username", "password", "sigfoxGroupId"], - }; - - return await this.repository.findOneOrFail(options); - } - - async create( - query: CreateSigFoxGroupRequestDto, - userId: number - ): Promise { - const sigfoxGroup = new SigFoxGroup(); - try { - sigfoxGroup.belongsTo = await this.organizationService.findById( - query.organizationId - ); - } catch (err) { - throw new BadRequestException(ErrorCodes.OrganizationDoesNotExists); - } - - const mappedSigfoxGroup = await this.map(sigfoxGroup, query); - mappedSigfoxGroup.createdBy = userId; - mappedSigfoxGroup.updatedBy = userId; - await this.addSigFoxDataToAllGroupsAndSave([mappedSigfoxGroup]); - return mappedSigfoxGroup; - } - - async update( - sigfoxGroup: SigFoxGroup, - query: UpdateSigFoxGroupRequestDto, - userId: number - ): Promise { - const mappedSigfoxGroup = await this.map(sigfoxGroup, query); - mappedSigfoxGroup.updatedBy = userId; - await this.addSigFoxDataToAllGroupsAndSave([mappedSigfoxGroup]); - return mappedSigfoxGroup; - } - - private async map( - sigfoxGroup: SigFoxGroup, - query: UpdateSigFoxGroupRequestDto - ): Promise { - sigfoxGroup.username = query.username; - sigfoxGroup.password = query.password; - - // Test that new credentials are good. - if (!(await this.genericSigfoxAdministationService.testConnection(sigfoxGroup))) { - throw new UnauthorizedException(ErrorCodes.SigFoxBadLogin); - } - - return sigfoxGroup; - } + return sigfoxGroup; + } } diff --git a/src/services/sigfox/sigfox-messages.service.ts b/src/services/sigfox/sigfox-messages.service.ts index 668a79f3..e87c76ca 100644 --- a/src/services/sigfox/sigfox-messages.service.ts +++ b/src/services/sigfox/sigfox-messages.service.ts @@ -5,22 +5,18 @@ import { Between, IsNull, Not, Repository } from "typeorm"; @Injectable() export class SigFoxMessagesService { - constructor( - @InjectRepository(ReceivedMessageSigFoxSignals) - private receivedMessageSigFoxSignalsRepository: Repository - ) {} + constructor( + @InjectRepository(ReceivedMessageSigFoxSignals) + private receivedMessageSigFoxSignalsRepository: Repository + ) {} - getMessageSignals( - deviceId: number, - fromDate: Date, - toDate: Date - ): Promise { - return this.receivedMessageSigFoxSignalsRepository.find({ - where: { - device: { id: deviceId }, - sentTime: Between(fromDate, toDate), - rssi: Not(IsNull()), - }, - }); - } + getMessageSignals(deviceId: number, fromDate: Date, toDate: Date): Promise { + return this.receivedMessageSigFoxSignalsRepository.find({ + where: { + device: { id: deviceId }, + sentTime: Between(fromDate, toDate), + rssi: Not(IsNull()), + }, + }); + } } diff --git a/src/services/user-management/auth.service.ts b/src/services/user-management/auth.service.ts index 74dedbc8..f851b412 100644 --- a/src/services/user-management/auth.service.ts +++ b/src/services/user-management/auth.service.ts @@ -15,129 +15,125 @@ import { UserService } from "./user.service"; @Injectable() export class AuthService { - constructor( - private usersService: UserService, - private jwtService: JwtService, - private apiKeyService: ApiKeyService - ) { - this.KOMBIT_ROLE_URI = configuration()["kombit"]["roleUri"]; - } - private readonly logger = new Logger(AuthService.name); - private readonly KOMBIT_ROLE_URI: string; - - public async validateUser(username: string, password: string): Promise { - const user = await this.usersService.findOneUserByEmailWithPassword(username); - if (user) { - if (!user.active) { - throw new UnauthorizedException(ErrorCodes.UserInactive); - } - - const res = await compare(password, user.passwordHash); - if (res === true) { - await this.usersService.updateLastLoginToNow(user); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { passwordHash, ...result } = user; - return result; - } else { - this.logger.warn(`Login with user: '${username}' used wrong password`); - } - } else { - this.logger.warn(`Login with non-existing user: '${username}'`); - } - return null; - } - - public async validateKombitUser(profile: Profile): Promise { - const privilegesBase64 = await this.getPrivilegesIntermediate(profile); - if (!privilegesBase64 || !this.isAllowed(privilegesBase64)) { - // User doesn't have brugersystemrolle ... - throw new UnauthorizedException(ErrorCodes.MissingRole); - } - // Check if they have attribute to allow them into OS2IOT - let user = await this.usersService.findOneByNameId(profile.nameID); - if (user) { - this.logger.debug(`User from Kombit ('${profile.nameID}') already exists with id: ${user.id}`); - if (!user.active) { - this.logger.debug(`User (${user.id}) is disabled, not allowed!`); - throw new UnauthorizedException(ErrorCodes.UserInactive); - } - } else { - this.logger.debug(`User from Kombit ('${profile.nameID}') does not already exist, will create.`); - - user = await this.usersService.createUserFromKombit(profile); - } - + constructor(private usersService: UserService, private jwtService: JwtService, private apiKeyService: ApiKeyService) { + this.KOMBIT_ROLE_URI = configuration()["kombit"]["roleUri"]; + } + private readonly logger = new Logger(AuthService.name); + private readonly KOMBIT_ROLE_URI: string; + + public async validateUser(username: string, password: string): Promise { + const user = await this.usersService.findOneUserByEmailWithPassword(username); + if (user) { + if (!user.active) { + throw new UnauthorizedException(ErrorCodes.UserInactive); + } + + const res = await compare(password, user.passwordHash); + if (res === true) { await this.usersService.updateLastLoginToNow(user); - - return user; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { passwordHash, ...result } = user; + return result; + } else { + this.logger.warn(`Login with user: '${username}' used wrong password`); + } + } else { + this.logger.warn(`Login with non-existing user: '${username}'`); } - - public async issueJwt(email: string, id: number, isKombit?: boolean): Promise { - const payload: JwtPayloadDto = { username: email, sub: id, isKombit: isKombit }; - return { - accessToken: this.jwtService.sign(payload), - }; + return null; + } + + public async validateKombitUser(profile: Profile): Promise { + const privilegesBase64 = await this.getPrivilegesIntermediate(profile); + if (!privilegesBase64 || !this.isAllowed(privilegesBase64)) { + // User doesn't have brugersystemrolle ... + throw new UnauthorizedException(ErrorCodes.MissingRole); + } + // Check if they have attribute to allow them into OS2IOT + let user = await this.usersService.findOneByNameId(profile.nameID); + if (user) { + this.logger.debug(`User from Kombit ('${profile.nameID}') already exists with id: ${user.id}`); + if (!user.active) { + this.logger.debug(`User (${user.id}) is disabled, not allowed!`); + throw new UnauthorizedException(ErrorCodes.UserInactive); + } + } else { + this.logger.debug(`User from Kombit ('${profile.nameID}') does not already exist, will create.`); + + user = await this.usersService.createUserFromKombit(profile); } - public async validateApiKey(apiKey: string): Promise { - const apiKeyDb = await this.apiKeyService.findOne(apiKey); + await this.usersService.updateLastLoginToNow(user); - if (!apiKeyDb) { - this.logger.warn(`Login with API key: Key not found`); - } + return user; + } - return apiKeyDb; - } + public async issueJwt(email: string, id: number, isKombit?: boolean): Promise { + const payload: JwtPayloadDto = { username: email, sub: id, isKombit: isKombit }; + return { + accessToken: this.jwtService.sign(payload), + }; + } - private getXmlParser() { - const parserConfig = { - explicitRoot: true, - explicitCharkey: true, - tagNameProcessors: [xml2js.processors.stripPrefix], - }; - const parser = new xml2js.Parser(parserConfig); - return parser; - } + public async validateApiKey(apiKey: string): Promise { + const apiKeyDb = await this.apiKeyService.findOne(apiKey); - private async getPrivilegesIntermediate(profile: Profile): Promise { - const xml = profile.getAssertionXml(); - const parser = this.getXmlParser(); - - return await parser - .parseStringPromise(xml) - .then((doc: XMLOutput) => { - const assertion = doc["Assertion"]; - const privilegesNode = assertion["AttributeStatement"][0]["Attribute"].find((x: XMLOutput) => { - return x["$"]["Name"] == "dk:gov:saml:attribute:Privileges_intermediate"; - }); - const base64Xml = privilegesNode["AttributeValue"][0]["_"]; - - return base64Xml; - }) - .catch((err: any) => { - this.logger.error("Err: " + err); - this.logger.error("Could not load attribute in SAML response"); - return null; - }); + if (!apiKeyDb) { + this.logger.warn(`Login with API key: Key not found`); } - private async isAllowed(privilegesBase64: string): Promise { - const decodedXml = Buffer.from(privilegesBase64, "base64").toString("binary"); - - const parser = this.getXmlParser(); - - return await parser - .parseStringPromise(decodedXml) - .then((doc: XMLOutput) => { - return doc["PrivilegeList"]["PrivilegeGroup"].some((privilegeGroups: XMLOutput) => - privilegeGroups["Privilege"].some( - (privileges: XMLOutput) => privileges["_"].indexOf(this.KOMBIT_ROLE_URI) > -1 - ) - ); - }) - .catch((err: any) => { - this.logger.error("Could not find privileges in result"); - return false; - }); - } + return apiKeyDb; + } + + private getXmlParser() { + const parserConfig = { + explicitRoot: true, + explicitCharkey: true, + tagNameProcessors: [xml2js.processors.stripPrefix], + }; + const parser = new xml2js.Parser(parserConfig); + return parser; + } + + private async getPrivilegesIntermediate(profile: Profile): Promise { + const xml = profile.getAssertionXml(); + const parser = this.getXmlParser(); + + return await parser + .parseStringPromise(xml) + .then((doc: XMLOutput) => { + const assertion = doc["Assertion"]; + const privilegesNode = assertion["AttributeStatement"][0]["Attribute"].find((x: XMLOutput) => { + return x["$"]["Name"] == "dk:gov:saml:attribute:Privileges_intermediate"; + }); + const base64Xml = privilegesNode["AttributeValue"][0]["_"]; + + return base64Xml; + }) + .catch((err: any) => { + this.logger.error("Err: " + err); + this.logger.error("Could not load attribute in SAML response"); + return null; + }); + } + + private async isAllowed(privilegesBase64: string): Promise { + const decodedXml = Buffer.from(privilegesBase64, "base64").toString("binary"); + + const parser = this.getXmlParser(); + + return await parser + .parseStringPromise(decodedXml) + .then((doc: XMLOutput) => { + return doc["PrivilegeList"]["PrivilegeGroup"].some((privilegeGroups: XMLOutput) => + privilegeGroups["Privilege"].some( + (privileges: XMLOutput) => privileges["_"].indexOf(this.KOMBIT_ROLE_URI) > -1 + ) + ); + }) + .catch((err: any) => { + this.logger.error("Could not find privileges in result"); + return false; + }); + } } diff --git a/src/services/user-management/organization.service.ts b/src/services/user-management/organization.service.ts index ac231947..e8f86bce 100644 --- a/src/services/user-management/organization.service.ts +++ b/src/services/user-management/organization.service.ts @@ -5,8 +5,8 @@ import { In, Repository } from "typeorm"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; import { - ListAllMinimalOrganizationsResponseDto, - ListAllOrganizationsResponseDto, + ListAllMinimalOrganizationsResponseDto, + ListAllOrganizationsResponseDto, } from "@dto/list-all-organizations-response.dto"; import { CreateOrganizationDto } from "@dto/user-management/create-organization.dto"; import { UpdateOrganizationDto } from "@dto/user-management/update-organization.dto"; @@ -21,229 +21,225 @@ import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; @Injectable() export class OrganizationService { - constructor( - @InjectRepository(Organization) - private organizationRepository: Repository, - @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService, - @Inject(forwardRef(() => UserService)) - private userService: UserService - ) {} - - private readonly logger = new Logger(OrganizationService.name, { timestamp: true }); - - async create(dto: CreateOrganizationDto, userId: number): Promise { - const organization = new Organization(); - organization.name = dto.name; - organization.createdBy = userId; - organization.updatedBy = userId; - - try { - const res = await this.organizationRepository.save(organization); - - await this.permissionService.createDefaultPermissions(res, userId); - - return res; - } catch (err) { - throw new BadRequestException(ErrorCodes.OrganizationAlreadyExists); + constructor( + @InjectRepository(Organization) + private organizationRepository: Repository, + @Inject(forwardRef(() => PermissionService)) + private permissionService: PermissionService, + @Inject(forwardRef(() => UserService)) + private userService: UserService + ) {} + + private readonly logger = new Logger(OrganizationService.name, { timestamp: true }); + + async create(dto: CreateOrganizationDto, userId: number): Promise { + const organization = new Organization(); + organization.name = dto.name; + organization.createdBy = userId; + organization.updatedBy = userId; + + try { + const res = await this.organizationRepository.save(organization); + + await this.permissionService.createDefaultPermissions(res, userId); + + return res; + } catch (err) { + throw new BadRequestException(ErrorCodes.OrganizationAlreadyExists); + } + } + + async update(id: number, dto: UpdateOrganizationDto, userId: number): Promise { + const org = await this.findByIdWithRelations(id); + org.name = dto.name; + org.updatedBy = userId; + + return await this.organizationRepository.save(org); + } + + async updateOpenDataDkRegistered(id: number, userId: number): Promise { + const org = await this.findByIdWithRelations(id); + org.openDataDkRegistered = true; + org.updatedBy = userId; + return await this.organizationRepository.save(org); + } + + async updateAwaitingUsers(org: Organization, user: User): Promise { + if (!org.awaitingUsers.find(dbUser => dbUser.id === user.id)) { + org.awaitingUsers.push(user); + } + return await this.organizationRepository.save(org); + } + + async rejectAwaitingUser(user: User, organization: Organization): Promise { + if (organization.awaitingUsers.find(dbUser => dbUser.id === user.id)) { + const index = organization.awaitingUsers.findIndex(dbUser => dbUser.id === user.id); + organization.awaitingUsers.splice(index, 1); + await this.userService.sendRejectionMail(user, organization); + return await this.organizationRepository.save(organization); + } + throw new NotFoundException(ErrorCodes.UserDoesNotExistInArray); + } + + async findAll(): Promise { + const [data, count] = await this.organizationRepository.findAndCount({ + relations: ["applications", "permissions"], + }); + + return { + count: count, + data: data, + }; + } + + mapPermissionsToOrganizations(permissions: Permission[]): Organization[] { + const requestedOrganizations: Organization[] = []; + + for (let index = 0; index < permissions.length; index++) { + if ( + requestedOrganizations.find(org => { + return permissions[index].organization.id === org.id; + }) + ) { + } else { + requestedOrganizations.push(permissions[index].organization); + } + } + + requestedOrganizations.forEach(org => { + org.permissions = []; + permissions.forEach(permission => { + if (org.id === permission.organization.id) { + org.permissions.push(permission); } - } - - async update(id: number, dto: UpdateOrganizationDto, userId: number): Promise { - const org = await this.findByIdWithRelations(id); - org.name = dto.name; - org.updatedBy = userId; - - return await this.organizationRepository.save(org); - } - - async updateOpenDataDkRegistered(id: number, userId: number): Promise { - const org = await this.findByIdWithRelations(id); - org.openDataDkRegistered = true; - org.updatedBy = userId; - return await this.organizationRepository.save(org); - } - - async updateAwaitingUsers(org: Organization, user: User): Promise { - if (!org.awaitingUsers.find(dbUser => dbUser.id === user.id)) { - org.awaitingUsers.push(user); - } - return await this.organizationRepository.save(org); - } - - async rejectAwaitingUser(user: User, organization: Organization): Promise { - if (organization.awaitingUsers.find(dbUser => dbUser.id === user.id)) { - const index = organization.awaitingUsers.findIndex(dbUser => dbUser.id === user.id); - organization.awaitingUsers.splice(index, 1); - await this.userService.sendRejectionMail(user, organization); - return await this.organizationRepository.save(organization); - } - throw new NotFoundException(ErrorCodes.UserDoesNotExistInArray); - } - - async findAll(): Promise { - const [data, count] = await this.organizationRepository.findAndCount({ - relations: ["applications", "permissions"], - }); - - return { - count: count, - data: data, - }; - } - - mapPermissionsToOrganizations(permissions: Permission[]): Organization[] { - const requestedOrganizations: Organization[] = []; - - for (let index = 0; index < permissions.length; index++) { - if ( - requestedOrganizations.find(org => { - return permissions[index].organization.id === org.id; - }) - ) { - } else { - requestedOrganizations.push(permissions[index].organization); - } - } - - requestedOrganizations.forEach(org => { - org.permissions = []; - permissions.forEach(permission => { - if (org.id === permission.organization.id) { - org.permissions.push(permission); - } - }); - }); - permissions.forEach(permission => { - permission.organization = null; - }); - - return requestedOrganizations; - } - - mapPermissionsToOneOrganization(permissions: Permission[]): Organization { - const org: Organization = new Organization(); - - permissions.map(permission => { - org.id = permission.organization.id; - org.name = permission.organization.name; - }); - - org.permissions = []; - permissions.forEach(permission => { - if (org.id === permission.organization.id) { - org.permissions.push(permission); - } - }); - permissions.forEach(permission => { - permission.organization = null; - }); - return org; - } - - async findAllPaginated(query?: ListAllEntitiesDto): Promise { - const sorting: { [id: string]: string | number } = this.getSorting(query); - - const [data, count] = await this.organizationRepository.findAndCount({ - relations: ["applications"], - take: query?.limit ? query.limit : 100, - skip: query?.offset ? query.offset : 0, - order: sorting, - }); - - return { - count: count, - data: data, - }; - } - - async findAllMinimal(): Promise { - - const [data, count] = await this.organizationRepository.findAndCount({ - select: ["id", "name"], - }); - - return { - count: count, - data: data, - }; - } - - async findAllInOrganizationList( - allowedOrganizations: number[], - query?: ListAllEntitiesDto - ): Promise { - const sorting: { [id: string]: string | number } = this.getSorting(query); - if (allowedOrganizations.length === 0) { - return { count: 0, data: [] }; - } - const [data, count] = await this.organizationRepository.findAndCount({ - where: { id: In(allowedOrganizations) }, - relations: ["applications"], - take: +query.limit, - skip: +query.offset, - order: sorting, - }); - - return { - count: count, - data: data, - }; - } - - private getSorting(query: ListAllEntitiesDto) { - const sorting: { [id: string]: string | number } = {}; - if ( - query?.orderOn != null && - (query.orderOn == "id" || query.orderOn == "name" || query.orderOn == "lastLogin") - ) { - sorting[query.orderOn] = query.sort.toLocaleUpperCase(); - } else { - sorting["id"] = "ASC"; - } - return sorting; - } - - async findById(organizationId: number): Promise { - return await this.organizationRepository.findOneByOrFail({ id: organizationId }); - } - - async findByIdWithRelations(organizationId: number): Promise { - return await this.organizationRepository.findOneOrFail({ - where: { id: organizationId }, - relations: ["permissions"], - loadRelationIds: { - relations: ["applications.iotDevices", "createdBy", "updatedBy"], - }, - }); - } - async findByIdWithUsers(organizationId: number): Promise { - return await this.organizationRepository.findOneOrFail({ - where: { id: organizationId }, - relations: ["awaitingUsers"], - }); - } - - async findByIdWithPermissions(organizationId: number): Promise { - return await this.organizationRepository.findOneOrFail({ - where: { id: organizationId }, - relations: ["permissions"], - }); - } - - findByPermissionIds(permissionIds: number[]): Promise { - return this.organizationRepository - .createQueryBuilder("organization") - .innerJoin("organization.permissions", "perm") - .where("perm.id IN (:...permissionIds)", { permissionIds }) - .getMany(); - } - - async delete(id: number): Promise { - const res = await this.organizationRepository.delete(id); - if (res.affected == 0) { - throw new NotFoundException(); - } - return new DeleteResponseDto(res.affected); - } + }); + }); + permissions.forEach(permission => { + permission.organization = null; + }); + + return requestedOrganizations; + } + + mapPermissionsToOneOrganization(permissions: Permission[]): Organization { + const org: Organization = new Organization(); + + permissions.map(permission => { + org.id = permission.organization.id; + org.name = permission.organization.name; + }); + + org.permissions = []; + permissions.forEach(permission => { + if (org.id === permission.organization.id) { + org.permissions.push(permission); + } + }); + permissions.forEach(permission => { + permission.organization = null; + }); + return org; + } + + async findAllPaginated(query?: ListAllEntitiesDto): Promise { + const sorting: { [id: string]: string | number } = this.getSorting(query); + + const [data, count] = await this.organizationRepository.findAndCount({ + relations: ["applications"], + take: query?.limit ? query.limit : 100, + skip: query?.offset ? query.offset : 0, + order: sorting, + }); + + return { + count: count, + data: data, + }; + } + + async findAllMinimal(): Promise { + const [data, count] = await this.organizationRepository.findAndCount({ + select: ["id", "name"], + }); + + return { + count: count, + data: data, + }; + } + + async findAllInOrganizationList( + allowedOrganizations: number[], + query?: ListAllEntitiesDto + ): Promise { + const sorting: { [id: string]: string | number } = this.getSorting(query); + if (allowedOrganizations.length === 0) { + return { count: 0, data: [] }; + } + const [data, count] = await this.organizationRepository.findAndCount({ + where: { id: In(allowedOrganizations) }, + relations: ["applications"], + take: +query.limit, + skip: +query.offset, + order: sorting, + }); + + return { + count: count, + data: data, + }; + } + + private getSorting(query: ListAllEntitiesDto) { + const sorting: { [id: string]: string | number } = {}; + if (query?.orderOn != null && (query.orderOn == "id" || query.orderOn == "name" || query.orderOn == "lastLogin")) { + sorting[query.orderOn] = query.sort.toLocaleUpperCase(); + } else { + sorting["id"] = "ASC"; + } + return sorting; + } + + async findById(organizationId: number): Promise { + return await this.organizationRepository.findOneByOrFail({ id: organizationId }); + } + + async findByIdWithRelations(organizationId: number): Promise { + return await this.organizationRepository.findOneOrFail({ + where: { id: organizationId }, + relations: ["permissions"], + loadRelationIds: { + relations: ["applications.iotDevices", "createdBy", "updatedBy"], + }, + }); + } + async findByIdWithUsers(organizationId: number): Promise { + return await this.organizationRepository.findOneOrFail({ + where: { id: organizationId }, + relations: ["awaitingUsers"], + }); + } + + async findByIdWithPermissions(organizationId: number): Promise { + return await this.organizationRepository.findOneOrFail({ + where: { id: organizationId }, + relations: ["permissions"], + }); + } + + findByPermissionIds(permissionIds: number[]): Promise { + return this.organizationRepository + .createQueryBuilder("organization") + .innerJoin("organization.permissions", "perm") + .where("perm.id IN (:...permissionIds)", { permissionIds }) + .getMany(); + } + + async delete(id: number): Promise { + const res = await this.organizationRepository.delete(id); + if (res.affected == 0) { + throw new NotFoundException(); + } + return new DeleteResponseDto(res.affected); + } } diff --git a/src/services/user-management/permission.service.ts b/src/services/user-management/permission.service.ts index e10f76db..e214d9f3 100644 --- a/src/services/user-management/permission.service.ts +++ b/src/services/user-management/permission.service.ts @@ -28,480 +28,407 @@ import { UserService } from "./user.service"; @Injectable() export class PermissionService { - constructor( - @InjectRepository(Permission) - private permissionRepository: Repository, - @Inject(forwardRef(() => OrganizationService)) - private organizationService: OrganizationService, - @Inject(forwardRef(() => UserService)) - private userService: UserService, - @Inject(forwardRef(() => ApplicationService)) - private applicationService: ApplicationService - ) {} - - async createDefaultPermissions( - org: Organization, - userId: number - ): Promise { - const { - readPermission, - orgApplicationAdminPermission, - orgAllAdminPermission, - } = this.instantiateDefaultPermissions(org, userId); - - // Use the manager since otherwise, we'd need a repository for each of them - const res = await this.permissionRepository.save([ - readPermission, - orgApplicationAdminPermission, - orgAllAdminPermission, - ]); - res.forEach(val => - AuditLog.success(ActionType.CREATE, Permission.name, userId, val.id, val.name) - ); - return res; + constructor( + @InjectRepository(Permission) + private permissionRepository: Repository, + @Inject(forwardRef(() => OrganizationService)) + private organizationService: OrganizationService, + @Inject(forwardRef(() => UserService)) + private userService: UserService, + @Inject(forwardRef(() => ApplicationService)) + private applicationService: ApplicationService + ) {} + + async createDefaultPermissions(org: Organization, userId: number): Promise { + const { readPermission, orgApplicationAdminPermission, orgAllAdminPermission } = this.instantiateDefaultPermissions( + org, + userId + ); + + // Use the manager since otherwise, we'd need a repository for each of them + const res = await this.permissionRepository.save([ + readPermission, + orgApplicationAdminPermission, + orgAllAdminPermission, + ]); + res.forEach(val => AuditLog.success(ActionType.CREATE, Permission.name, userId, val.id, val.name)); + return res; + } + + private instantiateDefaultPermissions(org: Organization, userId: number) { + const nameSuffixSeparator = " - "; + const allAdminSuffix = `${nameSuffixSeparator}${Translations.OrganizationAdmin}`; + const organizationApplicationAdminSuffix = `${nameSuffixSeparator}${Translations.ApplicationAdmin}`; + const readSuffix = `${nameSuffixSeparator}${Translations.ReadLevel}`; + + const readPermission = PermissionCreator.createRead(org.name + readSuffix, org, true); + const orgApplicationAdminPermission = PermissionCreator.createApplicationAdmin( + org.name + organizationApplicationAdminSuffix, + org, + true + ); + orgApplicationAdminPermission.type.push({ + type: PermissionType.Read, + } as PermissionTypeEntity); + + const orgAllAdminPermission = PermissionCreator.createUserAdmin(org.name + allAdminSuffix, org); + orgAllAdminPermission.type.push({ + type: PermissionType.OrganizationApplicationAdmin, + } as PermissionTypeEntity); + orgAllAdminPermission.type.push({ + type: PermissionType.OrganizationGatewayAdmin, + } as PermissionTypeEntity); + orgAllAdminPermission.type.push({ + type: PermissionType.Read, + } as PermissionTypeEntity); + + this.setUserIdOnPermissions(readPermission, userId); + this.setUserIdOnPermissions(orgApplicationAdminPermission, userId); + this.setUserIdOnPermissions(orgAllAdminPermission, userId); + + readPermission.createdBy = userId; + readPermission.updatedBy = userId; + orgApplicationAdminPermission.createdBy = userId; + orgApplicationAdminPermission.updatedBy = userId; + orgAllAdminPermission.createdBy = userId; + orgAllAdminPermission.updatedBy = userId; + return { readPermission, orgApplicationAdminPermission, orgAllAdminPermission }; + } + + private setUserIdOnPermissions(permission: Permission, userId: number) { + permission.type.forEach(type => { + type.createdBy = userId; + type.updatedBy = userId; + }); + } + + async findOrCreateGlobalAdminPermission(): Promise { + // Use query builder since the other syntax doesn't support one-to-many for property querying + const globalAdmin = await this.permissionRepository + .createQueryBuilder("permission") + .where(" type.type = :permType", { + permType: PermissionType.GlobalAdmin, + }) + .leftJoin("permission.type", "type") + .getOne(); + + if (globalAdmin) { + return globalAdmin; } - private instantiateDefaultPermissions(org: Organization, userId: number) { - const nameSuffixSeparator = " - "; - const allAdminSuffix = `${nameSuffixSeparator}${Translations.OrganizationAdmin}`; - const organizationApplicationAdminSuffix = `${nameSuffixSeparator}${Translations.ApplicationAdmin}`; - const readSuffix = `${nameSuffixSeparator}${Translations.ReadLevel}`; - - const readPermission = PermissionCreator.createRead( - org.name + readSuffix, - org, - true - ); - const orgApplicationAdminPermission = PermissionCreator.createApplicationAdmin( - org.name + organizationApplicationAdminSuffix, - org, - true - ); - orgApplicationAdminPermission.type.push({ - type: PermissionType.Read, - } as PermissionTypeEntity); - - const orgAllAdminPermission = PermissionCreator.createUserAdmin( - org.name + allAdminSuffix, - org - ); - orgAllAdminPermission.type.push({ - type: PermissionType.OrganizationApplicationAdmin, - } as PermissionTypeEntity); - orgAllAdminPermission.type.push({ - type: PermissionType.OrganizationGatewayAdmin, - } as PermissionTypeEntity); - orgAllAdminPermission.type.push({ - type: PermissionType.Read, - } as PermissionTypeEntity); - - this.setUserIdOnPermissions(readPermission, userId); - this.setUserIdOnPermissions(orgApplicationAdminPermission, userId); - this.setUserIdOnPermissions(orgAllAdminPermission, userId); - - readPermission.createdBy = userId; - readPermission.updatedBy = userId; - orgApplicationAdminPermission.createdBy = userId; - orgApplicationAdminPermission.updatedBy = userId; - orgAllAdminPermission.createdBy = userId; - orgAllAdminPermission.updatedBy = userId; - return { readPermission, orgApplicationAdminPermission, orgAllAdminPermission }; - } - - private setUserIdOnPermissions(permission: Permission, userId: number) { - permission.type.forEach(type => { - type.createdBy = userId; - type.updatedBy = userId; - }); - } - - async findOrCreateGlobalAdminPermission(): Promise { - // Use query builder since the other syntax doesn't support one-to-many for property querying - const globalAdmin = await this.permissionRepository - .createQueryBuilder("permission") - .where(" type.type = :permType", { - permType: PermissionType.GlobalAdmin, - }) - .leftJoin("permission.type", "type") - .getOne(); - - if (globalAdmin) { - return globalAdmin; - } - - return await this.permissionRepository.save( - PermissionCreator.createGlobalAdmin() - ); - } - - async createNewPermission( - dto: CreatePermissionDto, - userId: number - ): Promise { - const org: Organization = await this.organizationService.findById( - dto.organizationId - ); - - const permission = PermissionCreator.createByTypes( - dto.name, - dto.levels.map(level => level.type), - org, - dto.automaticallyAddNewApplications - ); - permission.type.forEach(type => { - type.createdBy = userId; - type.updatedBy = userId; - }); - - await this.mapToPermission(permission, dto); - permission.createdBy = userId; - permission.updatedBy = userId; - - return await this.permissionRepository.save(permission); - } - - async autoAddPermissionsToApplication(app: Application): Promise { - // Use query builder since the other syntax doesn't support one-to-many for property querying - const permissionsInOrganisation = await this.permissionRepository - .createQueryBuilder("permission") - .where( - "permission.organization.id = :orgId" + - " AND type.type IN (:...permType)" + - ` AND "${nameof( - "automaticallyAddNewApplications" - )}" = True`, - { - orgId: app.belongsTo.id, - permType: [ - PermissionType.OrganizationApplicationAdmin, - PermissionType.Read, - ], - } - ) - .leftJoinAndSelect("permission.applications", "app") - .leftJoin("permission.type", "type") - .getMany(); - - await Promise.all( - permissionsInOrganisation.map(async p => { - p.applications.push(app); - await this.permissionRepository.save(p); - }) - ); - } - - async addUsersToPermission(permission: Permission, users: User[]): Promise { - users.forEach(x => { - x.permissions = _.union(x.permissions, [permission]); - }); - } - async removeUserFromPermission(permission: Permission, user: User): Promise { - user.permissions = user.permissions.filter(x => x.id != permission.id); - } - - async findManyWithRelations(organizationIds: number[]): Promise { - const perm = await this.permissionRepository.find({ - where: { organization: { id: In(organizationIds) } }, - relations: ["organization", "users", "type"], - }); - - return perm; - } - - async findOneWithRelations(organizationId: number): Promise { - const perm = await this.permissionRepository.find({ - where: { organization: { id: organizationId } }, - relations: ["organization", "users", "type"], - }); - - return perm; - } - - async updatePermission( - id: number, - dto: UpdatePermissionDto, - userId: number - ): Promise { - const permission = await this.permissionRepository.findOne({ - where: { id }, - relations: ["organization", "users", "applications", "type"], - }); - - permission.name = dto.name; - - await this.mapToPermission(permission, dto); - permission.updatedBy = userId; - - const savedPermission = await this.permissionRepository.save(permission); - - return savedPermission; - } - - private async mapToPermission( - permission: Permission, - dto: UpdatePermissionDto - ): Promise { - if (isOrganizationApplicationPermission(permission)) { - permission.applications = await this.applicationService.findManyByIds( - dto.applicationIds - ); - - permission.automaticallyAddNewApplications = - dto.automaticallyAddNewApplications; - } - if (dto?.userIds?.length >= 0) { - permission.users = await this.userService.findManyUsersByIds(dto.userIds); - } - } - - async deletePermission(id: number): Promise { - const res = await this.permissionRepository.delete(id); - return new DeleteResponseDto(res.affected); - } - - async getAllPermissions( - query?: ListAllPermissionsDto, - orgs?: number[] - ): Promise { - const orderBy = this.getSorting(query); - const order: "DESC" | "ASC" = - query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC"; - let qb: SelectQueryBuilder = this.permissionRepository - .createQueryBuilder("permission") - .leftJoinAndSelect("permission.organization", "org") - .leftJoinAndSelect("permission.users", "user") - .leftJoinAndSelect("permission.type", "permission_type") - .take(query?.limit ? +query.limit : 100) - .skip(query?.offset ? +query.offset : 0) - .orderBy(orderBy, order); - - if (query?.userId !== undefined && query.userId !== "undefined") { - qb = qb.andWhere("user.id = :userId", { userId: +query.userId }); - } - if (orgs) { - qb = qb.andWhere({ organization: In(orgs) }); - } else if ( - query?.organisationId !== undefined && - query.organisationId !== "undefined" - ) { - qb = qb.andWhere("org.id = :orgId", { orgId: +query.organisationId }); - } - - const [data, count] = await qb.getManyAndCount(); - - return { - data: data, - count: count, - }; - } - - private getSorting(query: ListAllPermissionsDto | undefined) { - let orderBy = `permission.id`; - if ( - query && - query?.orderOn !== null && - (query.orderOn === "id" || - query.orderOn === "name" || - query.orderOn === "type" || - query.orderOn === "organisations") - ) { - if (query.orderOn === "organisations") { - orderBy = "org.name"; - } else if (query.orderOn === "type") { - orderBy = `permission_type.${query.orderOn}`; - } else { - orderBy = `permission.${query.orderOn}`; - } + return await this.permissionRepository.save(PermissionCreator.createGlobalAdmin()); + } + + async createNewPermission(dto: CreatePermissionDto, userId: number): Promise { + const org: Organization = await this.organizationService.findById(dto.organizationId); + + const permission = PermissionCreator.createByTypes( + dto.name, + dto.levels.map(level => level.type), + org, + dto.automaticallyAddNewApplications + ); + permission.type.forEach(type => { + type.createdBy = userId; + type.updatedBy = userId; + }); + + await this.mapToPermission(permission, dto); + permission.createdBy = userId; + permission.updatedBy = userId; + + return await this.permissionRepository.save(permission); + } + + async autoAddPermissionsToApplication(app: Application): Promise { + // Use query builder since the other syntax doesn't support one-to-many for property querying + const permissionsInOrganisation = await this.permissionRepository + .createQueryBuilder("permission") + .where( + "permission.organization.id = :orgId" + + " AND type.type IN (:...permType)" + + ` AND "${nameof("automaticallyAddNewApplications")}" = True`, + { + orgId: app.belongsTo.id, + permType: [PermissionType.OrganizationApplicationAdmin, PermissionType.Read], } - return orderBy; - } - - async getAllPermissionsInOrganizations( - orgs: number[], - query?: ListAllEntitiesDto - ): Promise { - return this.getAllPermissions(query, orgs); - } - - async getPermission(id: number): Promise { - return await this.permissionRepository.findOneOrFail({ - where: { id }, - relations: ["organization", "users", "applications", "type"], - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - } - - getGlobalPermission(): Promise { - return this.permissionRepository - .createQueryBuilder("permission") - .where(" type.type = :permType", { - permType: PermissionType.GlobalAdmin, - }) - .leftJoin("permission.type", "type") - .leftJoinAndSelect("permission.users", "users") - .getOneOrFail(); - } - - buildPermissionsQuery(): SelectQueryBuilder { - return this.permissionRepository - .createQueryBuilder("permission") - .leftJoinAndSelect( - "application_permissions_permission", - "application_permission", - '"permission"."id" = "application_permission"."permissionId"' - ) - .leftJoinAndSelect( - "application", - "application", - '"application"."id"="application_permission"."applicationId" ' - ) - .leftJoinAndSelect( - "permission_type", - "permission_type", - '"permission_type"."permissionId"="permission"."id"' - ) - .select([ - "permission_type.type as permission_type_type", - "permission.organization as organization_id", - "application.id as application_id", - ]); + ) + .leftJoinAndSelect("permission.applications", "app") + .leftJoin("permission.type", "type") + .getMany(); + + await Promise.all( + permissionsInOrganisation.map(async p => { + p.applications.push(app); + await this.permissionRepository.save(p); + }) + ); + } + + async addUsersToPermission(permission: Permission, users: User[]): Promise { + users.forEach(x => { + x.permissions = _.union(x.permissions, [permission]); + }); + } + async removeUserFromPermission(permission: Permission, user: User): Promise { + user.permissions = user.permissions.filter(x => x.id != permission.id); + } + + async findManyWithRelations(organizationIds: number[]): Promise { + const perm = await this.permissionRepository.find({ + where: { organization: { id: In(organizationIds) } }, + relations: ["organization", "users", "type"], + }); + + return perm; + } + + async findOneWithRelations(organizationId: number): Promise { + const perm = await this.permissionRepository.find({ + where: { organization: { id: organizationId } }, + relations: ["organization", "users", "type"], + }); + + return perm; + } + + async updatePermission(id: number, dto: UpdatePermissionDto, userId: number): Promise { + const permission = await this.permissionRepository.findOne({ + where: { id }, + relations: ["organization", "users", "applications", "type"], + }); + + permission.name = dto.name; + + await this.mapToPermission(permission, dto); + permission.updatedBy = userId; + + const savedPermission = await this.permissionRepository.save(permission); + + return savedPermission; + } + + private async mapToPermission(permission: Permission, dto: UpdatePermissionDto): Promise { + if (isOrganizationApplicationPermission(permission)) { + permission.applications = await this.applicationService.findManyByIds(dto.applicationIds); + + permission.automaticallyAddNewApplications = dto.automaticallyAddNewApplications; } - - async findPermissionsForUser(userId: number): Promise { - return await this.buildPermissionsQuery() - .leftJoin("permission.users", "user") - .where("user.id = :id", { id: userId }) - .getRawMany(); + if (dto?.userIds?.length >= 0) { + permission.users = await this.userService.findManyUsersByIds(dto.userIds); } - - async findPermissionsForApiKey(apiKeyId: number): Promise { - return await this.buildPermissionsQuery() - .leftJoin("permission.apiKeys", "apiKey") - .where("apiKey.id = :id", { id: apiKeyId }) - .getRawMany(); + } + + async deletePermission(id: number): Promise { + const res = await this.permissionRepository.delete(id); + return new DeleteResponseDto(res.affected); + } + + async getAllPermissions(query?: ListAllPermissionsDto, orgs?: number[]): Promise { + const orderBy = this.getSorting(query); + const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() === "DESC" ? "DESC" : "ASC"; + let qb: SelectQueryBuilder = this.permissionRepository + .createQueryBuilder("permission") + .leftJoinAndSelect("permission.organization", "org") + .leftJoinAndSelect("permission.users", "user") + .leftJoinAndSelect("permission.type", "permission_type") + .take(query?.limit ? +query.limit : 100) + .skip(query?.offset ? +query.offset : 0) + .orderBy(orderBy, order); + + if (query?.userId !== undefined && query.userId !== "undefined") { + qb = qb.andWhere("user.id = :userId", { userId: +query.userId }); } - - async findPermissionsForOrgAdminWithApplications( - userId: number - ): Promise { - return await this.buildPermissionsWithApplicationsQuery() - .leftJoin("permission.users", "user") - .where("permission_type.type = :permType AND user.id = :id", { - permType: PermissionType.OrganizationUserAdmin, - id: userId, - }) - .getRawMany(); + if (orgs) { + qb = qb.andWhere({ organization: In(orgs) }); + } else if (query?.organisationId !== undefined && query.organisationId !== "undefined") { + qb = qb.andWhere("org.id = :orgId", { orgId: +query.organisationId }); } - buildPermissionsWithApplicationsQuery(): SelectQueryBuilder { - return this.permissionRepository - .createQueryBuilder("permission") - .leftJoinAndSelect("permission.organization", "organization") - .leftJoinAndSelect("organization.applications", "application") - .leftJoinAndSelect("permission.type", "permission_type") - .select([ - "permission_type.type as permission_type_type", - "permission.organization as organization_id", - "application.id as application_id", - ]); + const [data, count] = await qb.getManyAndCount(); + + return { + data: data, + count: count, + }; + } + + private getSorting(query: ListAllPermissionsDto | undefined) { + let orderBy = `permission.id`; + if ( + query && + query?.orderOn !== null && + (query.orderOn === "id" || + query.orderOn === "name" || + query.orderOn === "type" || + query.orderOn === "organisations") + ) { + if (query.orderOn === "organisations") { + orderBy = "org.name"; + } else if (query.orderOn === "type") { + orderBy = `permission_type.${query.orderOn}`; + } else { + orderBy = `permission.${query.orderOn}`; + } } - - async findPermissionsForApiKeyOrgAdminWithApplications( - apiKeyId: number - ): Promise { - return await this.buildPermissionsWithApplicationsQuery() - .leftJoin("permission.apiKeys", "apiKey") - .leftJoin("permission.type", "type") - .where("type.type = :permType AND apiKey.id = :id", { - permType: PermissionType.OrganizationUserAdmin, - id: apiKeyId, - }) - .getRawMany(); + return orderBy; + } + + async getAllPermissionsInOrganizations( + orgs: number[], + query?: ListAllEntitiesDto + ): Promise { + return this.getAllPermissions(query, orgs); + } + + async getPermission(id: number): Promise { + return await this.permissionRepository.findOneOrFail({ + where: { id }, + relations: ["organization", "users", "applications", "type"], + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } + + getGlobalPermission(): Promise { + return this.permissionRepository + .createQueryBuilder("permission") + .where(" type.type = :permType", { + permType: PermissionType.GlobalAdmin, + }) + .leftJoin("permission.type", "type") + .leftJoinAndSelect("permission.users", "users") + .getOneOrFail(); + } + + buildPermissionsQuery(): SelectQueryBuilder { + return this.permissionRepository + .createQueryBuilder("permission") + .leftJoinAndSelect( + "application_permissions_permission", + "application_permission", + '"permission"."id" = "application_permission"."permissionId"' + ) + .leftJoinAndSelect("application", "application", '"application"."id"="application_permission"."applicationId" ') + .leftJoinAndSelect("permission_type", "permission_type", '"permission_type"."permissionId"="permission"."id"') + .select([ + "permission_type.type as permission_type_type", + "permission.organization as organization_id", + "application.id as application_id", + ]); + } + + async findPermissionsForUser(userId: number): Promise { + return await this.buildPermissionsQuery() + .leftJoin("permission.users", "user") + .where("user.id = :id", { id: userId }) + .getRawMany(); + } + + async findPermissionsForApiKey(apiKeyId: number): Promise { + return await this.buildPermissionsQuery() + .leftJoin("permission.apiKeys", "apiKey") + .where("apiKey.id = :id", { id: apiKeyId }) + .getRawMany(); + } + + async findPermissionsForOrgAdminWithApplications(userId: number): Promise { + return await this.buildPermissionsWithApplicationsQuery() + .leftJoin("permission.users", "user") + .where("permission_type.type = :permType AND user.id = :id", { + permType: PermissionType.OrganizationUserAdmin, + id: userId, + }) + .getRawMany(); + } + + buildPermissionsWithApplicationsQuery(): SelectQueryBuilder { + return this.permissionRepository + .createQueryBuilder("permission") + .leftJoinAndSelect("permission.organization", "organization") + .leftJoinAndSelect("organization.applications", "application") + .leftJoinAndSelect("permission.type", "permission_type") + .select([ + "permission_type.type as permission_type_type", + "permission.organization as organization_id", + "application.id as application_id", + ]); + } + + async findPermissionsForApiKeyOrgAdminWithApplications(apiKeyId: number): Promise { + return await this.buildPermissionsWithApplicationsQuery() + .leftJoin("permission.apiKeys", "apiKey") + .leftJoin("permission.type", "type") + .where("type.type = :permType AND apiKey.id = :id", { + permType: PermissionType.OrganizationUserAdmin, + id: apiKeyId, + }) + .getRawMany(); + } + + async findPermissionGroupedByLevelForUser(userId: number): Promise { + let permissions = await this.findPermissionsForUser(userId); + if (this.hasAccessToAllApplicationsInOrganization(permissions)) { + // For organization admins, we need to fetch all applications they have permissions to + const permissionsForOrgAdmin = await this.findPermissionsForOrgAdminWithApplications(userId); + permissions = _.union(permissions, permissionsForOrgAdmin); } - async findPermissionGroupedByLevelForUser(userId: number): Promise { - let permissions = await this.findPermissionsForUser(userId); - if (this.hasAccessToAllApplicationsInOrganization(permissions)) { - // For organization admins, we need to fetch all applications they have permissions to - const permissionsForOrgAdmin = await this.findPermissionsForOrgAdminWithApplications( - userId - ); - permissions = _.union(permissions, permissionsForOrgAdmin); - } - - return this.createUserPermissionsFromPermissions(permissions); - } + return this.createUserPermissionsFromPermissions(permissions); + } - async findPermissionGroupedByLevelForApiKey( - apiKeyId: number - ): Promise { - let permissions = await this.findPermissionsForApiKey(apiKeyId); - if (this.hasAccessToAllApplicationsInOrganization(permissions)) { - // For organization admins, we need to fetch all applications they have permissions to - const permissionsForOrgAdmin = await this.findPermissionsForApiKeyOrgAdminWithApplications( - apiKeyId - ); - permissions = _.union(permissions, permissionsForOrgAdmin); - } - return this.createUserPermissionsFromPermissions(permissions); + async findPermissionGroupedByLevelForApiKey(apiKeyId: number): Promise { + let permissions = await this.findPermissionsForApiKey(apiKeyId); + if (this.hasAccessToAllApplicationsInOrganization(permissions)) { + // For organization admins, we need to fetch all applications they have permissions to + const permissionsForOrgAdmin = await this.findPermissionsForApiKeyOrgAdminWithApplications(apiKeyId); + permissions = _.union(permissions, permissionsForOrgAdmin); } - - private createUserPermissionsFromPermissions( - permissions: PermissionMinimalDto[] - ): UserPermissions { - const res = new UserPermissions(); - - permissions.forEach(p => { - if (p.permission_type_type === PermissionType.GlobalAdmin) { - res.isGlobalAdmin = true; - } else if ( - p.permission_type_type === PermissionType.OrganizationApplicationAdmin - ) { - this.addOrUpdateApplicationIds(res.orgToApplicationAdminPermissions, p); - } else if ( - p.permission_type_type === PermissionType.OrganizationGatewayAdmin - ) { - res.orgToGatewayAdminPermissions.add(p.organization_id); - } else if (p.permission_type_type === PermissionType.OrganizationUserAdmin) { - // A user admin can map applications to permissions, so they should also - // have access to them - this.addOrUpdateApplicationIds(res.orgToUserAdminPermissions, p); - } else if (p.permission_type_type === PermissionType.Read) { - this.addOrUpdateApplicationIds(res.orgToReadPermissions, p); - } - }); - - return res; + return this.createUserPermissionsFromPermissions(permissions); + } + + private createUserPermissionsFromPermissions(permissions: PermissionMinimalDto[]): UserPermissions { + const res = new UserPermissions(); + + permissions.forEach(p => { + if (p.permission_type_type === PermissionType.GlobalAdmin) { + res.isGlobalAdmin = true; + } else if (p.permission_type_type === PermissionType.OrganizationApplicationAdmin) { + this.addOrUpdateApplicationIds(res.orgToApplicationAdminPermissions, p); + } else if (p.permission_type_type === PermissionType.OrganizationGatewayAdmin) { + res.orgToGatewayAdminPermissions.add(p.organization_id); + } else if (p.permission_type_type === PermissionType.OrganizationUserAdmin) { + // A user admin can map applications to permissions, so they should also + // have access to them + this.addOrUpdateApplicationIds(res.orgToUserAdminPermissions, p); + } else if (p.permission_type_type === PermissionType.Read) { + this.addOrUpdateApplicationIds(res.orgToReadPermissions, p); + } + }); + + return res; + } + + async findManyByIds(ids: number[]): Promise { + if (ids == null || ids?.length == 0) { + return []; } + return await this.permissionRepository.findBy({ id: In(ids) }); + } - async findManyByIds(ids: number[]): Promise { - if (ids == null || ids?.length == 0) { - return []; - } - return await this.permissionRepository.findBy({ id: In(ids) }); - } + private hasAccessToAllApplicationsInOrganization(permissions: PermissionMinimalDto[]) { + return permissions.some(x => x.permission_type_type == PermissionType.OrganizationUserAdmin); + } - private hasAccessToAllApplicationsInOrganization( - permissions: PermissionMinimalDto[] - ) { - return permissions.some( - x => x.permission_type_type == PermissionType.OrganizationUserAdmin - ); + private addOrUpdateApplicationIds(permissions: Map, p: PermissionMinimalDto) { + if (!permissions.has(p.organization_id)) { + permissions.set(p.organization_id, []); } - - private addOrUpdateApplicationIds( - permissions: Map, - p: PermissionMinimalDto - ) { - if (!permissions.has(p.organization_id)) { - permissions.set(p.organization_id, []); - } - const applications = permissions.get(p.organization_id); - if (p.application_id != null) { - applications.push(p.application_id); - } - permissions.set(p.organization_id, applications); + const applications = permissions.get(p.organization_id); + if (p.application_id != null) { + applications.push(p.application_id); } + permissions.set(p.organization_id, applications); + } } diff --git a/src/services/user-management/user-bootstrapper.service.ts b/src/services/user-management/user-bootstrapper.service.ts index 03bbbe32..a7fe83db 100644 --- a/src/services/user-management/user-bootstrapper.service.ts +++ b/src/services/user-management/user-bootstrapper.service.ts @@ -3,35 +3,35 @@ import { Inject, Logger, OnApplicationBootstrap } from "@nestjs/common"; import { UserService } from "./user.service"; export class UserBootstrapperService implements OnApplicationBootstrap { - constructor( - @Inject(UserService) - private userService: UserService - ) {} + constructor( + @Inject(UserService) + private userService: UserService + ) {} - private readonly logger = new Logger(UserBootstrapperService.name); + private readonly logger = new Logger(UserBootstrapperService.name); - GLOBAL_ADMIN_EMAIL = "global-admin@os2iot.dk"; - GLOBAL_ADMIN_NAME = "GlobalAdmin"; - GLOBAL_ADMIN_DEFAULT_PASSWORD = "hunter2"; + GLOBAL_ADMIN_EMAIL = "global-admin@os2iot.dk"; + GLOBAL_ADMIN_NAME = "GlobalAdmin"; + GLOBAL_ADMIN_DEFAULT_PASSWORD = "hunter2"; - async onApplicationBootstrap(): Promise { - if (await this.userService.isEmailUsedByAUser(this.GLOBAL_ADMIN_EMAIL)) { - this.logger.debug("GlobalAdmin user already exists. Won't create a new one."); - return; - } - - await this.userService.createUser( - { - email: this.GLOBAL_ADMIN_EMAIL, - name: this.GLOBAL_ADMIN_NAME, - password: this.GLOBAL_ADMIN_DEFAULT_PASSWORD, - active: true, - globalAdmin: true, - }, - null - ); - this.logger.log( - `Created GlobalAdmin user with login - E-mail: '${this.GLOBAL_ADMIN_EMAIL}' - Password: '${this.GLOBAL_ADMIN_DEFAULT_PASSWORD}'` - ); + async onApplicationBootstrap(): Promise { + if (await this.userService.isEmailUsedByAUser(this.GLOBAL_ADMIN_EMAIL)) { + this.logger.debug("GlobalAdmin user already exists. Won't create a new one."); + return; } + + await this.userService.createUser( + { + email: this.GLOBAL_ADMIN_EMAIL, + name: this.GLOBAL_ADMIN_NAME, + password: this.GLOBAL_ADMIN_DEFAULT_PASSWORD, + active: true, + globalAdmin: true, + }, + null + ); + this.logger.log( + `Created GlobalAdmin user with login - E-mail: '${this.GLOBAL_ADMIN_EMAIL}' - Password: '${this.GLOBAL_ADMIN_DEFAULT_PASSWORD}'` + ); + } } diff --git a/src/services/user-management/user.service.ts b/src/services/user-management/user.service.ts index 3f3123b3..06b3a546 100644 --- a/src/services/user-management/user.service.ts +++ b/src/services/user-management/user.service.ts @@ -25,428 +25,421 @@ import { OS2IoTMail } from "@services/os2iot-mail.service"; @Injectable() export class UserService { - constructor( - @InjectRepository(User) - private userRepository: Repository, - @Inject(forwardRef(() => PermissionService)) - private permissionService: PermissionService, - private configService: ConfigService, - private oS2IoTMail: OS2IoTMail - ) {} - - private readonly logger = new Logger(UserService.name, { timestamp: true }); - - async isEmailUsedByAUser(email: string): Promise { - return ( - (await this.userRepository.count({ - where: { email }, - })) > 0 - ); - } - - async acceptUser(user: User, org: Organization, newUserPermissions: Permission[]): Promise { - user.awaitingConfirmation = false; - - if (user.permissions.find(perms => newUserPermissions.some(newPerm => newPerm.id === perms.id))) { - throw new BadRequestException(ErrorCodes.UserAlreadyInPermission); - } else { - const index = user.requestedOrganizations.findIndex(dbOrg => dbOrg.id === org.id); - user.requestedOrganizations.splice(index, 1); - user.permissions.push(...newUserPermissions); - await this.sendVerificationMail(user, org); - return await this.userRepository.save(user); - } - } - - async findOneUserByEmailWithPassword(email: string): Promise { - return await this.userRepository.findOne({ - where: { email }, - select: [ - "id", - "name", - "email", - "active", - "passwordHash", // This is required since passwordHash normally is hidden. - "lastLogin", - ], - }); - } - - async findOne(id: number, getExtendedInformation: boolean = false): Promise { - const relations = ["permissions", "requestedOrganizations"]; - const extendedBoolean = this.parseBoolean(getExtendedInformation); - if (extendedBoolean) { - relations.push("permissions.organization"); - relations.push("permissions.users"); - relations.push("permissions.type"); - } - - return await this.userRepository.findOne({ - where: { id }, - relations: relations, - loadRelationIds: { - relations: ["createdBy", "updatedBy"], - }, - }); - } - - async exists(id: number): Promise { - return ( - (await this.userRepository.count({ - where: { - id, - }, - })) > 0 - ); - } - - async findOneWithOrganizations(id: number): Promise { - return await this.userRepository.findOne({ - where: { id }, - relations: ["permissions", "permissions.organization", "permissions.type"], - loadRelationIds: { - relations: [`permissions.${nameof("applicationIds")}`], - }, - }); - } - - async findOneByNameId(nameId: string): Promise { - return await this.userRepository.findOneBy({ - nameId: nameId, - }); - } - - async findUserPermissions(id: number): Promise { - return ( - await this.userRepository.findOne({ - where: { id }, - relations: ["permissions"], - }) - ).permissions; - } - - async updateLastLoginToNow(user: User): Promise { - await this.userRepository - .createQueryBuilder() - .update(User) - .set({ lastLogin: new Date() }) - .where("id = :id", { id: user.id }) - .execute(); - } - - async createUser(dto: CreateUserDto, userId: number): Promise { - const user = new User(); - const mappedUser = this.mapDtoToUser(user, dto); - mappedUser.createdBy = userId; - mappedUser.updatedBy = userId; - mappedUser.showWelcomeScreen = true; - - await this.setPasswordHash(mappedUser, dto.password); - - if (dto.globalAdmin) { - const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); - await this.permissionService.addUsersToPermission(globalAdminPermission, [mappedUser]); - } - - return await this.userRepository.save(mappedUser, { reload: true }); - } - - async createUserFromKombit(profile: Profile): Promise { - const user = new User(); - await this.mapKombitLoginProfileToUser(user, profile); - user.showWelcomeScreen = true; - - return await this.userRepository.save(user); - } - - private async mapKombitLoginProfileToUser(user: User, profile: Profile): Promise { - user.active = true; - user.nameId = profile.nameID; - user.name = this.extractNameFromNameIDSAMLAttribute(profile.nameID); - user.active = true; - } - - private extractNameFromNameIDSAMLAttribute(nameId: string): string { - return nameId - ?.split(",") - ?.find(x => x.startsWith("CN=")) - ?.split("=") - ?.pop(); - } - - private async setPasswordHash(mappedUser: User, password: string) { - this.checkPassword(password); - // Hash password with bcrpyt - const salt = await bcrypt.genSalt(10); - mappedUser.passwordHash = await bcrypt.hash(password, salt); - } - - private checkPassword(password: string) { - if (password.length < 6) { - throw new BadRequestException(ErrorCodes.PasswordNotMetRequirements); - } - } - - private mapDtoToUser(user: User, dto: UpdateUserDto): User { - if (user.nameId != null) { - if (dto.name && user.name != dto.name) { - throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); - } - if (dto.password) { - throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); - } - } - - user.name = dto.name; - user.email = dto.email; - user.permissions = user.permissions ? user.permissions : []; - user.active = dto.active; - - return user; - } - - async updateUser(id: number, dto: UpdateUserDto, userId: number): Promise { - const user = await this.userRepository.findOne({ - where: { id }, - relations: ["permissions"], - }); - - const mappedUser = this.mapDtoToUser(user, dto); - mappedUser.updatedBy = userId; - - // If nameId set, this is a Kombit user and we are NOT allowed to set the password - if (dto.password && !mappedUser.nameId) { - this.logger.log(`Changing password for user: id: ${mappedUser.id} - '${mappedUser.email}' ...`); - await this.setPasswordHash(mappedUser, dto.password); - } - - await this.updateGlobalAdminStatusIfNeeded(dto, mappedUser); - - return await this.userRepository.save(mappedUser); - } - - private async updateGlobalAdminStatusIfNeeded(dto: UpdateUserDto, mappedUser: User) { - if (dto.globalAdmin) { - const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); - // Don't do anything if the user already is global admin. - if (!mappedUser.permissions.some(x => x.id == globalAdminPermission.id)) { - await this.permissionService.addUsersToPermission(globalAdminPermission, [mappedUser]); - } - } else { - const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); - await this.permissionService.removeUserFromPermission(globalAdminPermission, mappedUser); - } - } - - async newKombitUser( - dto: CreateNewKombitUserDto, - requestedOrganizations: Organization[], - user: User - ): Promise { - user.email = dto.email; - user.awaitingConfirmation = true; - for (let index = 0; index < requestedOrganizations.length; index++) { - await this.sendOrganizationRequestMail(user, requestedOrganizations[index]); - } - return await this.userRepository.save(user); - } - - async findManyUsersByIds(userIds: number[]): Promise { - return await this.userRepository.findBy({ id: In(userIds) }); - } - - async findAll(query?: ListAllEntitiesDto): Promise { - const sorting: { [id: string]: string | number } = {}; - if ( - query.orderOn != null && - (query.orderOn == "id" || query.orderOn == "name" || query.orderOn == "lastLogin") - ) { - sorting[query.orderOn] = query.sort.toLocaleUpperCase(); - } else { - sorting["id"] = "ASC"; - } - - const [data, count] = await this.userRepository.findAndCount({ - relations: ["permissions", "permissions.type"], - take: +query.limit, - skip: +query.offset, - order: sorting, - where: { - isSystemUser: false, - }, - }); - - return { - data: data.map(x => x as UserResponseDto), - count: count, - }; - } - - async getUsersOnPermissionId(permissionId: number, query: ListAllEntitiesDto): Promise { - const [data, count] = await this.userRepository - .createQueryBuilder("user") - .innerJoin("user.permissions", "p") - .where('"p"."id" = :permissionId', { permissionId: permissionId }) - .take(+query.limit) - .skip(+query.offset) - .getManyAndCount(); - - return { - data: data.map(x => x as UserResponseDto), - count: count, - }; - } - - async getUsersOnOrganization(organizationId: number, query: ListAllEntitiesDto): Promise { - let orderBy = `user.id`; - if (query.orderOn !== null && (query.orderOn === "id" || query.orderOn === "name")) { - orderBy = `user.${query.orderOn}`; - } - const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; - - const [data, count] = await this.userRepository - .createQueryBuilder("user") - .innerJoin("user.permissions", "p") - .where('"p"."organizationId" = :organizationId', { - organizationId: organizationId, - }) - .take(+query.limit) - .skip(+query.offset) - .orderBy(orderBy, order) - .getManyAndCount(); - - return { - data: data.map(x => x as UserResponseDto), - count: count, - }; - } - - async getUsersOnOrganizations( - organizationIds: number[], - query: ListAllEntitiesDto - ): Promise { - let orderBy = `user.id`; - if (query.orderOn !== null && (query.orderOn === "id" || query.orderOn === "name")) { - orderBy = `user.${query.orderOn}`; - } - const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; - - const [data, count] = await this.userRepository - .createQueryBuilder("user") - .innerJoin("user.permissions", "p") - .where("p.organizationId IN (:...organizationIds)", { organizationIds: organizationIds }) - .take(+query.limit) - .skip(+query.offset) - .orderBy(orderBy, order) - .getManyAndCount(); - - return { - data: data.map(x => x as UserResponseDto), - count: count, - }; - } - - async findAllMinimal(): Promise { - const result = await this.userRepository.find({ - select: ["id", "name"], - order: { id: "ASC" }, - }); - return { - users: result, - }; - } - - async sendOrganizationRequestMail(user: User, organization: Organization): Promise { - const emails = await this.getOrgAdminEmails(organization); - - await this.oS2IoTMail.sendMail({ - to: emails, - subject: "Ny ansøgning til din organisation i OS2iot", - html: `

Ny ansøgning til din organisation i OS2iot

+ constructor( + @InjectRepository(User) + private userRepository: Repository, + @Inject(forwardRef(() => PermissionService)) + private permissionService: PermissionService, + private configService: ConfigService, + private oS2IoTMail: OS2IoTMail + ) {} + + private readonly logger = new Logger(UserService.name, { timestamp: true }); + + async isEmailUsedByAUser(email: string): Promise { + return ( + (await this.userRepository.count({ + where: { email }, + })) > 0 + ); + } + + async acceptUser(user: User, org: Organization, newUserPermissions: Permission[]): Promise { + user.awaitingConfirmation = false; + + if (user.permissions.find(perms => newUserPermissions.some(newPerm => newPerm.id === perms.id))) { + throw new BadRequestException(ErrorCodes.UserAlreadyInPermission); + } else { + const index = user.requestedOrganizations.findIndex(dbOrg => dbOrg.id === org.id); + user.requestedOrganizations.splice(index, 1); + user.permissions.push(...newUserPermissions); + await this.sendVerificationMail(user, org); + return await this.userRepository.save(user); + } + } + + async findOneUserByEmailWithPassword(email: string): Promise { + return await this.userRepository.findOne({ + where: { email }, + select: [ + "id", + "name", + "email", + "active", + "passwordHash", // This is required since passwordHash normally is hidden. + "lastLogin", + ], + }); + } + + async findOne(id: number, getExtendedInformation: boolean = false): Promise { + const relations = ["permissions", "requestedOrganizations"]; + const extendedBoolean = this.parseBoolean(getExtendedInformation); + if (extendedBoolean) { + relations.push("permissions.organization"); + relations.push("permissions.users"); + relations.push("permissions.type"); + } + + return await this.userRepository.findOne({ + where: { id }, + relations: relations, + loadRelationIds: { + relations: ["createdBy", "updatedBy"], + }, + }); + } + + async exists(id: number): Promise { + return ( + (await this.userRepository.count({ + where: { + id, + }, + })) > 0 + ); + } + + async findOneWithOrganizations(id: number): Promise { + return await this.userRepository.findOne({ + where: { id }, + relations: ["permissions", "permissions.organization", "permissions.type"], + loadRelationIds: { + relations: [`permissions.${nameof("applicationIds")}`], + }, + }); + } + + async findOneByNameId(nameId: string): Promise { + return await this.userRepository.findOneBy({ + nameId: nameId, + }); + } + + async findUserPermissions(id: number): Promise { + return ( + await this.userRepository.findOne({ + where: { id }, + relations: ["permissions"], + }) + ).permissions; + } + + async updateLastLoginToNow(user: User): Promise { + await this.userRepository + .createQueryBuilder() + .update(User) + .set({ lastLogin: new Date() }) + .where("id = :id", { id: user.id }) + .execute(); + } + + async createUser(dto: CreateUserDto, userId: number): Promise { + const user = new User(); + const mappedUser = this.mapDtoToUser(user, dto); + mappedUser.createdBy = userId; + mappedUser.updatedBy = userId; + mappedUser.showWelcomeScreen = true; + + await this.setPasswordHash(mappedUser, dto.password); + + if (dto.globalAdmin) { + const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); + await this.permissionService.addUsersToPermission(globalAdminPermission, [mappedUser]); + } + + return await this.userRepository.save(mappedUser, { reload: true }); + } + + async createUserFromKombit(profile: Profile): Promise { + const user = new User(); + await this.mapKombitLoginProfileToUser(user, profile); + user.showWelcomeScreen = true; + + return await this.userRepository.save(user); + } + + private async mapKombitLoginProfileToUser(user: User, profile: Profile): Promise { + user.active = true; + user.nameId = profile.nameID; + user.name = this.extractNameFromNameIDSAMLAttribute(profile.nameID); + user.active = true; + } + + private extractNameFromNameIDSAMLAttribute(nameId: string): string { + return nameId + ?.split(",") + ?.find(x => x.startsWith("CN=")) + ?.split("=") + ?.pop(); + } + + private async setPasswordHash(mappedUser: User, password: string) { + this.checkPassword(password); + // Hash password with bcrpyt + const salt = await bcrypt.genSalt(10); + mappedUser.passwordHash = await bcrypt.hash(password, salt); + } + + private checkPassword(password: string) { + if (password.length < 6) { + throw new BadRequestException(ErrorCodes.PasswordNotMetRequirements); + } + } + + private mapDtoToUser(user: User, dto: UpdateUserDto): User { + if (user.nameId != null) { + if (dto.name && user.name != dto.name) { + throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); + } + if (dto.password) { + throw new BadRequestException(ErrorCodes.CannotModifyOnKombitUser); + } + } + + user.name = dto.name; + user.email = dto.email; + user.permissions = user.permissions ? user.permissions : []; + user.active = dto.active; + + return user; + } + + async updateUser(id: number, dto: UpdateUserDto, userId: number): Promise { + const user = await this.userRepository.findOne({ + where: { id }, + relations: ["permissions"], + }); + + const mappedUser = this.mapDtoToUser(user, dto); + mappedUser.updatedBy = userId; + + // If nameId set, this is a Kombit user and we are NOT allowed to set the password + if (dto.password && !mappedUser.nameId) { + this.logger.log(`Changing password for user: id: ${mappedUser.id} - '${mappedUser.email}' ...`); + await this.setPasswordHash(mappedUser, dto.password); + } + + await this.updateGlobalAdminStatusIfNeeded(dto, mappedUser); + + return await this.userRepository.save(mappedUser); + } + + private async updateGlobalAdminStatusIfNeeded(dto: UpdateUserDto, mappedUser: User) { + if (dto.globalAdmin) { + const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); + // Don't do anything if the user already is global admin. + if (!mappedUser.permissions.some(x => x.id == globalAdminPermission.id)) { + await this.permissionService.addUsersToPermission(globalAdminPermission, [mappedUser]); + } + } else { + const globalAdminPermission = await this.permissionService.findOrCreateGlobalAdminPermission(); + await this.permissionService.removeUserFromPermission(globalAdminPermission, mappedUser); + } + } + + async newKombitUser(dto: CreateNewKombitUserDto, requestedOrganizations: Organization[], user: User): Promise { + user.email = dto.email; + user.awaitingConfirmation = true; + for (let index = 0; index < requestedOrganizations.length; index++) { + await this.sendOrganizationRequestMail(user, requestedOrganizations[index]); + } + return await this.userRepository.save(user); + } + + async findManyUsersByIds(userIds: number[]): Promise { + return await this.userRepository.findBy({ id: In(userIds) }); + } + + async findAll(query?: ListAllEntitiesDto): Promise { + const sorting: { [id: string]: string | number } = {}; + if (query.orderOn != null && (query.orderOn == "id" || query.orderOn == "name" || query.orderOn == "lastLogin")) { + sorting[query.orderOn] = query.sort.toLocaleUpperCase(); + } else { + sorting["id"] = "ASC"; + } + + const [data, count] = await this.userRepository.findAndCount({ + relations: ["permissions", "permissions.type"], + take: +query.limit, + skip: +query.offset, + order: sorting, + where: { + isSystemUser: false, + }, + }); + + return { + data: data.map(x => x as UserResponseDto), + count: count, + }; + } + + async getUsersOnPermissionId(permissionId: number, query: ListAllEntitiesDto): Promise { + const [data, count] = await this.userRepository + .createQueryBuilder("user") + .innerJoin("user.permissions", "p") + .where('"p"."id" = :permissionId', { permissionId: permissionId }) + .take(+query.limit) + .skip(+query.offset) + .getManyAndCount(); + + return { + data: data.map(x => x as UserResponseDto), + count: count, + }; + } + + async getUsersOnOrganization(organizationId: number, query: ListAllEntitiesDto): Promise { + let orderBy = `user.id`; + if (query.orderOn !== null && (query.orderOn === "id" || query.orderOn === "name")) { + orderBy = `user.${query.orderOn}`; + } + const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; + + const [data, count] = await this.userRepository + .createQueryBuilder("user") + .innerJoin("user.permissions", "p") + .where('"p"."organizationId" = :organizationId', { + organizationId: organizationId, + }) + .take(+query.limit) + .skip(+query.offset) + .orderBy(orderBy, order) + .getManyAndCount(); + + return { + data: data.map(x => x as UserResponseDto), + count: count, + }; + } + + async getUsersOnOrganizations( + organizationIds: number[], + query: ListAllEntitiesDto + ): Promise { + let orderBy = `user.id`; + if (query.orderOn !== null && (query.orderOn === "id" || query.orderOn === "name")) { + orderBy = `user.${query.orderOn}`; + } + const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; + + const [data, count] = await this.userRepository + .createQueryBuilder("user") + .innerJoin("user.permissions", "p") + .where("p.organizationId IN (:...organizationIds)", { organizationIds: organizationIds }) + .take(+query.limit) + .skip(+query.offset) + .orderBy(orderBy, order) + .getManyAndCount(); + + return { + data: data.map(x => x as UserResponseDto), + count: count, + }; + } + + async findAllMinimal(): Promise { + const result = await this.userRepository.find({ + select: ["id", "name"], + order: { id: "ASC" }, + }); + return { + users: result, + }; + } + + async sendOrganizationRequestMail(user: User, organization: Organization): Promise { + const emails = await this.getOrgAdminEmails(organization); + + await this.oS2IoTMail.sendMail({ + to: emails, + subject: "Ny ansøgning til din organisation i OS2iot", + html: `

Ny ansøgning til din organisation i OS2iot

Klik her for at bekræfte eller afvise brugeren ${user.name} i organisationen ${ - organization.name - } + organization.name + }

Find brugeren under fanebladet "Afventende brugere"

`, // html body + }); + } + + async sendRejectionMail(user: User, organization: Organization): Promise { + await this.oS2IoTMail.sendMail({ + to: user.email, // list of receivers + subject: "Ansøgning i OS2iot afvist", // Subject line + html: `

Din ansøgning om tilknytning til organisationen ${organization.name} i OS2iot er afvist. Kontakt din OS2iot-administrator, hvis du vil vide mere.

`, // html body + }); + } + + async sendVerificationMail(user: User, organization: Organization): Promise { + await this.oS2IoTMail.sendMail({ + to: user.email, // list of receivers + subject: "Ansøgning i OS2iot godkendt", // Subject line + html: `

Din ansøgning om tilknytning til organisationen ${organization.name} i OS2iot er godkendt

`, // html body + }); + } + + async getOrgAdminEmails(organization: Organization): Promise { + const emails: string[] = []; + const globalAdminPermission: Permission = await this.permissionService.getGlobalPermission(); + organization.permissions.forEach(permission => { + if (isPermissionType(permission, PermissionType.OrganizationUserAdmin) && permission.users.length > 0) { + permission.users.forEach(user => { + emails.push(user.email); }); - } - - async sendRejectionMail(user: User, organization: Organization): Promise { - await this.oS2IoTMail.sendMail({ - to: user.email, // list of receivers - subject: "Ansøgning i OS2iot afvist", // Subject line - html: `

Din ansøgning om tilknytning til organisationen ${organization.name} i OS2iot er afvist. Kontakt din OS2iot-administrator, hvis du vil vide mere.

`, // html body - }); - } - - async sendVerificationMail(user: User, organization: Organization): Promise { - await this.oS2IoTMail.sendMail({ - to: user.email, // list of receivers - subject: "Ansøgning i OS2iot godkendt", // Subject line - html: `

Din ansøgning om tilknytning til organisationen ${organization.name} i OS2iot er godkendt

`, // html body - }); - } - - async getOrgAdminEmails(organization: Organization): Promise { - const emails: string[] = []; - const globalAdminPermission: Permission = await this.permissionService.getGlobalPermission(); - organization.permissions.forEach(permission => { - if (isPermissionType(permission, PermissionType.OrganizationUserAdmin) && permission.users.length > 0) { - permission.users.forEach(user => { - emails.push(user.email); - }); - } - }); - if (emails.length === 0) { - globalAdminPermission.users.forEach(user => { - emails.push(user.email); - }); - } - - return emails; - } - - async getAwaitingUsers(query?: ListAllEntitiesDto, organizationIds?: number[]): Promise { - let orderBy = `user.id`; - if (query.orderOn !== null && (query.orderOn === "id" || query.orderOn === "name")) { - orderBy = `user.${query.orderOn}`; - } - const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; - - let usersQuery = this.userRepository - .createQueryBuilder("user") - .innerJoin("user.requestedOrganizations", "org") - .addSelect("org.id") - .take(+query.limit) - .skip(+query.offset) - .orderBy(orderBy, order); - - if (organizationIds?.length) { - usersQuery = usersQuery.where("org.id IN (:...organizationIds)", { - organizationIds, - }); - } - - const [data, count] = await usersQuery.getManyAndCount(); - - return { - data: data.map(x => x as UserResponseDto), - count: count, - }; - } - - async hideWelcome(id: number): Promise { - const res = await this.userRepository.update(id, { showWelcomeScreen: false }); - return !!res.affected; - } - - parseBoolean(value: any): boolean { - if (value === 'false') { - return false; - } else { - return !!value; - } - } + } + }); + if (emails.length === 0) { + globalAdminPermission.users.forEach(user => { + emails.push(user.email); + }); + } + + return emails; + } + + async getAwaitingUsers(query?: ListAllEntitiesDto, organizationIds?: number[]): Promise { + let orderBy = `user.id`; + if (query.orderOn !== null && (query.orderOn === "id" || query.orderOn === "name")) { + orderBy = `user.${query.orderOn}`; + } + const order: "DESC" | "ASC" = query?.sort?.toLocaleUpperCase() == "DESC" ? "DESC" : "ASC"; + + let usersQuery = this.userRepository + .createQueryBuilder("user") + .innerJoin("user.requestedOrganizations", "org") + .addSelect("org.id") + .take(+query.limit) + .skip(+query.offset) + .orderBy(orderBy, order); + + if (organizationIds?.length) { + usersQuery = usersQuery.where("org.id IN (:...organizationIds)", { + organizationIds, + }); + } + + const [data, count] = await usersQuery.getManyAndCount(); + + return { + data: data.map(x => x as UserResponseDto), + count: count, + }; + } + + async hideWelcome(id: number): Promise { + const res = await this.userRepository.update(id, { showWelcomeScreen: false }); + return !!res.affected; + } + + parseBoolean(value: any): boolean { + if (value === "false") { + return false; + } else { + return !!value; + } + } }